-
Notifications
You must be signed in to change notification settings - Fork 2
763 parse and manipulate changes files #782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ArBridgeman
merged 32 commits into
main
from
refactoring/763-Parse_and_Manipulate_Changes_Files
Apr 17, 2026
+537
−0
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
2ba062b
#402: Created nox task to detect resolved GitHub security issues
ckunki 282591f
added typehint to get_vulnerabilities_from_latest_tag
ckunki 8b1e7b8
Validated warning in test
ckunki 91aebce
Renamed method resolved to resolved_vulnerabilities
ckunki 8d097a5
Renamed nox task and class SecurityAudit once again
ckunki 9bd15c9
Added integration test
ckunki ebfdff7
Added changes file parser
ckunki 41a85c1
added docstring and renamed methods
ckunki e8b6d82
Added support for list items with dash
ckunki 0d6242e
Updated docstring
ckunki 7308052
Refactored and renamed changes_file.py
ckunki c008cf4
Enhanced class Markdown
ckunki 9f7c93b
Refactored class Markdown
ckunki 0f1b346
Fixed bug when parsing intro and items
ckunki fc12b43
merged changes from changelog.py
ckunki f71604c
Refactored tests
ckunki 67fa82c
format:fix
ckunki 2331366
format:fix
ckunki efc20f5
Merge branch 'main' into feature/402-Created_nox_task_to_detect_resol…
ckunki 6e09021
Merge branch 'feature/402-Created_nox_task_to_detect_resolved_GitHub_…
ckunki f8b2d5b
Merge branch 'main' into sec-2-markdown
ckunki 742ced0
Updated changelog
ckunki b1b9a77
Merge branch 'main' into refactoring/763-Parse_and_Manipulate_Changes…
ckunki d7c34a8
Removed unused import
ckunki dcb1003
Fixed type errors
ckunki 11de13c
Apply suggestions from code review
ckunki 3374c43
Added support for numbered items in the changelog
ckunki 9a4dba7
Fixed review finding on bool(line)
ckunki 9420bfd
Merge branch 'main' into refactoring/763-Parse_and_Manipulate_Changes…
ckunki c6afb8b
nox -s format:fix
ckunki ccac9f8
FIxed sonar finding
ckunki 3dfac16
Merge branch 'main' into refactoring/763-Parse_and_Manipulate_Changes…
ckunki File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| """ | ||
| Class Markdown represents a file in Markdown syntax with some additional | ||
| constraints: | ||
|
|
||
| * The file must start with a title in the first line. | ||
| * Each subsequent title must be of a higher level, i.e. start with more "#" | ||
| characters than the top-level title. | ||
|
|
||
| Each title starts a section, optionally containing an additional intro and a | ||
| bullet list of items. | ||
|
|
||
| Each section can also contain subsections as children, hence sections can be | ||
| nested up to the top-level section representing the whole file. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import io | ||
| import re | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| class ParseError(Exception): | ||
| """ | ||
| Indicates inconsistencies when parsing a changelog from raw | ||
| text. E.g. a section with a body but no title. | ||
| """ | ||
|
|
||
|
|
||
| class IllegalChild(Exception): | ||
| """ | ||
| When adding a child to a parent with higher level title. | ||
| """ | ||
|
|
||
|
|
||
| def is_title(line: str) -> bool: | ||
| return (line != "") and line.startswith("#") | ||
|
|
||
|
|
||
| def is_list_item(line: str) -> bool: | ||
| return bool(re.match(r"^([*-]|\d+\.)", line)) | ||
|
|
||
|
|
||
| def is_intro(line: str) -> bool: | ||
| return (line != "") and not is_title(line) and not is_list_item(line) | ||
|
|
||
|
|
||
| def level(title: str) -> int: | ||
| """ | ||
| Return the hierarchical level of the title, i.e. the number of "#" | ||
| chars at the beginning of the title. | ||
| """ | ||
| return len(title) - len(title.lstrip("#")) | ||
|
|
||
|
|
||
| class Markdown: | ||
| """ | ||
| Represents a Markdown file or a section within a Markdown file. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| title: str, | ||
| intro: str = "", | ||
| items: str = "", | ||
| children: list[Markdown] | None = None, | ||
| ): | ||
| self.title = title.rstrip("\n") | ||
| self.intro = intro | ||
| self.items = items | ||
| children = children or [] | ||
| for child in children: | ||
| self._check(child) | ||
| self.children = children | ||
|
|
||
| def can_contain(self, child: Markdown) -> bool: | ||
| return level(self.title) < level(child.title) | ||
|
|
||
| def find(self, child_title: str) -> tuple[int, Markdown] | None: | ||
| """ | ||
| Return index and child having the specified title, or None if | ||
| there is none. | ||
| """ | ||
| for i, child in enumerate(self.children): | ||
| if child.title == child_title: | ||
| return i, child | ||
| return None | ||
|
|
||
| def child(self, title: str) -> Markdown | None: | ||
| """ | ||
| Retrieve the child with the specified title. | ||
| """ | ||
| return found[1] if (found := self.find(title)) else None | ||
|
|
||
| def _check(self, child: Markdown) -> Markdown: | ||
| if not self.can_contain(child): | ||
| raise IllegalChild( | ||
| f'Markdown section "{self.title}" cannot have "{child.title}" as child.' | ||
| ) | ||
| return child | ||
|
|
||
| def add_child(self, child: Markdown, pos: int = 1) -> Markdown: | ||
| """ | ||
| Insert the specified section as child at the specified position. | ||
| """ | ||
|
|
||
| self.children.insert(pos, self._check(child)) | ||
| return self | ||
|
|
||
| def replace_or_append_child(self, child: Markdown) -> Markdown: | ||
| """ | ||
| If there is a child with the same title, then replace this child; | ||
| otherwise, append the specified child. | ||
| """ | ||
|
|
||
| self._check(child) | ||
| if found := self.find(child.title): | ||
| self.children[found[0]] = child | ||
| else: | ||
| self.children.append(child) | ||
| return self | ||
|
|
||
| @property | ||
| def rendered(self) -> str: | ||
| def elements(): | ||
| yield from (self.title, self.intro, self.items) | ||
| yield from (c.rendered for c in self.children) | ||
|
|
||
| return "\n\n".join(e for e in elements() if e) | ||
|
|
||
| def __eq__(self, other) -> bool: | ||
| return ( | ||
| isinstance(other, Markdown) | ||
| and other.title == self.title | ||
| and other.intro == self.intro | ||
| and other.items == self.items | ||
| and other.children == self.children | ||
| ) | ||
|
|
||
| def __str__(self) -> str: | ||
| return self.rendered | ||
|
|
||
| @classmethod | ||
| def read(cls, file: Path) -> Markdown: | ||
| """ | ||
| Parse Markdown instance from the provided file. | ||
| """ | ||
|
|
||
| with file.open("r") as stream: | ||
| return cls.parse(stream) | ||
|
|
||
| @classmethod | ||
| def from_text(cls, text: str) -> Markdown: | ||
| """ | ||
| Parse Markdown instance from the provided text. | ||
| """ | ||
|
|
||
| return cls.parse(io.StringIO(text)) | ||
|
|
||
| @classmethod | ||
| def parse(cls, stream: io.TextIOBase) -> Markdown: | ||
| """ | ||
| Parse Markdown instance from the provided stream. | ||
| """ | ||
|
|
||
| line = stream.readline() | ||
| if not is_title(line): | ||
| raise ParseError( | ||
| f'First line of markdown file must be a title, but is "{line}"' | ||
| ) | ||
|
|
||
| section, line = cls._parse(stream, line) | ||
| if not line: | ||
| return section | ||
| raise ParseError( | ||
| f'Found additional line "{line}" after top-level section "{section.title}".' | ||
|
ArBridgeman marked this conversation as resolved.
|
||
| ) | ||
|
|
||
| @classmethod | ||
| def _parse(cls, stream: io.TextIOBase, title: str) -> tuple[Markdown, str]: | ||
| intro = "" | ||
| items = "" | ||
| children = [] | ||
|
|
||
| line = stream.readline() | ||
| while is_intro(line): | ||
| intro += line | ||
| line = stream.readline() | ||
| if is_list_item(line): | ||
| while line and not is_title(line): | ||
| items += line | ||
| line = stream.readline() | ||
| while is_title(line) and level(title) < level(line): | ||
| child, line = Markdown._parse(stream, title=line) | ||
| children.append(child) | ||
| return cls(title, intro.strip("\n"), items.strip("\n"), children), line | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.