From 42a86cc2094a369f6dc0af0245f99b04beee2b57 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sat, 24 May 2025 15:50:21 +0800 Subject: [PATCH 1/2] refactor(changelog): better typing, yield --- commitizen/changelog.py | 30 +++++++++++++++--------------- tests/test_changelog.py | 10 +++++----- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 704efe607..efcc2a060 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -29,10 +29,10 @@ import re from collections import OrderedDict, defaultdict -from collections.abc import Iterable +from collections.abc import Generator, Iterable, Mapping from dataclasses import dataclass from datetime import date -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from jinja2 import ( BaseLoader, @@ -84,7 +84,7 @@ def generate_tree_from_commits( changelog_message_builder_hook: MessageBuilderHook | None = None, changelog_release_hook: ChangelogReleaseHook | None = None, rules: TagRules | None = None, -) -> Iterable[dict]: +) -> Generator[dict[str, Any], None, None]: pat = re.compile(changelog_pattern) map_pat = re.compile(commit_parser, re.MULTILINE) body_map_pat = re.compile(commit_parser, re.MULTILINE | re.DOTALL) @@ -187,24 +187,24 @@ def process_commit_message( changes[change_type].append(msg) -def order_changelog_tree(tree: Iterable, change_type_order: list[str]) -> Iterable: +def order_changelog_tree( + tree: Iterable[Mapping[str, Any]], change_type_order: list[str] +) -> Generator[dict[str, Any], None, None]: if len(set(change_type_order)) != len(change_type_order): raise InvalidConfigurationError( f"Change types contain duplicates types ({change_type_order})" ) - sorted_tree = [] for entry in tree: - ordered_change_types = change_type_order + sorted( - set(entry["changes"].keys()) - set(change_type_order) - ) - changes = [ - (ct, entry["changes"][ct]) - for ct in ordered_change_types - if ct in entry["changes"] - ] - sorted_tree.append({**entry, **{"changes": OrderedDict(changes)}}) - return sorted_tree + yield { + **entry, + "changes": OrderedDict( + (ct, entry["changes"][ct]) + for ct in change_type_order + + sorted(set(entry["changes"].keys()) - set(change_type_order)) + if ct in entry["changes"] + ), + } def get_changelog_template(loader: BaseLoader, template: str) -> Template: diff --git a/tests/test_changelog.py b/tests/test_changelog.py index b1c7c802e..f98fa05f7 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1219,22 +1219,22 @@ def test_order_changelog_tree(change_type_order, expected_reordering): tree = changelog.order_changelog_tree(COMMITS_TREE, change_type_order) for index, entry in enumerate(tuple(tree)): - version = tree[index]["version"] + version = entry["version"] if version in expected_reordering: # Verify that all keys are present - assert [*tree[index].keys()] == [*COMMITS_TREE[index].keys()] + assert [*entry.keys()] == [*COMMITS_TREE[index].keys()] # Verify that the reorder only impacted the returned dict and not the original expected = expected_reordering[version] - assert [*tree[index]["changes"].keys()] == expected["sorted"] + assert [*entry["changes"].keys()] == expected["sorted"] assert [*COMMITS_TREE[index]["changes"].keys()] == expected["original"] else: - assert [*entry["changes"].keys()] == [*tree[index]["changes"].keys()] + assert [*entry["changes"].keys()] == [*entry["changes"].keys()] def test_order_changelog_tree_raises(): change_type_order = ["BREAKING CHANGE", "feat", "refactor", "feat"] with pytest.raises(InvalidConfigurationError) as excinfo: - changelog.order_changelog_tree(COMMITS_TREE, change_type_order) + list(changelog.order_changelog_tree(COMMITS_TREE, change_type_order)) assert "Change types contain duplicates types" in str(excinfo) From 89642c8374b0f82716b5778ac1640dfb4821cc25 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sat, 24 May 2025 18:27:17 +0800 Subject: [PATCH 2/2] refactor(containers): add unique list --- commitizen/changelog.py | 10 +++----- commitizen/commands/changelog.py | 14 +++++++---- commitizen/containers.py | 38 +++++++++++++++++++++++++++++ tests/test_changelog.py | 9 ------- tests/test_containers.py | 42 ++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 commitizen/containers.py create mode 100644 tests/test_containers.py diff --git a/commitizen/changelog.py b/commitizen/changelog.py index efcc2a060..d5fa9727c 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -42,8 +42,9 @@ Template, ) +from commitizen.containers import UniqueList from commitizen.cz.base import ChangelogReleaseHook -from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError +from commitizen.exceptions import NoCommitsFoundError from commitizen.git import GitCommit, GitTag from commitizen.tags import TagRules @@ -188,13 +189,8 @@ def process_commit_message( def order_changelog_tree( - tree: Iterable[Mapping[str, Any]], change_type_order: list[str] + tree: Iterable[Mapping[str, Any]], change_type_order: UniqueList[str] ) -> Generator[dict[str, Any], None, None]: - if len(set(change_type_order)) != len(change_type_order): - raise InvalidConfigurationError( - f"Change types contain duplicates types ({change_type_order})" - ) - for entry in tree: yield { **entry, diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 0e4efabfa..8da954166 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -10,6 +10,7 @@ from commitizen import changelog, defaults, factory, git, out from commitizen.changelog_formats import get_changelog_format from commitizen.config import BaseConfig +from commitizen.containers import UniqueList from commitizen.cz.base import ChangelogReleaseHook, MessageBuilderHook from commitizen.cz.utils import strip_local_version from commitizen.exceptions import ( @@ -75,11 +76,6 @@ def __init__(self, config: BaseConfig, args): self.change_type_map = ( self.config.settings.get("change_type_map") or self.cz.change_type_map ) - self.change_type_order = ( - self.config.settings.get("change_type_order") - or self.cz.change_type_order - or defaults.CHANGE_TYPE_ORDER - ) self.rev_range = args.get("rev_range") self.tag_format: str = ( args.get("tag_format") or self.config.settings["tag_format"] @@ -101,6 +97,14 @@ def __init__(self, config: BaseConfig, args): self.extras = args.get("extras") or {} self.export_template_to = args.get("export_template") + @property + def change_type_order(self) -> UniqueList[str]: + return UniqueList( + self.config.settings.get("change_type_order") # type: ignore + or self.cz.change_type_order + or defaults.CHANGE_TYPE_ORDER + ) + def _find_incremental_rev(self, latest_version: str, tags: list[GitTag]) -> str: """Try to find the 'start_rev'. diff --git a/commitizen/containers.py b/commitizen/containers.py new file mode 100644 index 000000000..4d574fda7 --- /dev/null +++ b/commitizen/containers.py @@ -0,0 +1,38 @@ +from collections.abc import Iterable +from typing import Generic, TypeVar + +T = TypeVar("T") + + +class UniqueList(Generic[T]): + def __init__(self, items: list[T]): + if len(items) != len(set(items)): + raise ValueError("Items must be unique") + self._items = items + + def __iter__(self): + return iter(self._items) + + def __getitem__(self, index): + return self._items[index] + + def __repr__(self): + return f"UniqueList({self._items})" + + def __add__(self, other: Iterable[T]) -> "UniqueList[T]": + # Support UniqueList + list or UniqueList + UniqueList + combined = self._items + list(other) + return UniqueList(combined) + + def __radd__(self, other: Iterable[T]) -> "UniqueList[T]": + combined = list(other) + self._items + return UniqueList(combined) + + def __len__(self) -> int: + return len(self._items) + + def __contains__(self, item: T) -> bool: + return item in self._items + + def __eq__(self, other: object) -> bool: + return isinstance(other, UniqueList) and self._items == other._items diff --git a/tests/test_changelog.py b/tests/test_changelog.py index f98fa05f7..2ec3e2c44 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -17,7 +17,6 @@ from commitizen.cz.conventional_commits.conventional_commits import ( ConventionalCommitsCz, ) -from commitizen.exceptions import InvalidConfigurationError from commitizen.version_schemes import Pep440 COMMITS_DATA: list[dict[str, Any]] = [ @@ -1231,14 +1230,6 @@ def test_order_changelog_tree(change_type_order, expected_reordering): assert [*entry["changes"].keys()] == [*entry["changes"].keys()] -def test_order_changelog_tree_raises(): - change_type_order = ["BREAKING CHANGE", "feat", "refactor", "feat"] - with pytest.raises(InvalidConfigurationError) as excinfo: - list(changelog.order_changelog_tree(COMMITS_TREE, change_type_order)) - - assert "Change types contain duplicates types" in str(excinfo) - - def test_render_changelog( gitcommits, tags, changelog_content, any_changelog_format: ChangelogFormat ): diff --git a/tests/test_containers.py b/tests/test_containers.py new file mode 100644 index 000000000..151fb8c4a --- /dev/null +++ b/tests/test_containers.py @@ -0,0 +1,42 @@ +import pytest + +from commitizen.containers import UniqueList + + +def test_unique_list(): + # Test initialization with unique items + unique_list = UniqueList([1, 2, 3]) + assert list(unique_list) == [1, 2, 3] + + # Test initialization with duplicate items + with pytest.raises(ValueError, match="Items must be unique"): + UniqueList([1, 1, 2]) + + # Test iteration + items = [1, 2, 3] + unique_list = UniqueList(items) + assert [x for x in unique_list] == items + + # Test indexing + assert unique_list[0] == 1 + assert unique_list[1] == 2 + assert unique_list[2] == 3 + + # Test string representation + assert repr(unique_list) == "UniqueList([1, 2, 3])" + + # Test with different types + string_list = UniqueList(["a", "b", "c"]) + assert list(string_list) == ["a", "b", "c"] + + # Test add + assert unique_list + [4, 5, 6] == UniqueList([1, 2, 3, 4, 5, 6]) + assert [4, 5, 6] + unique_list == UniqueList([4, 5, 6, 1, 2, 3]) + + # Test contains + assert 1 in unique_list + assert 3 in unique_list + assert 7 not in unique_list + + # Test len + assert len(unique_list) == 3