-
Notifications
You must be signed in to change notification settings - Fork 0
Fature/99 super chapters #281
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
Merged
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
de66dc7
feat: add super-chapters support for grouping chapters by label
miroslavpojer 8895bc2
feat: implement super-chapters functionality with uncategorized fallback
miroslavpojer b5b068c
fix: improve unclaimed IDs handling and normalize label input in Cust…
miroslavpojer d9291bc
Fixed review comments.
miroslavpojer d0250fd
Fix review comments.
miroslavpojer 8679c2a
feat: enhance hierarchy issue rendering logic for open and closed par…
miroslavpojer 30754f3
feat: add super-chapters support for grouping chapters by label
miroslavpojer 203a8cf
Fixed after merge issues.
miroslavpojer b93b0db
Implemented partial split by super chapter label.
miroslavpojer abfb0ff
fix: simplify comment in CustomChapters class for clarity
miroslavpojer 20cf902
Merge branch 'master' into fature/99-Super-chapters
miroslavpojer cd6cadc
refactor: streamline method calls in CustomChapters and HierarchyIssu…
miroslavpojer bafb0eb
Fixed review comments.
miroslavpojer 583978f
Self-review and reduction of pylint exceptions.
miroslavpojer 69d5b2d
Fixed black.
miroslavpojer 4666b0d
refactor: update type hints in CustomChapters and HierarchyIssueRecor…
miroslavpojer 2514d44
Fix review notes.
miroslavpojer f72bcc5
Fix review comments.
miroslavpojer 2da077c
Fix review comments.
miroslavpojer 8382e55
Fixed review comments.
miroslavpojer 85774a5
Fixed review comment.
miroslavpojer bf1d759
Add caching for super-chapters input to optimize parsing
miroslavpojer d7a11ca
Fixed review comments.
miroslavpojer a6490c2
Fix review notes.
miroslavpojer fdd73c9
Fixed review comment.
miroslavpojer bc4354a
Refactor docstrings to improve parameter descriptions and return valu…
miroslavpojer 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
Some comments aren't visible on the classic Files Changed page.
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
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
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 |
|---|---|---|
|
|
@@ -20,18 +20,28 @@ | |
| """ | ||
|
|
||
| import logging | ||
| from dataclasses import dataclass, field | ||
| from typing import Any, Iterable | ||
|
|
||
| from release_notes_generator.action_inputs import ActionInputs | ||
| from release_notes_generator.chapters.base_chapters import BaseChapters | ||
| from release_notes_generator.model.chapter import Chapter | ||
| from release_notes_generator.utils.constants import UNCATEGORIZED_CHAPTER_TITLE | ||
| from release_notes_generator.model.record.commit_record import CommitRecord | ||
| from release_notes_generator.model.record.hierarchy_issue_record import HierarchyIssueRecord | ||
| from release_notes_generator.model.record.record import Record | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| @dataclass | ||
| class SuperChapter: | ||
| """A label-based grouping that wraps regular chapters in the rendered output.""" | ||
|
|
||
| title: str | ||
| labels: list[str] = field(default_factory=list) | ||
|
|
||
|
|
||
| def _normalize_labels(raw: Any) -> list[str]: # helper for multi-label | ||
| """Normalize a raw labels definition into an ordered, de-duplicated list. | ||
|
|
||
|
|
@@ -75,6 +85,11 @@ class CustomChapters(BaseChapters): | |
| A class used to represent the custom chapters in the release notes. | ||
| """ | ||
|
|
||
| def __init__(self, sort_ascending: bool = True, print_empty_chapters: bool = True): | ||
| super().__init__(sort_ascending, print_empty_chapters) | ||
| self._super_chapters: list[SuperChapter] = [] | ||
| self._record_labels: dict[str, list[str]] = {} | ||
|
|
||
| def _find_catch_open_hierarchy_chapter(self) -> Chapter | None: | ||
| """Return the first chapter with catch_open_hierarchy enabled, or None.""" | ||
| for ch in self.chapters.values(): | ||
|
|
@@ -135,6 +150,7 @@ def populate(self, records: dict[str, Record]) -> None: | |
| continue | ||
|
|
||
| record_labels = getattr(record, "labels", []) | ||
| self._record_labels[record_id] = list(record_labels) | ||
|
|
||
| # Conditional Custom Chapter gate: intercept open hierarchy parents before label routing. | ||
| # Note: precedes the record_labels early-exit so label-less HierarchyIssueRecord | ||
|
|
@@ -175,9 +191,21 @@ def to_string(self) -> str: | |
| """ | ||
| Converts the custom chapters to a string, excluding hidden chapters. | ||
|
|
||
| When super chapters are configured, records are grouped under super-chapter | ||
| headings (``## Title``) with regular chapters nested inside (``### Title``). | ||
| A record may appear in multiple super chapters. Chapters with no matching | ||
| records under a given super chapter are governed by ``print_empty_chapters``. | ||
|
|
||
| Returns: | ||
| str: The chapters as a string with hidden chapters filtered out. | ||
| """ | ||
| if self._super_chapters: | ||
| return self._to_string_with_super_chapters() | ||
|
|
||
| return self._to_string_flat() | ||
|
|
||
| def _to_string_flat(self) -> str: | ||
| """Render chapters without super-chapter grouping (original behaviour).""" | ||
| result = "" | ||
| for chapter in self._sorted_chapters(): | ||
| # Skip hidden chapters from output | ||
|
|
@@ -196,6 +224,75 @@ def to_string(self) -> str: | |
| # Note: strip is required to remove leading newline chars when empty chapters are not printed option | ||
| return result.strip() | ||
|
|
||
| def _collect_super_chapter_ids(self, sc: SuperChapter) -> set[str]: | ||
| """Return record IDs whose labels match the given super chapter.""" | ||
| matching: set[str] = set() | ||
| for rid, labels in self._record_labels.items(): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What rid stands for please?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Short for record ID — the unique PR or issue number used as the key in |
||
| if any(lbl in sc.labels for lbl in labels): | ||
| matching.add(rid) | ||
| return matching | ||
|
|
||
| def _render_chapter_for_ids(self, chapter: Chapter, matching_ids: set[str]) -> str: | ||
| """Render a single chapter filtered to matching_ids, delegating to Chapter.to_string.""" | ||
| original_rows = chapter.rows | ||
| chapter.rows = { | ||
| rid: row for rid, row in original_rows.items() if str(rid) in matching_ids or rid in matching_ids | ||
| } | ||
| try: | ||
| return chapter.to_string(sort_ascending=self.sort_ascending, print_empty_chapters=self.print_empty_chapters) | ||
| finally: | ||
| chapter.rows = original_rows | ||
|
|
||
| def _to_string_with_super_chapters(self) -> str: | ||
| """Render chapters grouped under super-chapter headings.""" | ||
| # Collect all record IDs claimed by at least one super chapter | ||
| all_super_labels: set[str] = set() | ||
| for sc in self._super_chapters: | ||
| all_super_labels.update(sc.labels) | ||
|
|
||
| claimed_ids: set[str] = set() | ||
| for rid, labels in self._record_labels.items(): | ||
| if any(lbl in all_super_labels for lbl in labels): | ||
| claimed_ids.add(rid) | ||
|
|
||
| result = "" | ||
| for sc in self._super_chapters: | ||
| matching_ids = self._collect_super_chapter_ids(sc) | ||
|
|
||
| sc_block = "" | ||
| for chapter in self._sorted_chapters(): | ||
| if chapter.hidden: | ||
| continue | ||
| ch_str = self._render_chapter_for_ids(chapter, matching_ids) | ||
| if ch_str: | ||
| sc_block += ch_str + "\n\n" | ||
|
|
||
| if sc_block.strip(): | ||
| result += f"## {sc.title}\n{sc_block}" | ||
| elif self.print_empty_chapters: | ||
| result += f"## {sc.title}\nNo entries detected.\n\n" | ||
|
|
||
| # Fallback: records not claimed by any super chapter | ||
| unclaimed_ids: set[str] = set() | ||
| for chapter in self._sorted_chapters(): | ||
| for row_id in chapter.rows: | ||
| row_id_str = str(row_id) | ||
| if row_id_str not in claimed_ids: | ||
| unclaimed_ids.add(row_id_str) | ||
|
|
||
| if unclaimed_ids: | ||
| uc_block = "" | ||
| for chapter in self._sorted_chapters(): | ||
| if chapter.hidden: | ||
| continue | ||
| ch_str = self._render_chapter_for_ids(chapter, unclaimed_ids) | ||
| if ch_str: | ||
| uc_block += ch_str + "\n\n" | ||
| if uc_block.strip(): | ||
| result += f"## {UNCATEGORIZED_CHAPTER_TITLE}\n{uc_block}" | ||
|
|
||
| return result.strip() | ||
|
|
||
| def from_yaml_array(self, chapters: list[dict[str, Any]]) -> "CustomChapters": # type: ignore[override] | ||
| """ | ||
| Populate the custom chapters from a list of dict objects. | ||
|
|
@@ -262,8 +359,45 @@ def from_yaml_array(self, chapters: list[dict[str, Any]]) -> "CustomChapters": | |
| parsed_order=parsed_order, | ||
| ) | ||
|
|
||
| # Parse super-chapter definitions from action inputs | ||
| self._super_chapters = self._parse_super_chapters(ActionInputs.get_super_chapters()) | ||
|
|
||
| return self | ||
|
|
||
| @staticmethod | ||
| def _parse_super_chapters(raw_super_chapters: list[dict[str, str]]) -> list[SuperChapter]: | ||
| """Parse super-chapter YAML definitions into SuperChapter instances. | ||
|
|
||
| Parameters: | ||
| raw_super_chapters: Parsed YAML list of dicts with 'title' and 'label'/'labels'. | ||
|
|
||
| Returns: | ||
| List of validated SuperChapter instances; invalid entries are skipped with a warning. | ||
| """ | ||
| result: list[SuperChapter] = [] | ||
| for entry in raw_super_chapters: | ||
| if not isinstance(entry, dict): | ||
| logger.warning("Skipping super-chapter definition with invalid type %s: %s", type(entry), entry) | ||
| continue | ||
| if "title" not in entry: | ||
| logger.warning("Skipping super-chapter without title key: %s", entry) | ||
| continue | ||
| title = entry["title"] | ||
|
|
||
| raw_labels = entry.get("labels", entry.get("label")) | ||
| if raw_labels is None: | ||
| logger.warning("Super-chapter '%s' has no 'label' or 'labels' key; skipping", title) | ||
| continue | ||
| labels_input: str | list[str] = [raw_labels] if isinstance(raw_labels, str) else raw_labels | ||
| normalized = _normalize_labels(labels_input) | ||
|
miroslavpojer marked this conversation as resolved.
Outdated
|
||
| if not normalized: | ||
| logger.warning("Super-chapter '%s' labels definition empty after normalization; skipping", title) | ||
| continue | ||
|
|
||
| result.append(SuperChapter(title=title, labels=normalized)) | ||
| logger.debug("Registered super-chapter '%s' with labels: %s", title, normalized) | ||
| return result | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| @staticmethod | ||
| def _parse_bool_flag(title: str, raw: Any, key: str) -> bool: | ||
| """Parse and validate a boolean flag from a chapter config entry. | ||
|
|
||
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
Oops, something went wrong.
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.