From 515c02371c11597e79dff995632ec087bd64b966 Mon Sep 17 00:00:00 2001 From: Mark Minakov Date: Sun, 8 Oct 2023 19:52:40 +0300 Subject: [PATCH 1/2] feat: Customizing code convention Issue: pawamoy#59 Resolves: pawamoy#59 --- docs/usage.md | 27 ++++++++++++ src/git_changelog/build.py | 9 ++++ src/git_changelog/cli.py | 13 ++++++ src/git_changelog/commit.py | 87 +++++++++++++++++++++++-------------- tests/test_commit.py | 56 +++++++++++++++++++++++- 5 files changed, 159 insertions(+), 33 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 180bfd2..92aeeae 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -503,6 +503,33 @@ template = "keepachangelog" version-regex = "^## \\\\[(?Pv?[^\\\\]]+)" ``` +## Custom types + +Configuration files offer greater flexibility compared to using CLI arguments. +You can overwrite default types with the `rewrite-convention` parameter. + +In that case `sections` is required and `minor-types` is strongly recommended. + +This can be useful for custom conventions or translating sections in your changelog. + +```toml +[tool.git-changelog] +convention = "conventional" +sections = "build,chore,doc,n,feat" +minor-types = "feat,n" +... + +[tool.git-changelog.rewrite-convention] +build = "Build" +chore = "Chore" +ci = "Continuous Integration" +deps = "Dependencies" +doc = "Documentation" +feat = "Features" +n = "Notes" + +``` + [keepachangelog]: https://keepachangelog.com/en/1.0.0/ [conventional-commit]: https://www.conventionalcommits.org/en/v1.0.0-beta.4/ [jinja]: https://jinja.palletsprojects.com/en/3.1.x/ diff --git a/src/git_changelog/build.py b/src/git_changelog/build.py index 5a5aae7..8e9662d 100644 --- a/src/git_changelog/build.py +++ b/src/git_changelog/build.py @@ -170,6 +170,8 @@ def __init__( sections: list[str] | None = None, bump_latest: bool = False, bump: str | None = None, + rewrite_convention: dict[str, str] | None = None, + minor_types: str | None = None, ): """Initialization method. @@ -182,6 +184,9 @@ def __init__( sections: The sections to render (features, bug fixes, etc.). bump_latest: Deprecated, use `bump="auto"` instead. Whether to try and bump latest version to guess new one. bump: Whether to try and bump to a given version. + rewrite_convention: A dictionary mapping type to section, intended to modify the default convention.TYPES. + If provided, the 'sections' argument becomes mandatory. + minor_types: Types signifying a minor version change. String separated by commas. """ self.repository: str | Path = repository self.parse_provider_refs: bool = parse_provider_refs @@ -209,6 +214,10 @@ def __init__( # set convention if isinstance(convention, str): try: + if rewrite_convention: + self.CONVENTION[convention].replace_types(rewrite_convention) + if minor_types: + self.CONVENTION[convention].update_minor_list(minor_types) convention = self.CONVENTION[convention]() except KeyError: print( # noqa: T201 diff --git a/src/git_changelog/cli.py b/src/git_changelog/cli.py index b72e231..1fb3b44 100644 --- a/src/git_changelog/cli.py +++ b/src/git_changelog/cli.py @@ -65,6 +65,8 @@ "sections": None, "template": "keepachangelog", "version_regex": DEFAULT_VERSION_REGEX, + "rewrite_convention": None, + "minor_types": None, } @@ -440,6 +442,8 @@ def build_and_render( omit_empty_versions: bool = False, # noqa: FBT001,FBT002 provider: str | None = None, bump: str | None = None, + rewrite_convention: dict | None = None, + minor_types: str | None = None, ) -> tuple[Changelog, str]: """Build a changelog and render it. @@ -462,6 +466,9 @@ def build_and_render( omit_empty_versions: Whether to omit empty versions from the output. provider: Provider class used by this repository. bump: Whether to try and bump to a given version. + rewrite_convention: A dictionary mapping type to section, intended to modify the default convention.TYPES. + If provided, the 'sections' argument becomes mandatory. + minor_types: Types signifying a minor version change. String separated by commas. Raises: ValueError: When some arguments are incompatible or missing. @@ -486,6 +493,10 @@ def build_and_render( if in_place and output is sys.stdout: raise ValueError("Cannot write in-place to stdout") + if rewrite_convention and not sections: + raise ValueError("When using 'rewrite-convention', please specify the " + "sections you wish to render, e.g., sections='feat,docs'.") + # get provider provider_class = providers[provider] if provider else None @@ -504,6 +515,8 @@ def build_and_render( parse_trailers=parse_trailers, sections=sections, bump=bump, + rewrite_convention=rewrite_convention, + minor_types=minor_types, ) # remove empty versions from changelog data diff --git a/src/git_changelog/commit.py b/src/git_changelog/commit.py index 0f8bc41..c4ab94b 100644 --- a/src/git_changelog/commit.py +++ b/src/git_changelog/commit.py @@ -198,6 +198,38 @@ def _format_sections_help(cls) -> str: """, ) + def is_minor(self, commit_type: str) -> bool: + """Tell if this commit is worth a minor bump. + + Arguments: + commit_type: The commit type. + + Returns: + Whether it's a minor commit. + """ + return commit_type in [self.TYPES[t] for t in self.MINOR_TYPES if t in self.TYPES] + + @classmethod + def replace_types(cls, types: dict[str, str]) -> None: + """Replace default TYPES with dict. + + Arguments: + types: Dict with custom types. + """ + cls.TYPES = types + + @classmethod + def update_minor_list(cls, commit_type: str) -> None: + """Updates the MINOR_TYPES class variable with the provided comma-separated commit types. + + Arguments: + commit_type (str): A comma-separated string of commit types to be considered as minor. + + Example: + CommitConvention.update_minor_list("feat,fix,update") + """ + cls.MINOR_TYPES = commit_type.split(",") + class BasicConvention(CommitConvention): """Basic commit message convention.""" @@ -210,8 +242,7 @@ class BasicConvention(CommitConvention): "merge": "Merged", "doc": "Documented", } - - TYPE_REGEX: ClassVar[Pattern] = re.compile(r"^(?P(%s))" % "|".join(TYPES.keys()), re.I) + MINOR_TYPES: ClassVar[list] = ["add"] BREAK_REGEX: ClassVar[Pattern] = re.compile( r"^break(s|ing changes?)?[ :].+$", re.I | re.MULTILINE, @@ -223,6 +254,11 @@ class BasicConvention(CommitConvention): TYPES["remove"], ] + @property + def type_regex(self) -> re.Pattern: + """Type regex.""" + return re.compile(r"^(?P(%s))" % "|".join(self.TYPES.keys()), re.I) + def parse_commit(self, commit: Commit) -> dict[str, str | bool]: # noqa: D102 commit_type = self.parse_type(commit.subject) message = "\n".join([commit.subject, *commit.body]) @@ -246,22 +282,11 @@ def parse_type(self, commit_subject: str) -> str: Returns: The commit type. """ - type_match = self.TYPE_REGEX.match(commit_subject) + type_match = self.type_regex.match(commit_subject) if type_match: return self.TYPES.get(type_match.groupdict()["type"].lower(), "") return "" - def is_minor(self, commit_type: str) -> bool: - """Tell if this commit is worth a minor bump. - - Arguments: - commit_type: The commit type. - - Returns: - Whether it's a minor commit. - """ - return commit_type == self.TYPES["add"] - def is_major(self, commit_message: str) -> bool: """Tell if this commit is worth a major bump. @@ -294,9 +319,7 @@ class AngularConvention(CommitConvention): "test": "Tests", "tests": "Tests", } - SUBJECT_REGEX: ClassVar[Pattern] = re.compile( - r"^(?P(%s))(?:\((?P.+)\))?: (?P.+)$" % ("|".join(TYPES.keys())), # (%) - ) + MINOR_TYPES: ClassVar[list] = ["feat"] BREAK_REGEX: ClassVar[Pattern] = re.compile( r"^break(s|ing changes?)?[ :].+$", re.I | re.MULTILINE, @@ -309,6 +332,13 @@ class AngularConvention(CommitConvention): TYPES["perf"], ] + @property + def subject_regex(self) -> re.Pattern: + """Subject regex.""" + return re.compile( + r"^(?P(%s))(?:\((?P.+)\))?: (?P.+)$" % ("|".join(self.TYPES.keys())), # (%) + ) + def parse_commit(self, commit: Commit) -> dict[str, str | bool]: # noqa: D102 subject = self.parse_subject(commit.subject) message = "\n".join([commit.subject, *commit.body]) @@ -334,24 +364,13 @@ def parse_subject(self, commit_subject: str) -> dict[str, str]: Returns: The parsed data. """ - subject_match = self.SUBJECT_REGEX.match(commit_subject) + subject_match = self.subject_regex.match(commit_subject) if subject_match: dct = subject_match.groupdict() dct["type"] = self.TYPES[dct["type"]] return dct return {"type": "", "scope": "", "subject": commit_subject} - def is_minor(self, commit_type: str) -> bool: - """Tell if this commit is worth a minor bump. - - Arguments: - commit_type: The commit type. - - Returns: - Whether it's a minor commit. - """ - return commit_type == self.TYPES["feat"] - def is_major(self, commit_message: str) -> bool: """Tell if this commit is worth a major bump. @@ -369,9 +388,13 @@ class ConventionalCommitConvention(AngularConvention): TYPES: ClassVar[dict[str, str]] = AngularConvention.TYPES DEFAULT_RENDER: ClassVar[list[str]] = AngularConvention.DEFAULT_RENDER - SUBJECT_REGEX: ClassVar[Pattern] = re.compile( - r"^(?P(%s))(?:\((?P.+)\))?(?P!)?: (?P.+)$" % ("|".join(TYPES.keys())), # (%) - ) + + @property + def subject_regex(self) -> re.Pattern: + """Subject regex.""" + return re.compile( + r"^(?P(%s))(?:\((?P.+)\))?(?P!)?: (?P.+)$" % ("|".join(self.TYPES.keys())), # (%) + ) def parse_commit(self, commit: Commit) -> dict[str, str | bool]: # noqa: D102 subject = self.parse_subject(commit.subject) diff --git a/tests/test_commit.py b/tests/test_commit.py index 04ea9c1..68ba706 100644 --- a/tests/test_commit.py +++ b/tests/test_commit.py @@ -4,7 +4,12 @@ import pytest -from git_changelog.commit import Commit +from git_changelog.commit import ( + AngularConvention, + BasicConvention, + Commit, + ConventionalCommitConvention, +) @pytest.mark.parametrize( @@ -35,3 +40,52 @@ def test_parsing_trailers(body: str, expected_trailers: dict[str, str]) -> None: parse_trailers=True, ) assert commit.trailers == expected_trailers + + +@pytest.fixture() +def _reserve_types() -> None: + """Fixture to preserve the conventional types.""" + original_types = { + AngularConvention: [dict(AngularConvention.TYPES), list(AngularConvention.MINOR_TYPES)], + BasicConvention: [dict(BasicConvention.TYPES), list(BasicConvention.MINOR_TYPES)], + ConventionalCommitConvention: [dict(ConventionalCommitConvention.TYPES), list(ConventionalCommitConvention.MINOR_TYPES)], + } + yield + AngularConvention.TYPES = original_types[AngularConvention][0] + BasicConvention.TYPES = original_types[BasicConvention][0] + ConventionalCommitConvention.TYPES = original_types[ConventionalCommitConvention][0] + AngularConvention.MINOR_TYPES = original_types[AngularConvention][1] + BasicConvention.MINOR_TYPES = original_types[BasicConvention][1] + ConventionalCommitConvention.MINOR_TYPES = original_types[ConventionalCommitConvention][1] + + +@pytest.mark.usefixtures("_reserve_types") +def test_replace_types() -> None: + """Test that the TYPES attribute is replaced correctly in various conventions.""" + _new_types = {"n": "Notes", "o": "Other", "d": "Draft"} + for convention in [AngularConvention, BasicConvention, ConventionalCommitConvention]: + assert _new_types != convention.TYPES + convention.replace_types(_new_types) + assert _new_types == convention.TYPES + + +@pytest.mark.usefixtures("_reserve_types") +def test_is_minor_works_with_custom_minor_types() -> None: + """Test that custom minor types are correctly recognized as minor changes.""" + _new_types = {"n": "Notes", "o": "Other", "d": "Draft"} + _minor_types = "n,o" + for convention in [AngularConvention, BasicConvention, ConventionalCommitConvention]: + subject = "n: Added a new feature" + commit = Commit( + commit_hash="aaaaaaa", + subject=subject, + body=[""], + author_date="1574340645", + committer_date="1574340645", + ) + convention.replace_types(_new_types) + convention.update_minor_list(_minor_types) + commit_dict = convention().parse_commit(commit) + assert not commit_dict["is_major"] + assert commit_dict["is_minor"] + assert not commit_dict["is_patch"] From 4739f4d941378bd3f99b84405254ae763ef8c2b7 Mon Sep 17 00:00:00 2001 From: Mark Minakov Date: Sun, 8 Oct 2023 20:19:26 +0300 Subject: [PATCH 2/2] style: Resolve duty check-types Issue: pawamoy#59 Resolves: pawamoy#59 --- src/git_changelog/commit.py | 1 + tests/test_commit.py | 39 +++++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/git_changelog/commit.py b/src/git_changelog/commit.py index c4ab94b..804880f 100644 --- a/src/git_changelog/commit.py +++ b/src/git_changelog/commit.py @@ -161,6 +161,7 @@ class CommitConvention(ABC): TYPE_REGEX: ClassVar[Pattern] BREAK_REGEX: ClassVar[Pattern] DEFAULT_RENDER: ClassVar[list[str]] + MINOR_TYPES: ClassVar[list[str]] @abstractmethod def parse_commit(self, commit: Commit) -> dict[str, str | bool]: diff --git a/tests/test_commit.py b/tests/test_commit.py index 68ba706..5d27052 100644 --- a/tests/test_commit.py +++ b/tests/test_commit.py @@ -2,12 +2,16 @@ from __future__ import annotations +from abc import ABCMeta +from typing import Generator + import pytest from git_changelog.commit import ( AngularConvention, BasicConvention, Commit, + CommitConvention, ConventionalCommitConvention, ) @@ -43,20 +47,23 @@ def test_parsing_trailers(body: str, expected_trailers: dict[str, str]) -> None: @pytest.fixture() -def _reserve_types() -> None: +def _reserve_types() -> Generator[None, None, None]: """Fixture to preserve the conventional types.""" - original_types = { - AngularConvention: [dict(AngularConvention.TYPES), list(AngularConvention.MINOR_TYPES)], - BasicConvention: [dict(BasicConvention.TYPES), list(BasicConvention.MINOR_TYPES)], - ConventionalCommitConvention: [dict(ConventionalCommitConvention.TYPES), list(ConventionalCommitConvention.MINOR_TYPES)], + original_types: dict[type[CommitConvention], tuple[dict[str, str], list[str]]] = { + AngularConvention: (dict(AngularConvention.TYPES), list(AngularConvention.MINOR_TYPES)), + BasicConvention: (dict(BasicConvention.TYPES), list(BasicConvention.MINOR_TYPES)), + ConventionalCommitConvention: (dict(ConventionalCommitConvention.TYPES), + list(ConventionalCommitConvention.MINOR_TYPES)), } + yield - AngularConvention.TYPES = original_types[AngularConvention][0] - BasicConvention.TYPES = original_types[BasicConvention][0] - ConventionalCommitConvention.TYPES = original_types[ConventionalCommitConvention][0] - AngularConvention.MINOR_TYPES = original_types[AngularConvention][1] - BasicConvention.MINOR_TYPES = original_types[BasicConvention][1] - ConventionalCommitConvention.MINOR_TYPES = original_types[ConventionalCommitConvention][1] + + AngularConvention.TYPES = dict(original_types[AngularConvention][0]) + BasicConvention.TYPES = dict(original_types[BasicConvention][0]) + ConventionalCommitConvention.TYPES = dict(original_types[ConventionalCommitConvention][0]) + AngularConvention.MINOR_TYPES = list(original_types[AngularConvention][1]) + BasicConvention.MINOR_TYPES = list(original_types[BasicConvention][1]) + ConventionalCommitConvention.MINOR_TYPES = list(original_types[ConventionalCommitConvention][1]) @pytest.mark.usefixtures("_reserve_types") @@ -85,7 +92,9 @@ def test_is_minor_works_with_custom_minor_types() -> None: ) convention.replace_types(_new_types) convention.update_minor_list(_minor_types) - commit_dict = convention().parse_commit(commit) - assert not commit_dict["is_major"] - assert commit_dict["is_minor"] - assert not commit_dict["is_patch"] + if not isinstance(convention, ABCMeta): + conv = convention() + commit_dict = conv.parse_commit(commit) + assert not commit_dict["is_major"] + assert commit_dict["is_minor"] + assert not commit_dict["is_patch"]