From 335d266424a0b07bd6066b1fc282d8fb681681dd Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:14:24 -0400 Subject: [PATCH 01/21] refactor(parser-base): re-define default option initialization --- semantic_release/commit_parser/_base.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/semantic_release/commit_parser/_base.py b/semantic_release/commit_parser/_base.py index c104ee579..03460c0e3 100644 --- a/semantic_release/commit_parser/_base.py +++ b/semantic_release/commit_parser/_base.py @@ -65,10 +65,12 @@ def __init__(self, options: parser_options) -> None: ... """ - parser_options: type[_OPTS] + def __init__(self, options: _OPTS | None = None) -> None: + self.options: _OPTS = options if options is not None else self.get_default_options() - def __init__(self, options: _OPTS) -> None: - self.options = options + @staticmethod + @abstractmethod + def get_default_options() -> _OPTS: ... @abstractmethod def parse(self, commit: Commit) -> _TT: ... From 365308da021c5a9189eccac1bf030ae56148e5dd Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:15:15 -0400 Subject: [PATCH 02/21] refactor(parser-angular): implement `get_default_options()` --- semantic_release/commit_parser/angular.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/semantic_release/commit_parser/angular.py b/semantic_release/commit_parser/angular.py index 92a391d15..13936836d 100644 --- a/semantic_release/commit_parser/angular.py +++ b/semantic_release/commit_parser/angular.py @@ -62,13 +62,11 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]): commits. See https://www.conventionalcommits.org/en/v1.0.0-beta.4/ """ - parser_options = AngularParserOptions - - def __init__(self, options: AngularParserOptions) -> None: + def __init__(self, options: AngularParserOptions | None = None) -> None: super().__init__(options) self.re_parser = re.compile( rf""" - (?P{"|".join(options.allowed_tags)}) # e.g. feat + (?P{"|".join(self.options.allowed_tags)}) # e.g. feat (?:\((?P[^\n]+)\))? # or feat(parser) (?P!)?:\s+ # breaking if feat!: (?P[^\n]+) # commit subject @@ -77,6 +75,10 @@ def __init__(self, options: AngularParserOptions) -> None: flags=re.VERBOSE | re.DOTALL, ) + @staticmethod + def get_default_options() -> AngularParserOptions: + return AngularParserOptions() + # Maybe this can be cached as an optimisation, similar to how # mypy/pytest use their own caching directories, for very large commit # histories? From 1f7eab596b169b1b58e7d8f2de379c8562608faa Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:15:29 -0400 Subject: [PATCH 03/21] refactor(parser-emoji): implement `get_default_options()` --- semantic_release/commit_parser/emoji.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/semantic_release/commit_parser/emoji.py b/semantic_release/commit_parser/emoji.py index f0ab4f218..5d88952a4 100644 --- a/semantic_release/commit_parser/emoji.py +++ b/semantic_release/commit_parser/emoji.py @@ -57,7 +57,9 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]): the commit subject in the changelog. """ - parser_options = EmojiParserOptions + @staticmethod + def get_default_options() -> EmojiParserOptions: + return EmojiParserOptions() def parse(self, commit: Commit) -> ParseResult: all_emojis = ( From 565eb050af2277de77f7baf6ab3b4da12eaee984 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:16:05 -0400 Subject: [PATCH 04/21] refactor(parser-scipy): implement `get_default_options()` --- semantic_release/commit_parser/scipy.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/semantic_release/commit_parser/scipy.py b/semantic_release/commit_parser/scipy.py index 0503cfa2b..2fb76b2d7 100644 --- a/semantic_release/commit_parser/scipy.py +++ b/semantic_release/commit_parser/scipy.py @@ -117,9 +117,7 @@ def __post_init__(self) -> None: class ScipyCommitParser(CommitParser[ParseResult, ScipyParserOptions]): """Parser for scipy-style commit messages""" - parser_options = ScipyParserOptions - - def __init__(self, options: ScipyParserOptions) -> None: + def __init__(self, options: ScipyParserOptions | None = None) -> None: super().__init__(options) self.re_parser = re.compile( rf"(?P{_COMMIT_FILTER})?" @@ -130,6 +128,10 @@ def __init__(self, options: ScipyParserOptions) -> None: re.DOTALL, ) + @staticmethod + def get_default_options() -> ScipyParserOptions: + return ScipyParserOptions() + def parse(self, commit: Commit) -> ParseResult: message = str(commit.message) parsed = self.re_parser.match(message) From ca3391581a5df05a934ee874cf68d9ab455038d6 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:16:18 -0400 Subject: [PATCH 05/21] refactor(parser-tag): implement `get_default_options()` --- semantic_release/commit_parser/tag.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/semantic_release/commit_parser/tag.py b/semantic_release/commit_parser/tag.py index dc210c5b5..4272ee339 100644 --- a/semantic_release/commit_parser/tag.py +++ b/semantic_release/commit_parser/tag.py @@ -34,7 +34,9 @@ class TagCommitParser(CommitParser[ParseResult, TagParserOptions]): first line as changelog content. """ - parser_options = TagParserOptions + @staticmethod + def get_default_options() -> TagParserOptions: + return TagParserOptions() def parse(self, commit: Commit) -> ParseResult: message = str(commit.message) From 7dc4be935c5494d6b3dde7cd3d59c04e171ef89e Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:18:21 -0400 Subject: [PATCH 06/21] refactor(cli-config): use `get_default_options()` for parser option initialization --- semantic_release/cli/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/semantic_release/cli/config.py b/semantic_release/cli/config.py index 3f7a353e4..c6df2dbc0 100644 --- a/semantic_release/cli/config.py +++ b/semantic_release/cli/config.py @@ -234,7 +234,7 @@ def set_default_opts(self) -> Self: if self.commit_parser in _known_commit_parsers: parser_opts_type = _known_commit_parsers[ self.commit_parser - ].parser_options + ].get_default_options().__class__ else: # if its a custom parser, try to import it and pull the default options object type custom_class = dynamic_import(self.commit_parser) @@ -389,8 +389,10 @@ def from_raw_config( else dynamic_import(raw.commit_parser) ) + commit_parser_opts_class = commit_parser_cls.get_default_options().__class__ + commit_parser = commit_parser_cls( - options=commit_parser_cls.parser_options(**raw.commit_parser_options) + options=commit_parser_opts_class(**raw.commit_parser_options) ) # We always exclude PSR's own release commits from the Changelog From 03a0ab8a23964baad6bee795fc90a7e02e2806c5 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:20:04 -0400 Subject: [PATCH 07/21] refactor(parser-scipy): improve maintainability of parser options --- semantic_release/commit_parser/scipy.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/semantic_release/commit_parser/scipy.py b/semantic_release/commit_parser/scipy.py index 2fb76b2d7..94945689e 100644 --- a/semantic_release/commit_parser/scipy.py +++ b/semantic_release/commit_parser/scipy.py @@ -82,26 +82,20 @@ def _logged_parse_error(commit: Commit, error: str) -> ParseError: @dataclass class ScipyParserOptions(ParserOptions): + major_tags: Tuple[str, ...] = ("API",) + minor_tags: Tuple[str, ...] = ("DEP", "DEV", "ENH", "REV", "FEAT") + patch_tags: Tuple[str, ...] = ("BLD", "BUG", "MAINT") allowed_tags: Tuple[str, ...] = ( - "API", - "DEP", - "ENH", - "REV", - "BUG", - "MAINT", + *major_tags, + *minor_tags, + *patch_tags, "BENCH", - "BLD", - "DEV", "DOC", "STY", "TST", "REL", - "FEAT", "TEST", ) - major_tags: Tuple[str, ...] = ("API",) - minor_tags: Tuple[str, ...] = ("DEP", "DEV", "ENH", "REV", "FEAT") - patch_tags: Tuple[str, ...] = ("BLD", "BUG", "MAINT") default_level_bump: LevelBump = LevelBump.NO_RELEASE def __post_init__(self) -> None: From 83c514620ee1ee043f635770066b83508833bb9c Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:22:10 -0400 Subject: [PATCH 08/21] test(fixtures): redefine parser fixtures with new default option function --- tests/fixtures/commit_parsers.py | 43 +++++++++++++++++++------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/tests/fixtures/commit_parsers.py b/tests/fixtures/commit_parsers.py index ea62240e9..a9b010e90 100644 --- a/tests/fixtures/commit_parsers.py +++ b/tests/fixtures/commit_parsers.py @@ -2,38 +2,45 @@ from semantic_release.commit_parser import ( AngularCommitParser, + AngularParserOptions, EmojiCommitParser, + EmojiParserOptions, TagCommitParser, + TagParserOptions, ) # Note scipy defined in ./scipy.py as already used there -@pytest.fixture -def default_angular_parser_options(): - return AngularCommitParser.parser_options() +@pytest.fixture(scope="session") +def default_angular_parser() -> AngularCommitParser: + return AngularCommitParser() -@pytest.fixture -def default_angular_parser(default_angular_parser_options): - return AngularCommitParser(options=default_angular_parser_options) +@pytest.fixture(scope="session") +def default_angular_parser_options( + default_angular_parser: AngularCommitParser, +) -> AngularParserOptions: + return default_angular_parser.get_default_options() -@pytest.fixture -def default_emoji_parser_options(): - return EmojiCommitParser.parser_options() +@pytest.fixture(scope="session") +def default_emoji_parser() -> EmojiCommitParser: + return EmojiCommitParser() -@pytest.fixture -def default_emoji_parser(default_emoji_parser_options): - return EmojiCommitParser(options=default_emoji_parser_options) +@pytest.fixture(scope="session") +def default_emoji_parser_options( + default_emoji_parser: EmojiCommitParser, +) -> EmojiParserOptions: + return default_emoji_parser.get_default_options() -@pytest.fixture -def default_tag_parser_options(): - return TagCommitParser.parser_options() +@pytest.fixture(scope="session") +def default_tag_parser() -> TagCommitParser: + return TagCommitParser() -@pytest.fixture -def default_tag_parser(default_tag_parser_options): - return TagCommitParser(options=default_tag_parser_options) +@pytest.fixture(scope="session") +def default_tag_parser_options(default_tag_parser: TagCommitParser) -> TagParserOptions: + return default_tag_parser.get_default_options() From 0ac2e42c6f95a5931ef95010c47aebbabc54dd43 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:23:23 -0400 Subject: [PATCH 09/21] test(fixtures): refactor make_commit_obj into fixture --- tests/conftest.py | 11 +++++++++++ tests/unit/semantic_release/commit_parser/helper.py | 5 ----- 2 files changed, 11 insertions(+), 5 deletions(-) delete mode 100644 tests/unit/semantic_release/commit_parser/helper.py diff --git a/tests/conftest.py b/tests/conftest.py index 6133ef788..6a9797dfa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING import pytest +from git import Commit from tests.fixtures import * from tests.util import remove_dir_tree @@ -15,6 +16,9 @@ from tempfile import _TemporaryFileWrapper from typing import Generator, Protocol + class MakeCommitObjFn(Protocol): + def __call__(self, message: str) -> Commit: ... + class NetrcFileFn(Protocol): def __call__(self, machine: str) -> _TemporaryFileWrapper[str]: ... @@ -84,3 +88,10 @@ def _teardown_cached_dir(directory: Path | str) -> Path: for directory in directories: if directory.exists(): remove_dir_tree(directory, force=True) + + +@pytest.fixture(scope="session") +def make_commit_obj() -> MakeCommitObjFn: + def _make_commit(message: str) -> Commit: + return Commit(repo=Repo(), binsha=Commit.NULL_BIN_SHA, message=message) + return _make_commit diff --git a/tests/unit/semantic_release/commit_parser/helper.py b/tests/unit/semantic_release/commit_parser/helper.py deleted file mode 100644 index a231f61f1..000000000 --- a/tests/unit/semantic_release/commit_parser/helper.py +++ /dev/null @@ -1,5 +0,0 @@ -from git import Commit - - -def make_commit(message: str) -> Commit: - return Commit(repo=None, binsha=Commit.NULL_BIN_SHA, message=message) From b7a7de76d830fafeda9bcdff1b8154523dae5b0b Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:24:28 -0400 Subject: [PATCH 10/21] test(parser-angular): adapt tests to updated fixtures & option initialization --- .../commit_parser/test_angular.py | 89 +++++++++++-------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/tests/unit/semantic_release/commit_parser/test_angular.py b/tests/unit/semantic_release/commit_parser/test_angular.py index 5643ca7e0..09d7bfc03 100644 --- a/tests/unit/semantic_release/commit_parser/test_angular.py +++ b/tests/unit/semantic_release/commit_parser/test_angular.py @@ -1,27 +1,28 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + import pytest -from semantic_release.commit_parser.angular import AngularCommitParser +from semantic_release.commit_parser.angular import ( + AngularCommitParser, + AngularParserOptions, +) from semantic_release.commit_parser.token import ParsedCommit, ParseError from semantic_release.enums import LevelBump -from tests.unit.semantic_release.commit_parser.helper import make_commit - - -@pytest.fixture -def default_options(): - return AngularCommitParser.parser_options() +if TYPE_CHECKING: + from tests.conftest import MakeCommitObjFn -@pytest.fixture -def default_angular_parser(default_options): - return AngularCommitParser(default_options) - - -def test_parser_raises_unknown_message_style(default_angular_parser): - assert isinstance(default_angular_parser.parse(make_commit("")), ParseError) +def test_parser_raises_unknown_message_style( + default_angular_parser: AngularCommitParser, + make_commit_obj: MakeCommitObjFn +): + assert isinstance(default_angular_parser.parse(make_commit_obj("")), ParseError) assert isinstance( default_angular_parser.parse( - make_commit("feat(parser\n): Add new parser pattern") + make_commit_obj("feat(parser\n): Add new parser pattern") ), ParseError, ) @@ -50,9 +51,12 @@ def test_parser_raises_unknown_message_style(default_angular_parser): ], ) def test_parser_returns_correct_bump_level( - default_angular_parser, commit_message, bump + default_angular_parser: AngularCommitParser, + commit_message: str, + bump: LevelBump, + make_commit_obj: MakeCommitObjFn, ): - result = default_angular_parser.parse(make_commit(commit_message)) + result = default_angular_parser.parse(make_commit_obj(commit_message)) assert isinstance(result, ParsedCommit) assert result.bump is bump @@ -69,8 +73,13 @@ def test_parser_returns_correct_bump_level( ("chore(parser): ...", "chore"), ], ) -def test_parser_return_type_from_commit_message(default_angular_parser, message, type_): - result = default_angular_parser.parse(make_commit(message)) +def test_parser_return_type_from_commit_message( + default_angular_parser: AngularCommitParser, + message: str, + type_: str, + make_commit_obj: MakeCommitObjFn, +): + result = default_angular_parser.parse(make_commit_obj(message)) assert isinstance(result, ParsedCommit) assert result.type == type_ @@ -90,9 +99,12 @@ def test_parser_return_type_from_commit_message(default_angular_parser, message, ], ) def test_parser_return_scope_from_commit_message( - default_angular_parser, message, scope + default_angular_parser: AngularCommitParser, + message: str, + scope: str, + make_commit_obj: MakeCommitObjFn, ): - result = default_angular_parser.parse(make_commit(message)) + result = default_angular_parser.parse(make_commit_obj(message)) assert isinstance(result, ParsedCommit) assert result.scope == scope @@ -121,9 +133,12 @@ def test_parser_return_scope_from_commit_message( ], ) def test_parser_return_subject_from_commit_message( - default_angular_parser, message, descriptions + default_angular_parser: AngularCommitParser, + message: str, + descriptions: list[str], + make_commit_obj: MakeCommitObjFn, ): - result = default_angular_parser.parse(make_commit(message)) + result = default_angular_parser.parse(make_commit_obj(message)) assert isinstance(result, ParsedCommit) assert result.descriptions == descriptions @@ -131,16 +146,16 @@ def test_parser_return_subject_from_commit_message( ############################## # test custom parser options # ############################## -def test_parser_custom_default_level(): - options = AngularCommitParser.parser_options(default_bump_level=LevelBump.MINOR) +def test_parser_custom_default_level(make_commit_obj: MakeCommitObjFn): + options = AngularParserOptions(default_bump_level=LevelBump.MINOR) parser = AngularCommitParser(options) - result = parser.parse(make_commit("test(parser): Add a test for angular parser")) + result = parser.parse(make_commit_obj("test(parser): Add a test for angular parser")) assert isinstance(result, ParsedCommit) assert result.bump is LevelBump.MINOR -def test_parser_custom_allowed_types(): - options = AngularCommitParser.parser_options( +def test_parser_custom_allowed_types(make_commit_obj: MakeCommitObjFn): + options = AngularParserOptions( allowed_tags=( "custom", "build", @@ -156,28 +171,28 @@ def test_parser_custom_allowed_types(): ) parser = AngularCommitParser(options) - res1 = parser.parse(make_commit("custom: ...")) + res1 = parser.parse(make_commit_obj("custom: ...")) assert isinstance(res1, ParsedCommit) assert res1.bump is LevelBump.NO_RELEASE - res2 = parser.parse(make_commit("custom(parser): ...")) + res2 = parser.parse(make_commit_obj("custom(parser): ...")) assert isinstance(res2, ParsedCommit) assert res2.type == "custom" - assert isinstance(parser.parse(make_commit("feat(parser): ...")), ParseError) + assert isinstance(parser.parse(make_commit_obj("feat(parser): ...")), ParseError) -def test_parser_custom_minor_tags(): - options = AngularCommitParser.parser_options(minor_tags=("docs",)) +def test_parser_custom_minor_tags(make_commit_obj: MakeCommitObjFn): + options = AngularParserOptions(minor_tags=("docs",)) parser = AngularCommitParser(options) - res = parser.parse(make_commit("docs: write some docs")) + res = parser.parse(make_commit_obj("docs: write some docs")) assert isinstance(res, ParsedCommit) assert res.bump is LevelBump.MINOR -def test_parser_custom_patch_tags(): - options = AngularCommitParser.parser_options(patch_tags=("test",)) +def test_parser_custom_patch_tags(make_commit_obj: MakeCommitObjFn): + options = AngularParserOptions(patch_tags=("test",)) parser = AngularCommitParser(options) - res = parser.parse(make_commit("test(this): added a test")) + res = parser.parse(make_commit_obj("test(this): added a test")) assert isinstance(res, ParsedCommit) assert res.bump is LevelBump.PATCH From 0b29e0a6d991e1803296d8676230aa6cc11afa56 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:24:41 -0400 Subject: [PATCH 11/21] test(parser-emoji): adapt tests to updated fixtures & option initialization --- .../commit_parser/test_emoji.py | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/unit/semantic_release/commit_parser/test_emoji.py b/tests/unit/semantic_release/commit_parser/test_emoji.py index 1d9a24baf..210afe638 100644 --- a/tests/unit/semantic_release/commit_parser/test_emoji.py +++ b/tests/unit/semantic_release/commit_parser/test_emoji.py @@ -1,19 +1,16 @@ -import pytest - -from semantic_release.commit_parser.emoji import EmojiCommitParser -from semantic_release.enums import LevelBump +from __future__ import annotations -from tests.unit.semantic_release.commit_parser.helper import make_commit +from typing import TYPE_CHECKING +import pytest -@pytest.fixture -def default_options(): - return EmojiCommitParser.parser_options() +from semantic_release.commit_parser.token import ParsedCommit +from semantic_release.enums import LevelBump +if TYPE_CHECKING: + from semantic_release.commit_parser.emoji import EmojiCommitParser -@pytest.fixture -def default_emoji_parser(default_options): - return EmojiCommitParser(default_options) + from tests.conftest import MakeCommitObjFn @pytest.mark.parametrize( @@ -70,16 +67,19 @@ def default_emoji_parser(default_options): ], ) def test_default_emoji_parser( - default_emoji_parser, - commit_message, - bump, - type_, - descriptions, - breaking_descriptions, + default_emoji_parser: EmojiCommitParser, + commit_message: str, + bump: LevelBump, + type_: str, + descriptions: list[str], + breaking_descriptions: list[str], + make_commit_obj: MakeCommitObjFn, ): - commit = make_commit(commit_message) - parsed = default_emoji_parser.parse(commit) - assert parsed.bump is bump - assert parsed.type == type_ - assert parsed.descriptions == descriptions - assert parsed.breaking_descriptions == breaking_descriptions + commit = make_commit_obj(commit_message) + result = default_emoji_parser.parse(commit) + + assert isinstance(result, ParsedCommit) + assert bump is result.bump + assert type_ == result.type + assert descriptions == result.descriptions + assert breaking_descriptions == result.breaking_descriptions From 5485d45dcdd4e39d6336377065c5ede16f38c221 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:24:58 -0400 Subject: [PATCH 12/21] test(parser-tag): adapt tests to updated fixtures & option initialization --- .../commit_parser/test_tag.py | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/tests/unit/semantic_release/commit_parser/test_tag.py b/tests/unit/semantic_release/commit_parser/test_tag.py index 97781d4fc..4289b6b07 100644 --- a/tests/unit/semantic_release/commit_parser/test_tag.py +++ b/tests/unit/semantic_release/commit_parser/test_tag.py @@ -1,20 +1,16 @@ -import pytest - -from semantic_release.commit_parser.tag import TagCommitParser -from semantic_release.commit_parser.token import ParseError -from semantic_release.enums import LevelBump +from __future__ import annotations -from tests.unit.semantic_release.commit_parser.helper import make_commit +from typing import TYPE_CHECKING +import pytest -@pytest.fixture -def default_options(): - return TagCommitParser.parser_options() +from semantic_release.commit_parser.token import ParsedCommit, ParseError +from semantic_release.enums import LevelBump +if TYPE_CHECKING: + from semantic_release.commit_parser.tag import TagCommitParser -@pytest.fixture -def default_tag_parser(default_options): - return TagCommitParser(default_options) + from tests.conftest import MakeCommitObjFn text = ( @@ -24,8 +20,11 @@ def default_tag_parser(default_options): footer = "Closes #400" -def test_parser_raises_unknown_message_style(default_tag_parser): - result = default_tag_parser.parse(make_commit("")) +def test_parser_raises_unknown_message_style( + default_tag_parser: TagCommitParser, + make_commit_obj: MakeCommitObjFn +): + result = default_tag_parser.parse(make_commit_obj("")) assert isinstance(result, ParseError) @@ -65,10 +64,17 @@ def test_parser_raises_unknown_message_style(default_tag_parser): ], ) def test_default_tag_parser( - default_tag_parser, commit_message, bump, type_, descriptions + default_tag_parser: TagCommitParser, + commit_message: str, + bump: LevelBump, + type_: str, + descriptions: list[str], + make_commit_obj: MakeCommitObjFn, ): - commit = make_commit(commit_message) - parsed = default_tag_parser.parse(commit) - assert parsed.bump is bump - assert parsed.type == type_ - assert parsed.descriptions == descriptions + commit = make_commit_obj(commit_message) + result = default_tag_parser.parse(commit) + + assert isinstance(result, ParsedCommit) + assert result.bump is bump + assert result.type == type_ + assert result.descriptions == descriptions From 99859c0c4ad0126439a81d4ed0551750066c94ab Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:31:24 -0400 Subject: [PATCH 13/21] test(parser): adapt tests to new create commit fixture --- .../commit_parser/test_parsed_commit.py | 27 +++++++++++++++++++ .../commit_parser/test_token.py | 22 --------------- 2 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 tests/unit/semantic_release/commit_parser/test_parsed_commit.py delete mode 100644 tests/unit/semantic_release/commit_parser/test_token.py diff --git a/tests/unit/semantic_release/commit_parser/test_parsed_commit.py b/tests/unit/semantic_release/commit_parser/test_parsed_commit.py new file mode 100644 index 000000000..2cb4c11d6 --- /dev/null +++ b/tests/unit/semantic_release/commit_parser/test_parsed_commit.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from semantic_release import LevelBump +from semantic_release.commit_parser import ParsedCommit + +if TYPE_CHECKING: + from tests.conftest import MakeCommitObjFn + + +def test_parsed_commit_computed_properties(make_commit_obj: MakeCommitObjFn): + message = "feat(parser): Add new parser pattern" + commit = make_commit_obj(message) + + parsed_commit = ParsedCommit( + bump=LevelBump.MINOR, + type="feature", + scope="parser", + descriptions=["Add new parser pattern"], + breaking_descriptions=[], + commit=commit, + ) + + assert message == parsed_commit.message + assert commit.hexsha == parsed_commit.hexsha + assert commit.hexsha[:7] == parsed_commit.short_hash diff --git a/tests/unit/semantic_release/commit_parser/test_token.py b/tests/unit/semantic_release/commit_parser/test_token.py deleted file mode 100644 index 3e50d7a0a..000000000 --- a/tests/unit/semantic_release/commit_parser/test_token.py +++ /dev/null @@ -1,22 +0,0 @@ -from semantic_release import LevelBump -from semantic_release.commit_parser import ParsedCommit - -from tests.unit.semantic_release.commit_parser.helper import make_commit - - -def test_parsed_commit_computed_properties(): - message = "feat(parser): Add new parser pattern" - commit = make_commit(message) - - token = ParsedCommit( - bump=LevelBump.MINOR, - type="feature", - scope="parser", - descriptions=["Add new parser pattern"], - breaking_descriptions=[], - commit=commit, - ) - - assert token.message == message - assert token.hexsha == commit.hexsha - assert token.short_hash == commit.hexsha[:7] From a80518340fe0f86c31e1cc94ddb850ba09331974 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 18:59:10 -0400 Subject: [PATCH 14/21] test(parser-scipy): refactor for decreased scipy testing duration --- tests/const.py | 32 +- tests/fixtures/scipy.py | 492 +++++++++++++----- .../commit_parser/test_scipy.py | 131 ++++- 3 files changed, 500 insertions(+), 155 deletions(-) diff --git a/tests/const.py b/tests/const.py index 9801b13c7..505ac2f4a 100644 --- a/tests/const.py +++ b/tests/const.py @@ -68,37 +68,9 @@ EMOJI_COMMITS_MAJOR = EMOJI_COMMITS_MINOR.copy() EMOJI_COMMITS_MAJOR.insert(4, ":boom: Move to the blockchain") -SCIPY_FORMATTED_COMMIT_BODY_PARTS = [ - # a squash merge that preserved PR commit messages - ( - "DOC: import ropy.transform to test for numpy error", - "DOC: lower numpy version", - "DOC: lower numpy version further", - "MAINT: remove debugging import", - ), - # empty body - (), - # formatted body - ( - """Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 3.5.3 to 4.1.1. - - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - - [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) - - [Commits](https://github.com/sphinx-doc/sphinx/commits/v4.1.1)""", - """--- - updated-dependencies: - - dependency-name: sphinx - dependency-type: direct:development - update-type: version-update:semver-major""", - ), - ( - "Bug spotted on Fedora, see https://src.fedoraproject.org/rpms/scipy/pull-request/22", - "The `int[::]` annotation is used to accept non-contiguous views.", - ), - ("[skip azp] [skip actions]",), -] -# Note - the scipy commit testing in v7 is very comprehensive - -# fixtures for commits that should evaluate to the various scopes + +# Note - the scipy commit fixtures for commits that should evaluate to the various scopes # are in tests/fixtures/scipy diff --git a/tests/fixtures/scipy.py b/tests/fixtures/scipy.py index 0023ece5c..0e06290f8 100644 --- a/tests/fixtures/scipy.py +++ b/tests/fixtures/scipy.py @@ -1,163 +1,423 @@ -import random +from __future__ import annotations + from itertools import chain, zip_longest +from textwrap import dedent +from typing import TYPE_CHECKING import pytest -from semantic_release.commit_parser.scipy import ( - ScipyCommitParser, - ScipyParserOptions, - tag_to_section, -) +from semantic_release.commit_parser.scipy import ScipyCommitParser from semantic_release.enums import LevelBump -from tests.const import SCIPY_FORMATTED_COMMIT_BODY_PARTS -from tests.util import xdist_sort_hack +if TYPE_CHECKING: + from typing import Protocol + from semantic_release.commit_parser.scipy import ScipyParserOptions -@pytest.fixture -def default_scipy_parser_options(): - return ScipyCommitParser.parser_options() + class FormatScipyCommitFn(Protocol): + def __call__(self, scipy_tag: str, subject: str, body_parts: list[str]) -> str: ... -@pytest.fixture -def default_scipy_parser(default_scipy_parser_options): - return ScipyCommitParser(default_scipy_parser_options) +@pytest.fixture(scope="session") +def format_scipy_commit(): + def _format_scipy_commit(scipy_tag: str, subject: str, body_parts: list[str]) -> str: + body = str.join("\n\n", body_parts) + return f"{scipy_tag}: {subject}\n\n{body}" + return _format_scipy_commit -@pytest.fixture(params=tag_to_section.keys()) -def scipy_tag(request): - return request.param +@pytest.fixture(scope="session") +def default_scipy_parser() -> ScipyCommitParser: + return ScipyCommitParser() -@pytest.fixture( - params=xdist_sort_hack( - [ - "scipy.stats.qmc: centered-discrepancy optimization of a Latin hypercube", - "inverse missing in idstn, idctn (#14479)", - "Merge pull request #14447 from AnirudhDagar/rename_ndimage_modules", - "Add tests for args kwarg in quad_vec", - "badge with version of the doc in the navbar (#14132)", - "Bump scipy from 1.7.0 to 1.7.1 (#28)", - "avoid nan if angle=0 in RotvecRotation", - ] - ) -) -def subject(request): - return request.param +@pytest.fixture(scope="session") +def default_scipy_parser_options(default_scipy_parser: ScipyCommitParser) -> ScipyParserOptions: + return default_scipy_parser.get_default_options() -@pytest.fixture(params=xdist_sort_hack(SCIPY_FORMATTED_COMMIT_BODY_PARTS)) -def body_parts(request): - return request.param +@pytest.fixture(scope="session") +def scipy_chore_commit_types(default_scipy_parser_options: ScipyParserOptions) -> list[str]: + return [ + k + for k, v in default_scipy_parser_options.tag_to_level.items() + if v < LevelBump.PATCH + ] -@pytest.fixture -def expected_response_scipy( - default_scipy_parser_options, scipy_tag, subject, body_parts -): - bump_level = default_scipy_parser_options.tag_to_level[scipy_tag] - type_ = tag_to_section[scipy_tag] - changelog_body = (subject, *body_parts) - return (bump_level, type_, None, changelog_body) +@pytest.fixture(scope="session") +def scipy_patch_commit_types(default_scipy_parser_options: ScipyParserOptions) -> list[str]: + return [ + k + for k, v in default_scipy_parser_options.tag_to_level.items() + if v is LevelBump.PATCH + ] -def _make_scipy_commit(scipy_tag, subject, body_parts): - body = "\n\n".join(body_parts) - return f"{scipy_tag}: {subject}\n\n{body}" +@pytest.fixture(scope="session") +def scipy_minor_commit_types(default_scipy_parser_options: ScipyParserOptions) -> list[str]: + return [ + k + for k, v in default_scipy_parser_options.tag_to_level.items() + if v is LevelBump.MINOR + ] -@pytest.fixture -def valid_scipy_commit(scipy_tag, subject, body_parts): - return _make_scipy_commit(scipy_tag, subject, body_parts) +@pytest.fixture(scope="session") +def scipy_major_commit_types(default_scipy_parser_options: ScipyParserOptions) -> list[str]: + return [ + k + for k, v in default_scipy_parser_options.tag_to_level.items() + if v is LevelBump.MAJOR + ] -@pytest.fixture -def scipy_chore_commits(): - return ["DOC: Add a note to the documentation"] +@pytest.fixture(scope="session") +def scipy_nonparseable_commits() -> list[str]: + return [ + "Initial Commit", + "Merge pull request #14447 from AnirudhDagar/rename_ndimage_modules", + ] + + +@pytest.fixture(scope="session") +def scipy_chore_subjects(scipy_chore_commit_types: list[str]) -> list[str]: + subjects = { + "BENCH": "disable very slow benchmark in optimize_milp.py", + "DOC": "change approx_fprime doctest (#20568)", + "STY": "fixed ruff & mypy issues", + "TST": "Skip Cython tests for editable installs", + "REL": "set version to 1.0.0", + "TEST": "Add Cython tests for editable installs", + } + # Test fixture modification failure prevention + assert len(subjects.keys()) == len(scipy_chore_commit_types) + return [subjects[k] for k in scipy_chore_commit_types] + + +@pytest.fixture(scope="session") +def scipy_patch_subjects(scipy_patch_commit_types: list[str]) -> list[str]: + subjects = { + "BLD": "move the optimize build steps earlier into the build sequence", + "BUG": "Fix invalid default bracket selection in _bracket_minimum (#20563)", + "MAINT": "optimize.linprog: fix bug when integrality is a list of all zeros (#20586)", + } + # Test fixture modification failure prevention + assert len(subjects.keys()) == len(scipy_patch_commit_types) + return [subjects[k] for k in scipy_patch_commit_types] + + +@pytest.fixture(scope="session") +def scipy_minor_subjects(scipy_minor_commit_types: list[str]) -> list[str]: + subjects = { + "DEP": "stats: switch kendalltau to kwarg-only, remove initial_lexsort", + "DEV": "add unicode check to pre-commit-hook", + "ENH": "stats.ttest_1samp: add array-API support (#20545)", + "REV": "reverted a previous commit", + "FEAT": "added a new feature", + } + # Test fixture modification failure prevention + assert len(subjects.keys()) == len(scipy_minor_commit_types) + return [subjects[k] for k in scipy_minor_commit_types] + + +@pytest.fixture(scope="session") +def scipy_major_subjects(scipy_major_commit_types: list[str]) -> list[str]: + subjects = { + "API": "dropped support for python 3.7", + } + # Test fixture modification failure prevention + assert len(subjects.keys()) == len(scipy_major_commit_types) + return [subjects[k] for k in scipy_major_commit_types] + + +@pytest.fixture(scope="session") +def scipy_brk_change_commit_bodies() -> list[list[str]]: + brk_chg_msg = dedent( + """ + BREAKING CHANGE: a description of what is now different + with multiple lines + """ + ).strip() + + one_line_desc = "resolves bug related to windows incompatiblity" -@pytest.fixture( - params=xdist_sort_hack( - [ - k - for k, v in ScipyParserOptions().tag_to_level.items() - if v is LevelBump.PATCH - ] - ) -) -def scipy_patch_commits(request, subject): return [ - _make_scipy_commit(request.param, subject, body_parts) - for body_parts in SCIPY_FORMATTED_COMMIT_BODY_PARTS + # No regular change description + [brk_chg_msg], + # regular change description & breaking change message + [one_line_desc, brk_chg_msg], + # regular change description & breaking change message with footer + [one_line_desc, brk_chg_msg, "Resolves: #666"], ] -@pytest.fixture( - params=xdist_sort_hack( +@pytest.fixture(scope="session") +def scipy_nonbrking_commit_bodies() -> list[list[str]]: + # a GitHub squash merge that preserved PR commit messages (all chore-like) + github_squash_merge_body = str.join("\n\n", [ + "* DOC: import ropy.transform to test for numpy error", + "* DOC: lower numpy version", + "* DOC: lower numpy version further", + "* STY: resolve linting issues", + ]) + + one_block_desc = dedent( + """ + Bug spotted on Fedora, see https://src.fedoraproject.org/rpms/scipy/pull-request/22 + with an additional multiline description + """ + ).strip() + + return [ + github_squash_merge_body.split("\n\n"), # split into blocks + # empty body + [], + [""], + # formatted body (ie dependabot) + dedent( + """ + Bumps [package](https://github.com/namespace/project) from 3.5.3 to 4.1.1. + - [Release notes](https://github.com/namespace/project/releases) + - [Changelog](https://github.com/namespace/project/blob/4.x/CHANGES) + - [Commits](https://github.com/namespace/project/commits/v4.1.1) + + --- + updated-dependencies: + - dependency-name: package + dependency-type: direct:development + update-type: version-update:semver-major + """ + ).lstrip().split("\n\n"), + # 1 block description + one_block_desc.split("\n\n"), + # keywords + ["[skip azp] [skip actions]"], + # Resolving an issue on GitHub + ["Resolves: #127"], [ - k - for k, v in ScipyParserOptions().tag_to_level.items() - if v is LevelBump.MINOR - ] - ) -) -def scipy_minor_commits(request, subject, default_scipy_parser_options): - patch_tags = [ - k - for k, v in default_scipy_parser_options.tag_to_level.items() - if v is LevelBump.PATCH + one_block_desc, + "Closes: #1024" + ], ] - patch_commits = [ - _make_scipy_commit(random.choice(patch_tags), subject, body_parts) - for body_parts in SCIPY_FORMATTED_COMMIT_BODY_PARTS + + +@pytest.fixture(scope="session") +def scipy_chore_commit_parts( + scipy_chore_commit_types: list[str], + scipy_chore_subjects: list[str], + scipy_nonbrking_commit_bodies: list[list[str]], +) -> list[tuple[str, str, list[str]]]: + # Test fixture modification failure prevention + assert len(scipy_chore_commit_types) == len(scipy_chore_subjects) + + # build full commit messages with commit type prefix, subject, and body variant + # for all body variants + return [ + (commit_type, subject, commit_body_blocks) + for commit_type, subject in zip(scipy_chore_commit_types, scipy_chore_subjects) + for commit_body_blocks in scipy_nonbrking_commit_bodies ] - minor_commits = [ - _make_scipy_commit(request.param, subject, body_parts) - for body_parts in SCIPY_FORMATTED_COMMIT_BODY_PARTS + + +@pytest.fixture(scope="session") +def scipy_chore_commits( + scipy_chore_commit_parts: list[tuple[str, str, list[str]]], + format_scipy_commit: FormatScipyCommitFn, +) -> list[str]: + # build full commit messages with commit type prefix, subject, and body variant + # for all body variants + return [ + format_scipy_commit(commit_type, subject, commit_body) + for commit_type, subject, commit_body in scipy_chore_commit_parts ] - return list( - chain.from_iterable( - zip_longest(minor_commits, patch_commits, fillvalue="uninteresting") - ) - ) -@pytest.fixture( - params=xdist_sort_hack( - [ - k - for k, v in ScipyParserOptions().tag_to_level.items() - if v is LevelBump.MAJOR - ] - ) -) -def scipy_major_commits(request, subject, default_scipy_parser_options): - patch_tags = [ - k - for k, v in default_scipy_parser_options.tag_to_level.items() - if v is LevelBump.PATCH +@pytest.fixture(scope="session") +def scipy_patch_commit_parts( + scipy_patch_commit_types: list[str], + scipy_patch_subjects: list[str], + scipy_nonbrking_commit_bodies: list[list[str]], +) -> list[tuple[str, str, list[str]]]: + # Test fixture modification failure prevention + assert len(scipy_patch_commit_types) == len(scipy_patch_subjects) + + # build full commit messages with commit type prefix, subject, and body variant + # for all body variants + return [ + (commit_type, subject, commit_body_blocks) + for commit_type, subject in zip(scipy_patch_commit_types, scipy_patch_subjects) + for commit_body_blocks in scipy_nonbrking_commit_bodies ] - minor_tags = [ - k - for k, v in default_scipy_parser_options.tag_to_level.items() - if v is LevelBump.MINOR + + +@pytest.fixture(scope="session") +def scipy_patch_commits( + scipy_patch_commit_parts: list[tuple[str, str, list[str]]], + format_scipy_commit: FormatScipyCommitFn, +) -> list[str]: + # build full commit messages with commit type prefix, subject, and body variant + # for all body variants + return [ + format_scipy_commit(commit_type, subject, commit_body) + for commit_type, subject, commit_body in scipy_patch_commit_parts ] - patch_commits = [ - _make_scipy_commit(random.choice(patch_tags), subject, body_parts) - for body_parts in SCIPY_FORMATTED_COMMIT_BODY_PARTS + + +@pytest.fixture(scope="session") +def scipy_minor_commit_parts( + scipy_minor_commit_types: list[str], + scipy_minor_subjects: list[str], + scipy_nonbrking_commit_bodies: list[list[str]], +) -> list[tuple[str, str, list[str]]]: + # Test fixture modification failure prevention + assert len(scipy_minor_commit_types) == len(scipy_minor_subjects) + + # build full commit messages with commit type prefix, subject, and body variant + # for all body variants + return [ + (commit_type, subject, commit_body_blocks) + for commit_type, subject in zip(scipy_minor_commit_types, scipy_minor_subjects) + for commit_body_blocks in scipy_nonbrking_commit_bodies ] - minor_commits = [ - _make_scipy_commit(random.choice(minor_tags), subject, body_parts) - for body_parts in SCIPY_FORMATTED_COMMIT_BODY_PARTS + + +@pytest.fixture(scope="session") +def scipy_minor_commits( + scipy_minor_commit_parts: list[tuple[str, str, list[str]]], + format_scipy_commit: FormatScipyCommitFn, +) -> list[str]: + # build full commit messages with commit type prefix, subject, and body variant + # for all body variants + return [ + format_scipy_commit(commit_type, subject, commit_body) + for commit_type, subject, commit_body in scipy_minor_commit_parts ] - major_commits = [ - _make_scipy_commit(request.param, subject, body_parts) - for body_parts in SCIPY_FORMATTED_COMMIT_BODY_PARTS + + +@pytest.fixture(scope="session") +def scipy_major_commit_parts( + scipy_major_commit_types: list[str], + scipy_major_subjects: list[str], + scipy_brk_change_commit_bodies: list[list[str]], +) -> list[tuple[str, str, list[str]]]: + # Test fixture modification failure prevention + assert len(scipy_major_commit_types) == len(scipy_major_subjects) + + # build full commit messages with commit type prefix, subject, and body variant + # for all body variants + return [ + (commit_type, subject, commit_body_blocks) + for commit_type, subject in zip(scipy_major_commit_types, scipy_major_subjects) + for commit_body_blocks in scipy_brk_change_commit_bodies + ] + + +@pytest.fixture(scope="session") +def scipy_major_commits( + scipy_major_commit_parts: list[tuple[str, str, list[str]]], + format_scipy_commit: FormatScipyCommitFn, +) -> list[str]: + # build full commit messages with commit type prefix, subject, and body variant + # for all body variants + return [ + format_scipy_commit(commit_type, subject, commit_body) + for commit_type, subject, commit_body in scipy_major_commit_parts ] + + +@pytest.fixture(scope="session") +def scipy_patch_mixed_commits( + scipy_patch_commits: list[str], + scipy_chore_commits: list[str], +) -> list[str]: + return list( + filter( + None, + chain.from_iterable( + zip_longest(scipy_patch_commits, scipy_chore_commits) + ) + ) + ) + + +@pytest.fixture(scope="session") +def scipy_minor_mixed_commits( + scipy_minor_commits: list[str], + scipy_patch_commits: list[str], + scipy_chore_commits: list[str], +) -> list[str]: return list( chain.from_iterable( zip_longest( - major_commits, minor_commits, patch_commits, fillvalue="uninteresting" + scipy_minor_commits, + scipy_patch_commits, + scipy_chore_commits, + fillvalue="uninteresting" + ) + ) + ) + + +@pytest.fixture(scope="session") +def scipy_major_mixed_commits( + scipy_major_commits: list[str], + scipy_minor_commits: list[str], + scipy_patch_commits: list[str], + scipy_chore_commits: list[str], +) -> list[str]: + return list( + filter( + None, + chain.from_iterable( + zip_longest( + scipy_major_commits, + scipy_minor_commits, + scipy_patch_commits, + scipy_chore_commits, + ) ) ) ) + + +# @pytest.fixture( +# params=xdist_sort_hack( +# [ +# k +# for k, v in ScipyParserOptions().tag_to_level.items() +# if v is LevelBump.MAJOR +# ] +# ) +# ) +# def scipy_major_commits(request, subject, default_scipy_parser_options): +# patch_tags = [ +# k +# for k, v in default_scipy_parser_options.tag_to_level.items() +# if v is LevelBump.PATCH +# ] +# minor_tags = [ +# k +# for k, v in default_scipy_parser_options.tag_to_level.items() +# if v is LevelBump.MINOR +# ] +# patch_commits = [ +# _make_scipy_commit(random.choice(patch_tags), subject, body_parts) +# for body_parts in SCIPY_FORMATTED_COMMIT_BODY_PARTS +# ] +# minor_commits = [ +# _make_scipy_commit(random.choice(minor_tags), subject, body_parts) +# for body_parts in SCIPY_FORMATTED_COMMIT_BODY_PARTS +# ] +# major_commits = [ +# _make_scipy_commit(request.param, subject, body_parts) +# for body_parts in SCIPY_FORMATTED_COMMIT_BODY_PARTS +# ] +# return list( +# chain.from_iterable( +# zip_longest( +# major_commits, minor_commits, patch_commits, fillvalue="uninteresting" +# ) +# ) +# ) diff --git a/tests/unit/semantic_release/commit_parser/test_scipy.py b/tests/unit/semantic_release/commit_parser/test_scipy.py index da24f2811..0e5e45092 100644 --- a/tests/unit/semantic_release/commit_parser/test_scipy.py +++ b/tests/unit/semantic_release/commit_parser/test_scipy.py @@ -1,13 +1,126 @@ -from tests.unit.semantic_release.commit_parser.helper import make_commit +from __future__ import annotations +from typing import TYPE_CHECKING -def test_valid_scipy_commit( - default_scipy_parser, valid_scipy_commit, expected_response_scipy +from semantic_release.commit_parser.scipy import tag_to_section +from semantic_release.commit_parser.token import ParsedCommit +from semantic_release.enums import LevelBump + +if TYPE_CHECKING: + from semantic_release.commit_parser.scipy import ScipyCommitParser + + from tests.conftest import MakeCommitObjFn + + +def test_valid_scipy_parsed_chore_commits( + default_scipy_parser: ScipyCommitParser, + make_commit_obj: MakeCommitObjFn, + scipy_chore_commit_parts: list[list[str]], + scipy_chore_commits: list[str], +): + expected_parts = scipy_chore_commit_parts + + for i, full_commit_msg in enumerate(scipy_chore_commits): + (commit_type, subject, commit_bodies) = expected_parts[i] + exepcted_type = tag_to_section[commit_type] + expected_descriptions = [ + subject, + *[body for body in commit_bodies if body], + ] + expected_brk_desc = [] + + commit = make_commit_obj(full_commit_msg) + result = default_scipy_parser.parse(commit) + + assert isinstance(result, ParsedCommit) + assert LevelBump.NO_RELEASE is result.bump + assert exepcted_type == result.type + assert expected_descriptions == result.descriptions + assert expected_brk_desc == result.breaking_descriptions + assert result.scope is None + + +def test_valid_scipy_parsed_patch_commits( + default_scipy_parser: ScipyCommitParser, + make_commit_obj: MakeCommitObjFn, + scipy_patch_commit_parts: list[list[str]], + scipy_patch_commits: list[str], +): + expected_parts = scipy_patch_commit_parts + + for i, full_commit_msg in enumerate(scipy_patch_commits): + (commit_type, subject, commit_bodies) = expected_parts[i] + exepcted_type = tag_to_section[commit_type] + expected_descriptions = [ + subject, + *[body for body in commit_bodies if body], + ] + expected_brk_desc = [] + + commit = make_commit_obj(full_commit_msg) + result = default_scipy_parser.parse(commit) + + assert isinstance(result, ParsedCommit) + assert LevelBump.PATCH is result.bump + assert exepcted_type == result.type + assert expected_descriptions == result.descriptions + assert expected_brk_desc == result.breaking_descriptions + assert result.scope is None + + +def test_valid_scipy_parsed_minor_commits( + default_scipy_parser: ScipyCommitParser, + make_commit_obj: MakeCommitObjFn, + scipy_minor_commit_parts: list[list[str]], + scipy_minor_commits: list[str], +): + expected_parts = scipy_minor_commit_parts + + for i, full_commit_msg in enumerate(scipy_minor_commits): + (commit_type, subject, commit_bodies) = expected_parts[i] + exepcted_type = tag_to_section[commit_type] + expected_descriptions = [ + subject, + *[body for body in commit_bodies if body], + ] + expected_brk_desc = [] + + commit = make_commit_obj(full_commit_msg) + result = default_scipy_parser.parse(commit) + + assert isinstance(result, ParsedCommit) + assert LevelBump.MINOR is result.bump + assert exepcted_type == result.type + assert expected_descriptions == result.descriptions + assert expected_brk_desc == result.breaking_descriptions + assert result.scope is None + + +def test_valid_scipy_parsed_major_commits( + default_scipy_parser: ScipyCommitParser, + make_commit_obj: MakeCommitObjFn, + scipy_major_commit_parts: list[list[str]], + scipy_major_commits: list[str], ): - (bump, type_, _, body_parts) = expected_response_scipy - result = default_scipy_parser.parse(make_commit(valid_scipy_commit)) + expected_parts = scipy_major_commit_parts + + for i, full_commit_msg in enumerate(scipy_major_commits): + (commit_type, subject, commit_bodies) = expected_parts[i] + exepcted_type = tag_to_section[commit_type] + expected_descriptions = [ + subject, + *[body for body in commit_bodies if body], + ] + expected_brk_desc = [ + block for block in commit_bodies if block.startswith("BREAKING CHANGE") + ] + + commit = make_commit_obj(full_commit_msg) + result = default_scipy_parser.parse(commit) - assert result.bump is bump - assert result.type == type_ - assert len(result.descriptions) == len(body_parts) - assert all(a == b for a, b in zip(result.descriptions, body_parts)) + assert isinstance(result, ParsedCommit) + assert LevelBump.MAJOR is result.bump + assert exepcted_type == result.type + assert expected_descriptions == result.descriptions + assert expected_brk_desc == result.breaking_descriptions + assert result.scope is None From 41ce1cbf122a09ef4f19e32ff1592c98b9d14853 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 27 Apr 2024 19:07:35 -0400 Subject: [PATCH 15/21] style: beautify formatting --- semantic_release/cli/config.py | 8 ++- semantic_release/commit_parser/_base.py | 4 +- tests/conftest.py | 1 + tests/const.py | 1 - tests/fixtures/scipy.py | 62 ++++++++++++------- .../commit_parser/test_angular.py | 7 ++- .../commit_parser/test_tag.py | 3 +- 7 files changed, 52 insertions(+), 34 deletions(-) diff --git a/semantic_release/cli/config.py b/semantic_release/cli/config.py index c6df2dbc0..a5c29d169 100644 --- a/semantic_release/cli/config.py +++ b/semantic_release/cli/config.py @@ -232,9 +232,11 @@ def set_default_opts(self) -> Self: parser_opts_type = None # If the commit parser is a known one, pull the default options object from it if self.commit_parser in _known_commit_parsers: - parser_opts_type = _known_commit_parsers[ - self.commit_parser - ].get_default_options().__class__ + parser_opts_type = ( + _known_commit_parsers[self.commit_parser] + .get_default_options() + .__class__ + ) else: # if its a custom parser, try to import it and pull the default options object type custom_class = dynamic_import(self.commit_parser) diff --git a/semantic_release/commit_parser/_base.py b/semantic_release/commit_parser/_base.py index 03460c0e3..5d450417b 100644 --- a/semantic_release/commit_parser/_base.py +++ b/semantic_release/commit_parser/_base.py @@ -66,7 +66,9 @@ def __init__(self, options: parser_options) -> None: """ def __init__(self, options: _OPTS | None = None) -> None: - self.options: _OPTS = options if options is not None else self.get_default_options() + self.options: _OPTS = ( + options if options is not None else self.get_default_options() + ) @staticmethod @abstractmethod diff --git a/tests/conftest.py b/tests/conftest.py index 6a9797dfa..a88da9dbd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -94,4 +94,5 @@ def _teardown_cached_dir(directory: Path | str) -> Path: def make_commit_obj() -> MakeCommitObjFn: def _make_commit(message: str) -> Commit: return Commit(repo=Repo(), binsha=Commit.NULL_BIN_SHA, message=message) + return _make_commit diff --git a/tests/const.py b/tests/const.py index 505ac2f4a..06007e757 100644 --- a/tests/const.py +++ b/tests/const.py @@ -69,7 +69,6 @@ EMOJI_COMMITS_MAJOR.insert(4, ":boom: Move to the blockchain") - # Note - the scipy commit fixtures for commits that should evaluate to the various scopes # are in tests/fixtures/scipy diff --git a/tests/fixtures/scipy.py b/tests/fixtures/scipy.py index 0e06290f8..e9a2c6b76 100644 --- a/tests/fixtures/scipy.py +++ b/tests/fixtures/scipy.py @@ -15,12 +15,16 @@ from semantic_release.commit_parser.scipy import ScipyParserOptions class FormatScipyCommitFn(Protocol): - def __call__(self, scipy_tag: str, subject: str, body_parts: list[str]) -> str: ... + def __call__( + self, scipy_tag: str, subject: str, body_parts: list[str] + ) -> str: ... @pytest.fixture(scope="session") def format_scipy_commit(): - def _format_scipy_commit(scipy_tag: str, subject: str, body_parts: list[str]) -> str: + def _format_scipy_commit( + scipy_tag: str, subject: str, body_parts: list[str] + ) -> str: body = str.join("\n\n", body_parts) return f"{scipy_tag}: {subject}\n\n{body}" @@ -33,12 +37,16 @@ def default_scipy_parser() -> ScipyCommitParser: @pytest.fixture(scope="session") -def default_scipy_parser_options(default_scipy_parser: ScipyCommitParser) -> ScipyParserOptions: +def default_scipy_parser_options( + default_scipy_parser: ScipyCommitParser, +) -> ScipyParserOptions: return default_scipy_parser.get_default_options() @pytest.fixture(scope="session") -def scipy_chore_commit_types(default_scipy_parser_options: ScipyParserOptions) -> list[str]: +def scipy_chore_commit_types( + default_scipy_parser_options: ScipyParserOptions, +) -> list[str]: return [ k for k, v in default_scipy_parser_options.tag_to_level.items() @@ -47,7 +55,9 @@ def scipy_chore_commit_types(default_scipy_parser_options: ScipyParserOptions) - @pytest.fixture(scope="session") -def scipy_patch_commit_types(default_scipy_parser_options: ScipyParserOptions) -> list[str]: +def scipy_patch_commit_types( + default_scipy_parser_options: ScipyParserOptions, +) -> list[str]: return [ k for k, v in default_scipy_parser_options.tag_to_level.items() @@ -56,7 +66,9 @@ def scipy_patch_commit_types(default_scipy_parser_options: ScipyParserOptions) - @pytest.fixture(scope="session") -def scipy_minor_commit_types(default_scipy_parser_options: ScipyParserOptions) -> list[str]: +def scipy_minor_commit_types( + default_scipy_parser_options: ScipyParserOptions, +) -> list[str]: return [ k for k, v in default_scipy_parser_options.tag_to_level.items() @@ -65,7 +77,9 @@ def scipy_minor_commit_types(default_scipy_parser_options: ScipyParserOptions) - @pytest.fixture(scope="session") -def scipy_major_commit_types(default_scipy_parser_options: ScipyParserOptions) -> list[str]: +def scipy_major_commit_types( + default_scipy_parser_options: ScipyParserOptions, +) -> list[str]: return [ k for k, v in default_scipy_parser_options.tag_to_level.items() @@ -156,12 +170,15 @@ def scipy_brk_change_commit_bodies() -> list[list[str]]: @pytest.fixture(scope="session") def scipy_nonbrking_commit_bodies() -> list[list[str]]: # a GitHub squash merge that preserved PR commit messages (all chore-like) - github_squash_merge_body = str.join("\n\n", [ - "* DOC: import ropy.transform to test for numpy error", - "* DOC: lower numpy version", - "* DOC: lower numpy version further", - "* STY: resolve linting issues", - ]) + github_squash_merge_body = str.join( + "\n\n", + [ + "* DOC: import ropy.transform to test for numpy error", + "* DOC: lower numpy version", + "* DOC: lower numpy version further", + "* STY: resolve linting issues", + ], + ) one_block_desc = dedent( """ @@ -171,7 +188,7 @@ def scipy_nonbrking_commit_bodies() -> list[list[str]]: ).strip() return [ - github_squash_merge_body.split("\n\n"), # split into blocks + github_squash_merge_body.split("\n\n"), # split into blocks # empty body [], [""], @@ -189,17 +206,16 @@ def scipy_nonbrking_commit_bodies() -> list[list[str]]: dependency-type: direct:development update-type: version-update:semver-major """ - ).lstrip().split("\n\n"), + ) + .lstrip() + .split("\n\n"), # 1 block description one_block_desc.split("\n\n"), # keywords ["[skip azp] [skip actions]"], # Resolving an issue on GitHub ["Resolves: #127"], - [ - one_block_desc, - "Closes: #1024" - ], + [one_block_desc, "Closes: #1024"], ] @@ -335,9 +351,7 @@ def scipy_patch_mixed_commits( return list( filter( None, - chain.from_iterable( - zip_longest(scipy_patch_commits, scipy_chore_commits) - ) + chain.from_iterable(zip_longest(scipy_patch_commits, scipy_chore_commits)), ) ) @@ -354,7 +368,7 @@ def scipy_minor_mixed_commits( scipy_minor_commits, scipy_patch_commits, scipy_chore_commits, - fillvalue="uninteresting" + fillvalue="uninteresting", ) ) ) @@ -377,7 +391,7 @@ def scipy_major_mixed_commits( scipy_patch_commits, scipy_chore_commits, ) - ) + ), ) ) diff --git a/tests/unit/semantic_release/commit_parser/test_angular.py b/tests/unit/semantic_release/commit_parser/test_angular.py index 09d7bfc03..01af1f210 100644 --- a/tests/unit/semantic_release/commit_parser/test_angular.py +++ b/tests/unit/semantic_release/commit_parser/test_angular.py @@ -16,8 +16,7 @@ def test_parser_raises_unknown_message_style( - default_angular_parser: AngularCommitParser, - make_commit_obj: MakeCommitObjFn + default_angular_parser: AngularCommitParser, make_commit_obj: MakeCommitObjFn ): assert isinstance(default_angular_parser.parse(make_commit_obj("")), ParseError) assert isinstance( @@ -149,7 +148,9 @@ def test_parser_return_subject_from_commit_message( def test_parser_custom_default_level(make_commit_obj: MakeCommitObjFn): options = AngularParserOptions(default_bump_level=LevelBump.MINOR) parser = AngularCommitParser(options) - result = parser.parse(make_commit_obj("test(parser): Add a test for angular parser")) + result = parser.parse( + make_commit_obj("test(parser): Add a test for angular parser") + ) assert isinstance(result, ParsedCommit) assert result.bump is LevelBump.MINOR diff --git a/tests/unit/semantic_release/commit_parser/test_tag.py b/tests/unit/semantic_release/commit_parser/test_tag.py index 4289b6b07..a280c68ce 100644 --- a/tests/unit/semantic_release/commit_parser/test_tag.py +++ b/tests/unit/semantic_release/commit_parser/test_tag.py @@ -21,8 +21,7 @@ def test_parser_raises_unknown_message_style( - default_tag_parser: TagCommitParser, - make_commit_obj: MakeCommitObjFn + default_tag_parser: TagCommitParser, make_commit_obj: MakeCommitObjFn ): result = default_tag_parser.parse(make_commit_obj("")) assert isinstance(result, ParseError) From f683bc5fa411a71cd8bd615fc2cfcee96d380954 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sun, 28 Apr 2024 00:56:30 -0400 Subject: [PATCH 16/21] refactor(parser-base): maintain current compatibility w/ custom parsers --- semantic_release/cli/config.py | 20 +++++++++++++------- semantic_release/commit_parser/_base.py | 11 ++++++++--- semantic_release/commit_parser/angular.py | 3 +++ semantic_release/commit_parser/emoji.py | 3 +++ semantic_release/commit_parser/scipy.py | 3 +++ semantic_release/commit_parser/tag.py | 3 +++ tests/util.py | 16 ++++++++++++++-- 7 files changed, 47 insertions(+), 12 deletions(-) diff --git a/semantic_release/cli/config.py b/semantic_release/cli/config.py index a5c29d169..b9c9105c7 100644 --- a/semantic_release/cli/config.py +++ b/semantic_release/cli/config.py @@ -232,11 +232,15 @@ def set_default_opts(self) -> Self: parser_opts_type = None # If the commit parser is a known one, pull the default options object from it if self.commit_parser in _known_commit_parsers: - parser_opts_type = ( - _known_commit_parsers[self.commit_parser] - .get_default_options() - .__class__ - ) + # TODO: BREAKING CHANGE v10 + # parser_opts_type = ( + # _known_commit_parsers[self.commit_parser] + # .get_default_options() + # .__class__ + # ) + parser_opts_type = _known_commit_parsers[ + self.commit_parser + ].parser_options else: # if its a custom parser, try to import it and pull the default options object type custom_class = dynamic_import(self.commit_parser) @@ -245,7 +249,7 @@ def set_default_opts(self) -> Self: # from either the custom opts class or the known parser opts class, create an instance if callable(parser_opts_type): - opts_obj = parser_opts_type() + opts_obj: Any = parser_opts_type() # if the opts object is a dataclass, wrap it in a RootModel so it can be transformed to a Mapping opts_obj = ( opts_obj if not is_dataclass(opts_obj) else RootModel(opts_obj) @@ -391,7 +395,9 @@ def from_raw_config( else dynamic_import(raw.commit_parser) ) - commit_parser_opts_class = commit_parser_cls.get_default_options().__class__ + commit_parser_opts_class = commit_parser_cls.parser_options + # TODO: Breaking change v10 + # commit_parser_opts_class = commit_parser_cls.get_default_options().__class__ commit_parser = commit_parser_cls( options=commit_parser_opts_class(**raw.commit_parser_options) diff --git a/semantic_release/commit_parser/_base.py b/semantic_release/commit_parser/_base.py index 5d450417b..96caa69ea 100644 --- a/semantic_release/commit_parser/_base.py +++ b/semantic_release/commit_parser/_base.py @@ -65,14 +65,19 @@ def __init__(self, options: parser_options) -> None: ... """ + # TODO: Deprecate in lieu of get_default_options() + parser_options: type[ParserOptions] = ParserOptions + def __init__(self, options: _OPTS | None = None) -> None: self.options: _OPTS = ( options if options is not None else self.get_default_options() ) - @staticmethod - @abstractmethod - def get_default_options() -> _OPTS: ... + # TODO: BREAKING CHANGE v10, add abstract method for all custom parsers + # @staticmethod + # @abstractmethod + def get_default_options(self) -> _OPTS: + return self.parser_options() # type: ignore @abstractmethod def parse(self, commit: Commit) -> _TT: ... diff --git a/semantic_release/commit_parser/angular.py b/semantic_release/commit_parser/angular.py index 13936836d..8436d11e3 100644 --- a/semantic_release/commit_parser/angular.py +++ b/semantic_release/commit_parser/angular.py @@ -62,6 +62,9 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]): commits. See https://www.conventionalcommits.org/en/v1.0.0-beta.4/ """ + # TODO: Deprecate in lieu of get_default_options() + parser_options = AngularParserOptions + def __init__(self, options: AngularParserOptions | None = None) -> None: super().__init__(options) self.re_parser = re.compile( diff --git a/semantic_release/commit_parser/emoji.py b/semantic_release/commit_parser/emoji.py index 5d88952a4..9a2543dfa 100644 --- a/semantic_release/commit_parser/emoji.py +++ b/semantic_release/commit_parser/emoji.py @@ -57,6 +57,9 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]): the commit subject in the changelog. """ + # TODO: Deprecate in lieu of get_default_options() + parser_options = EmojiParserOptions + @staticmethod def get_default_options() -> EmojiParserOptions: return EmojiParserOptions() diff --git a/semantic_release/commit_parser/scipy.py b/semantic_release/commit_parser/scipy.py index 94945689e..52753edcb 100644 --- a/semantic_release/commit_parser/scipy.py +++ b/semantic_release/commit_parser/scipy.py @@ -111,6 +111,9 @@ def __post_init__(self) -> None: class ScipyCommitParser(CommitParser[ParseResult, ScipyParserOptions]): """Parser for scipy-style commit messages""" + # TODO: Deprecate in lieu of get_default_options() + parser_options = ScipyParserOptions + def __init__(self, options: ScipyParserOptions | None = None) -> None: super().__init__(options) self.re_parser = re.compile( diff --git a/semantic_release/commit_parser/tag.py b/semantic_release/commit_parser/tag.py index 4272ee339..0fd53ff08 100644 --- a/semantic_release/commit_parser/tag.py +++ b/semantic_release/commit_parser/tag.py @@ -34,6 +34,9 @@ class TagCommitParser(CommitParser[ParseResult, TagParserOptions]): first line as changelog content. """ + # TODO: Deprecate in lieu of get_default_options() + parser_options = TagParserOptions + @staticmethod def get_default_options() -> TagParserOptions: return TagParserOptions() diff --git a/tests/util.py b/tests/util.py index bddfe28b3..5f99346f8 100644 --- a/tests/util.py +++ b/tests/util.py @@ -14,7 +14,8 @@ from semantic_release.changelog.release_history import ReleaseHistory from semantic_release.cli import config as cliConfigModule from semantic_release.commit_parser._base import CommitParser, ParserOptions -from semantic_release.commit_parser.token import ParseResult +from semantic_release.commit_parser.token import ParsedCommit, ParseResult +from semantic_release.enums import LevelBump if TYPE_CHECKING: import filecmp @@ -29,9 +30,10 @@ from unittest.mock import MagicMock - from git import Repo + from git import Commit, Repo from semantic_release.cli.config import RuntimeContext + from semantic_release.commit_parser.token import ParseError _R = TypeVar("_R") @@ -183,3 +185,13 @@ class CustomParserOpts(ParserOptions): class CustomParserWithOpts(CommitParser[ParseResult, CustomParserOpts]): parser_options = CustomParserOpts + + def parse(self, commit: Commit) -> ParsedCommit | ParseError: + return ParsedCommit( + bump=LevelBump.NO_RELEASE, + type="custom", + scope="", + descriptions=[], + breaking_descriptions=[], + commit=commit, + ) From 5f8b7a13f0fbfec5e58acafaa9cea5d587657966 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 29 Apr 2024 00:03:33 -0400 Subject: [PATCH 17/21] test(fixtures): add fixture for custom parser use --- tests/fixtures/example_project.py | 64 ++++++++++++++++++++++++++----- tests/fixtures/git_repo.py | 6 ++- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/tests/fixtures/example_project.py b/tests/fixtures/example_project.py index ff7a70d88..f5f581146 100644 --- a/tests/fixtures/example_project.py +++ b/tests/fixtures/example_project.py @@ -1,5 +1,6 @@ from __future__ import annotations +from importlib import import_module import os from pathlib import Path from textwrap import dedent @@ -8,6 +9,7 @@ import pytest import tomlkit +import semantic_release from semantic_release.commit_parser import ( AngularCommitParser, EmojiCommitParser, @@ -43,6 +45,9 @@ def __call__(self, flag: bool) -> None: ... class UpdatePyprojectTomlFn(Protocol): def __call__(self, setting: str, value: Any) -> None: ... + class UseCustomParserFn(Protocol): + def __call__(self, module_import_str: str) -> type[CommitParser]: ... + class UseHvcsFn(Protocol): def __call__(self, domain: str | None = None) -> type[HvcsBase]: ... @@ -270,7 +275,12 @@ def _update_pyproject_toml(setting: str, value: Any) -> None: return _update_pyproject_toml -@pytest.fixture +@pytest.fixture(scope="session") +def pyproject_toml_config_option_parser() -> str: + return f"tool.{semantic_release.__name__}.commit_parser" + + +@pytest.fixture(scope="session") def set_major_on_zero(update_pyproject_toml: UpdatePyprojectTomlFn) -> SetFlagFn: """Turn on/off the major_on_zero setting.""" @@ -280,7 +290,7 @@ def _set_major_on_zero(flag: bool) -> None: return _set_major_on_zero -@pytest.fixture +@pytest.fixture(scope="session") def set_allow_zero_version(update_pyproject_toml: UpdatePyprojectTomlFn) -> SetFlagFn: """Turn on/off the allow_zero_version setting.""" @@ -291,49 +301,83 @@ def _set_allow_zero_version(flag: bool) -> None: @pytest.fixture(scope="session") -def use_angular_parser(update_pyproject_toml: UpdatePyprojectTomlFn) -> UseParserFn: +def use_angular_parser( + update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_config_option_parser: str +) -> UseParserFn: """Modify the configuration file to use the Angular parser.""" def _use_angular_parser() -> type[CommitParser]: - update_pyproject_toml("tool.semantic_release.commit_parser", "angular") + update_pyproject_toml(pyproject_toml_config_option_parser, "angular") return AngularCommitParser return _use_angular_parser @pytest.fixture(scope="session") -def use_emoji_parser(update_pyproject_toml: UpdatePyprojectTomlFn) -> UseParserFn: +def use_emoji_parser( + update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_config_option_parser: str +) -> UseParserFn: """Modify the configuration file to use the Emoji parser.""" def _use_emoji_parser() -> type[CommitParser]: - update_pyproject_toml("tool.semantic_release.commit_parser", "emoji") + update_pyproject_toml(pyproject_toml_config_option_parser, "emoji") return EmojiCommitParser return _use_emoji_parser @pytest.fixture(scope="session") -def use_scipy_parser(update_pyproject_toml: UpdatePyprojectTomlFn) -> UseParserFn: +def use_scipy_parser( + update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_config_option_parser: str +) -> UseParserFn: """Modify the configuration file to use the Scipy parser.""" def _use_scipy_parser() -> type[CommitParser]: - update_pyproject_toml("tool.semantic_release.commit_parser", "scipy") + update_pyproject_toml(pyproject_toml_config_option_parser, "scipy") return ScipyCommitParser return _use_scipy_parser @pytest.fixture(scope="session") -def use_tag_parser(update_pyproject_toml: UpdatePyprojectTomlFn) -> UseParserFn: +def use_tag_parser( + update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_config_option_parser: str +) -> UseParserFn: """Modify the configuration file to use the Tag parser.""" def _use_tag_parser() -> type[CommitParser]: - update_pyproject_toml("tool.semantic_release.commit_parser", "tag") + update_pyproject_toml(pyproject_toml_config_option_parser, "tag") return TagCommitParser return _use_tag_parser +@pytest.fixture(scope="session") +def use_custom_parser( + update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_config_option_parser: str +) -> UseCustomParserFn: + """Modify the configuration file to use a user defined string parser.""" + + def _use_custom_parser(module_import_str: str) -> type[CommitParser]: + # validate this is importable before writing to parser + module_name, attr = module_import_str.split(":", maxsplit=1) + try: + module = import_module(module_name) + custom_class = getattr(module, attr) + except (ModuleNotFoundError, AttributeError) as err: + raise ValueError("Custom parser object not found!") from err + + update_pyproject_toml(pyproject_toml_config_option_parser, module_import_str) + return custom_class + + return _use_custom_parser + + @pytest.fixture(scope="session") def use_github_hvcs(update_pyproject_toml: UpdatePyprojectTomlFn) -> UseHvcsFn: """Modify the configuration file to use GitHub as the HVCS.""" diff --git a/tests/fixtures/git_repo.py b/tests/fixtures/git_repo.py index 6e7eda27d..9e3a0d424 100644 --- a/tests/fixtures/git_repo.py +++ b/tests/fixtures/git_repo.py @@ -29,11 +29,12 @@ from tests.fixtures.example_project import ( ExProjectDir, UpdatePyprojectTomlFn, + UseCustomParserFn, UseHvcsFn, UseParserFn, ) - CommitConvention = Literal["angular", "emoji", "scipy", "tag"] + CommitConvention = Literal["angular", "emoji", "scipy", "tag"] | str VersionStr = str CommitMsg = str ChangelogTypeHeading = str @@ -253,6 +254,7 @@ def build_configured_base_repo( # noqa: C901 use_emoji_parser: UseParserFn, use_scipy_parser: UseParserFn, use_tag_parser: UseParserFn, + use_custom_parser: UseCustomParserFn, example_git_https_url: str, update_pyproject_toml: UpdatePyprojectTomlFn, ) -> BuildRepoFn: @@ -289,7 +291,7 @@ def _build_configured_base_repo( # noqa: C901 elif commit_type == "tag": use_tag_parser() else: - raise ValueError(f"Unknown parser name: {commit_type}") + use_custom_parser(commit_type) # Set HVCS configuration if hvcs_client_name == "github": From f783b0e371138998eb631bda8ab55f0f76e60003 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 29 Apr 2024 00:05:11 -0400 Subject: [PATCH 18/21] test(parser-custom): add custom parser import & initialize validation --- .../unit/semantic_release/cli/test_config.py | 109 +++++++++++++++++- tests/util.py | 15 ++- 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/tests/unit/semantic_release/cli/test_config.py b/tests/unit/semantic_release/cli/test_config.py index 63e3efe1b..181485be4 100644 --- a/tests/unit/semantic_release/cli/test_config.py +++ b/tests/unit/semantic_release/cli/test_config.py @@ -7,12 +7,14 @@ import tomlkit from pydantic import RootModel, ValidationError +import semantic_release from semantic_release.cli.config import ( GlobalCommandLineOptions, HvcsClient, RawConfig, RuntimeContext, ) +from semantic_release.cli.util import load_raw_config_file from semantic_release.commit_parser.angular import AngularParserOptions from semantic_release.commit_parser.emoji import EmojiParserOptions from semantic_release.commit_parser.scipy import ScipyParserOptions @@ -20,14 +22,16 @@ from semantic_release.const import DEFAULT_COMMIT_AUTHOR from semantic_release.enums import LevelBump +from semantic_release.errors import ParserLoadError from tests.fixtures.repos import repo_with_no_tags_angular_commits -from tests.util import CustomParserOpts +from tests.util import CustomParserOpts, CustomParserWithNoOpts, CustomParserWithOpts, IncompleteCustomParser if TYPE_CHECKING: from pathlib import Path from typing import Any - from tests.fixtures.example_project import ExProjectDir + from tests.fixtures.example_project import ExProjectDir, UpdatePyprojectTomlFn + from tests.fixtures.git_repo import BuildRepoFn @pytest.mark.parametrize( @@ -98,8 +102,11 @@ def test_invalid_hvcs_type(remote_config: dict[str, Any]): ("emoji", RootModel(EmojiParserOptions()).model_dump()), ("scipy", RootModel(ScipyParserOptions()).model_dump()), ("tag", RootModel(TagParserOptions()).model_dump()), - ("tests.util:CustomParserWithNoOpts", {}), - ("tests.util:CustomParserWithOpts", RootModel(CustomParserOpts()).model_dump()), + (f"{CustomParserWithNoOpts.__module__}:{CustomParserWithNoOpts.__name__}", {}), + ( + f"{CustomParserWithOpts.__module__}:{CustomParserWithOpts.__name__}", + RootModel(CustomParserOpts()).model_dump(), + ), ], ) def test_load_default_parser_opts( @@ -107,7 +114,9 @@ def test_load_default_parser_opts( ): raw_config = RawConfig.model_validate( # Since TOML does not support NoneTypes, we need to not include the key - {"commit_parser": commit_parser} if commit_parser else {} + {"commit_parser": commit_parser} + if commit_parser + else {} ) assert expected_parser_opts == raw_config.commit_parser_options @@ -169,7 +178,7 @@ def test_commit_author_configurable( example_pyproject_toml: Path, mock_env: dict[str, str], expected_author: str, - change_to_ex_proj_dir: str, + change_to_ex_proj_dir: None, ): content = tomlkit.loads(example_pyproject_toml.read_text(encoding="utf-8")).unwrap() @@ -183,3 +192,91 @@ def test_commit_author_configurable( f"{runtime.commit_author.name} <{runtime.commit_author.email}>" ) assert expected_author == resulting_author + + +def test_load_valid_runtime_config( + build_configured_base_repo: BuildRepoFn, + example_project_dir: ExProjectDir, + example_pyproject_toml: Path, + update_pyproject_toml: UpdatePyprojectTomlFn, + change_to_ex_proj_dir: None, +): + build_configured_base_repo(example_project_dir) + + # Wipe out any existing configuration options + update_pyproject_toml(f"tool.{semantic_release.__name__}", {}) + + runtime_ctx = RuntimeContext.from_raw_config( + RawConfig.model_validate( + load_raw_config_file(example_pyproject_toml) + ), + global_cli_options=GlobalCommandLineOptions(), + ) + + # TODO: add more validation + assert runtime_ctx + + +@pytest.mark.parametrize( + "commit_parser", + [ + f"{CustomParserWithNoOpts.__module__}:{CustomParserWithNoOpts.__name__}", + f"{CustomParserWithOpts.__module__}:{CustomParserWithOpts.__name__}", + ], +) +def test_load_valid_runtime_config_w_custom_parser( + commit_parser: str, + build_configured_base_repo: BuildRepoFn, + example_project_dir: ExProjectDir, + example_pyproject_toml: Path, + change_to_ex_proj_dir: None, +): + build_configured_base_repo( + example_project_dir, + commit_type=commit_parser, + ) + + runtime_ctx = RuntimeContext.from_raw_config( + RawConfig.model_validate( + load_raw_config_file(example_pyproject_toml) + ), + global_cli_options=GlobalCommandLineOptions(), + ) + assert runtime_ctx + + +@pytest.mark.parametrize( + "commit_parser", + [ + # Non-existant module + "tests.missing_module:CustomParser", + # Non-existant class + f"{CustomParserWithOpts.__module__}:MissingCustomParser", + # Incomplete class implementation + f"{IncompleteCustomParser.__module__}:{IncompleteCustomParser.__name__}", + ], +) +def test_load_invalid_custom_parser( + commit_parser: str, + build_configured_base_repo: BuildRepoFn, + example_project_dir: ExProjectDir, + example_pyproject_toml: Path, + update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_config_option_parser: str, + change_to_ex_proj_dir: None, +): + build_configured_base_repo(example_project_dir) + + # Wipe out any existing configuration options + update_pyproject_toml(f"{pyproject_toml_config_option_parser}_options", {}) + + # Insert invalid custom parser string into configuration + update_pyproject_toml(pyproject_toml_config_option_parser, commit_parser) + + with pytest.raises(ParserLoadError): + RuntimeContext.from_raw_config( + RawConfig.model_validate( + load_raw_config_file(example_pyproject_toml) + ), + global_cli_options=GlobalCommandLineOptions(), + ) diff --git a/tests/util.py b/tests/util.py index 5f99346f8..764d0a695 100644 --- a/tests/util.py +++ b/tests/util.py @@ -175,7 +175,16 @@ def __getattr__(self, name: str) -> Any: class CustomParserWithNoOpts(CommitParser[ParseResult, ParserOptions]): - parser_options = ParserOptions + + def parse(self, commit: Commit) -> ParsedCommit | ParseError: + return ParsedCommit( + bump=LevelBump.NO_RELEASE, + type="", + scope="", + descriptions=[], + breaking_descriptions=[], + commit=commit, + ) @dataclass @@ -195,3 +204,7 @@ def parse(self, commit: Commit) -> ParsedCommit | ParseError: breaking_descriptions=[], commit=commit, ) + + +class IncompleteCustomParser(CommitParser): + pass From 67f60389e3f6e93443ea108c0e1b4d30126b8e06 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 29 Apr 2024 00:06:09 -0400 Subject: [PATCH 19/21] fix(parser-custom): gracefully handle custom parser import errors --- semantic_release/cli/config.py | 70 +++++++++++++++++++++++++++------- semantic_release/errors.py | 6 +++ 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/semantic_release/cli/config.py b/semantic_release/cli/config.py index b9c9105c7..5915cbc69 100644 --- a/semantic_release/cli/config.py +++ b/semantic_release/cli/config.py @@ -39,7 +39,7 @@ TagCommitParser, ) from semantic_release.const import COMMIT_MESSAGE, DEFAULT_COMMIT_AUTHOR, SEMVER_REGEX -from semantic_release.errors import InvalidConfiguration, NotAReleaseBranch +from semantic_release.errors import ParserLoadError, InvalidConfiguration, NotAReleaseBranch from semantic_release.helpers import dynamic_import from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase from semantic_release.version import VersionTranslator @@ -242,10 +242,30 @@ def set_default_opts(self) -> Self: self.commit_parser ].parser_options else: - # if its a custom parser, try to import it and pull the default options object type - custom_class = dynamic_import(self.commit_parser) - if hasattr(custom_class, "parser_options"): - parser_opts_type = custom_class.parser_options + try: + # if its a custom parser, try to import it and pull the default options object type + custom_class = dynamic_import(self.commit_parser) + # TODO: BREAKING CHANGE v10 + # parser_opts_type = custom_class.get_default_options().__class__ + if hasattr(custom_class, "parser_options"): + parser_opts_type = custom_class.parser_options + + except ModuleNotFoundError as err: + raise ParserLoadError( + str.join("\n", [ + str(err), + "Unable to import your custom parser! Check your configuration!" + ]) + ) from err + + except AttributeError as err: + raise ParserLoadError( + str.join("\n", [ + str(err), + "Unable to find your custom parser class inside the given module.", + "Check your configuration!" + ]) + ) from err # from either the custom opts class or the known parser opts class, create an instance if callable(parser_opts_type): @@ -389,19 +409,41 @@ def from_raw_config( branch_config = cls.select_branch_options(raw.branches, active_branch) # commit_parser - commit_parser_cls = ( - _known_commit_parsers[raw.commit_parser] - if raw.commit_parser in _known_commit_parsers - else dynamic_import(raw.commit_parser) - ) + try: + commit_parser_cls = ( + _known_commit_parsers[raw.commit_parser] + if raw.commit_parser in _known_commit_parsers + else dynamic_import(raw.commit_parser) + ) + except ModuleNotFoundError as err: + raise ParserLoadError( + str.join("\n", [ + str(err), + "Unable to import your custom parser! Check your configuration!" + ]) + ) from err + except AttributeError as err: + raise ParserLoadError( + str.join("\n", [ + str(err), + "Unable to find the parser class inside the given module" + ]) + ) from err commit_parser_opts_class = commit_parser_cls.parser_options # TODO: Breaking change v10 # commit_parser_opts_class = commit_parser_cls.get_default_options().__class__ - - commit_parser = commit_parser_cls( - options=commit_parser_opts_class(**raw.commit_parser_options) - ) + try: + commit_parser = commit_parser_cls( + options=commit_parser_opts_class(**raw.commit_parser_options) + ) + except TypeError as err: + raise ParserLoadError( + str.join("\n", [ + str(err), + f"Failed to initialize {raw.commit_parser}" + ]) + ) from err # We always exclude PSR's own release commits from the Changelog # when parsing commits diff --git a/semantic_release/errors.py b/semantic_release/errors.py index 19fcda6ed..9ee496dfa 100644 --- a/semantic_release/errors.py +++ b/semantic_release/errors.py @@ -59,3 +59,9 @@ class AssetUploadError(SemanticReleaseBaseError): Raised when there is a failure uploading an asset to a remote hvcs's release artifact storage. """ + +class ParserLoadError(SemanticReleaseBaseError): + """ + Raised when there is a failure to find, load, or instaniate a custom parser + definition. + """ From e535e001d722fcd3d357041a2004beb3e5d1a8af Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 29 Apr 2024 00:18:49 -0400 Subject: [PATCH 20/21] style: beautify formatting --- semantic_release/cli/config.py | 57 ++++++++++++------- semantic_release/commit_parser/_base.py | 2 +- semantic_release/errors.py | 1 + tests/fixtures/example_project.py | 12 ++-- .../unit/semantic_release/cli/test_config.py | 25 ++++---- tests/util.py | 1 - 6 files changed, 54 insertions(+), 44 deletions(-) diff --git a/semantic_release/cli/config.py b/semantic_release/cli/config.py index 5915cbc69..9bd2fd8bf 100644 --- a/semantic_release/cli/config.py +++ b/semantic_release/cli/config.py @@ -39,7 +39,11 @@ TagCommitParser, ) from semantic_release.const import COMMIT_MESSAGE, DEFAULT_COMMIT_AUTHOR, SEMVER_REGEX -from semantic_release.errors import ParserLoadError, InvalidConfiguration, NotAReleaseBranch +from semantic_release.errors import ( + InvalidConfiguration, + NotAReleaseBranch, + ParserLoadError, +) from semantic_release.helpers import dynamic_import from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase from semantic_release.version import VersionTranslator @@ -252,19 +256,25 @@ def set_default_opts(self) -> Self: except ModuleNotFoundError as err: raise ParserLoadError( - str.join("\n", [ - str(err), - "Unable to import your custom parser! Check your configuration!" - ]) + str.join( + "\n", + [ + str(err), + "Unable to import your custom parser! Check your configuration!", + ], + ) ) from err except AttributeError as err: raise ParserLoadError( - str.join("\n", [ - str(err), - "Unable to find your custom parser class inside the given module.", - "Check your configuration!" - ]) + str.join( + "\n", + [ + str(err), + "Unable to find your custom parser class inside the given module.", + "Check your configuration!", + ], + ) ) from err # from either the custom opts class or the known parser opts class, create an instance @@ -417,17 +427,23 @@ def from_raw_config( ) except ModuleNotFoundError as err: raise ParserLoadError( - str.join("\n", [ - str(err), - "Unable to import your custom parser! Check your configuration!" - ]) + str.join( + "\n", + [ + str(err), + "Unable to import your custom parser! Check your configuration!", + ], + ) ) from err except AttributeError as err: raise ParserLoadError( - str.join("\n", [ - str(err), - "Unable to find the parser class inside the given module" - ]) + str.join( + "\n", + [ + str(err), + "Unable to find the parser class inside the given module", + ], + ) ) from err commit_parser_opts_class = commit_parser_cls.parser_options @@ -439,10 +455,7 @@ def from_raw_config( ) except TypeError as err: raise ParserLoadError( - str.join("\n", [ - str(err), - f"Failed to initialize {raw.commit_parser}" - ]) + str.join("\n", [str(err), f"Failed to initialize {raw.commit_parser}"]) ) from err # We always exclude PSR's own release commits from the Changelog diff --git a/semantic_release/commit_parser/_base.py b/semantic_release/commit_parser/_base.py index 96caa69ea..4a6303fca 100644 --- a/semantic_release/commit_parser/_base.py +++ b/semantic_release/commit_parser/_base.py @@ -77,7 +77,7 @@ def __init__(self, options: _OPTS | None = None) -> None: # @staticmethod # @abstractmethod def get_default_options(self) -> _OPTS: - return self.parser_options() # type: ignore + return self.parser_options() # type: ignore @abstractmethod def parse(self, commit: Commit) -> _TT: ... diff --git a/semantic_release/errors.py b/semantic_release/errors.py index 9ee496dfa..10e90b618 100644 --- a/semantic_release/errors.py +++ b/semantic_release/errors.py @@ -60,6 +60,7 @@ class AssetUploadError(SemanticReleaseBaseError): storage. """ + class ParserLoadError(SemanticReleaseBaseError): """ Raised when there is a failure to find, load, or instaniate a custom parser diff --git a/tests/fixtures/example_project.py b/tests/fixtures/example_project.py index f5f581146..7e453cdad 100644 --- a/tests/fixtures/example_project.py +++ b/tests/fixtures/example_project.py @@ -1,7 +1,7 @@ from __future__ import annotations -from importlib import import_module import os +from importlib import import_module from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING, Generator @@ -303,7 +303,7 @@ def _set_allow_zero_version(flag: bool) -> None: @pytest.fixture(scope="session") def use_angular_parser( update_pyproject_toml: UpdatePyprojectTomlFn, - pyproject_toml_config_option_parser: str + pyproject_toml_config_option_parser: str, ) -> UseParserFn: """Modify the configuration file to use the Angular parser.""" @@ -317,7 +317,7 @@ def _use_angular_parser() -> type[CommitParser]: @pytest.fixture(scope="session") def use_emoji_parser( update_pyproject_toml: UpdatePyprojectTomlFn, - pyproject_toml_config_option_parser: str + pyproject_toml_config_option_parser: str, ) -> UseParserFn: """Modify the configuration file to use the Emoji parser.""" @@ -331,7 +331,7 @@ def _use_emoji_parser() -> type[CommitParser]: @pytest.fixture(scope="session") def use_scipy_parser( update_pyproject_toml: UpdatePyprojectTomlFn, - pyproject_toml_config_option_parser: str + pyproject_toml_config_option_parser: str, ) -> UseParserFn: """Modify the configuration file to use the Scipy parser.""" @@ -345,7 +345,7 @@ def _use_scipy_parser() -> type[CommitParser]: @pytest.fixture(scope="session") def use_tag_parser( update_pyproject_toml: UpdatePyprojectTomlFn, - pyproject_toml_config_option_parser: str + pyproject_toml_config_option_parser: str, ) -> UseParserFn: """Modify the configuration file to use the Tag parser.""" @@ -359,7 +359,7 @@ def _use_tag_parser() -> type[CommitParser]: @pytest.fixture(scope="session") def use_custom_parser( update_pyproject_toml: UpdatePyprojectTomlFn, - pyproject_toml_config_option_parser: str + pyproject_toml_config_option_parser: str, ) -> UseCustomParserFn: """Modify the configuration file to use a user defined string parser.""" diff --git a/tests/unit/semantic_release/cli/test_config.py b/tests/unit/semantic_release/cli/test_config.py index 181485be4..334497d3d 100644 --- a/tests/unit/semantic_release/cli/test_config.py +++ b/tests/unit/semantic_release/cli/test_config.py @@ -21,10 +21,15 @@ from semantic_release.commit_parser.tag import TagParserOptions from semantic_release.const import DEFAULT_COMMIT_AUTHOR from semantic_release.enums import LevelBump - from semantic_release.errors import ParserLoadError + from tests.fixtures.repos import repo_with_no_tags_angular_commits -from tests.util import CustomParserOpts, CustomParserWithNoOpts, CustomParserWithOpts, IncompleteCustomParser +from tests.util import ( + CustomParserOpts, + CustomParserWithNoOpts, + CustomParserWithOpts, + IncompleteCustomParser, +) if TYPE_CHECKING: from pathlib import Path @@ -114,9 +119,7 @@ def test_load_default_parser_opts( ): raw_config = RawConfig.model_validate( # Since TOML does not support NoneTypes, we need to not include the key - {"commit_parser": commit_parser} - if commit_parser - else {} + {"commit_parser": commit_parser} if commit_parser else {} ) assert expected_parser_opts == raw_config.commit_parser_options @@ -207,9 +210,7 @@ def test_load_valid_runtime_config( update_pyproject_toml(f"tool.{semantic_release.__name__}", {}) runtime_ctx = RuntimeContext.from_raw_config( - RawConfig.model_validate( - load_raw_config_file(example_pyproject_toml) - ), + RawConfig.model_validate(load_raw_config_file(example_pyproject_toml)), global_cli_options=GlobalCommandLineOptions(), ) @@ -237,9 +238,7 @@ def test_load_valid_runtime_config_w_custom_parser( ) runtime_ctx = RuntimeContext.from_raw_config( - RawConfig.model_validate( - load_raw_config_file(example_pyproject_toml) - ), + RawConfig.model_validate(load_raw_config_file(example_pyproject_toml)), global_cli_options=GlobalCommandLineOptions(), ) assert runtime_ctx @@ -275,8 +274,6 @@ def test_load_invalid_custom_parser( with pytest.raises(ParserLoadError): RuntimeContext.from_raw_config( - RawConfig.model_validate( - load_raw_config_file(example_pyproject_toml) - ), + RawConfig.model_validate(load_raw_config_file(example_pyproject_toml)), global_cli_options=GlobalCommandLineOptions(), ) diff --git a/tests/util.py b/tests/util.py index 764d0a695..18cb181da 100644 --- a/tests/util.py +++ b/tests/util.py @@ -175,7 +175,6 @@ def __getattr__(self, name: str) -> Any: class CustomParserWithNoOpts(CommitParser[ParseResult, ParserOptions]): - def parse(self, commit: Commit) -> ParsedCommit | ParseError: return ParsedCommit( bump=LevelBump.NO_RELEASE, From 02fa4d21a2f06f31a8088508c6eacb57ee08f1b8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 29 Apr 2024 04:29:44 +0000 Subject: [PATCH 21/21] 9.6.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 83 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- semantic_release/__init__.py | 2 +- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dafaccc2..a51b7c73a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,89 @@ +## v9.6.0 (2024-04-29) + +### Feature + +* feat: changelog filters are specialized per vcs type (#890) + +* test(github): sync pr url expectation with GitHub api documentation + +* fix(github): correct changelog filter for pull request urls + +* refactor(hvcs-base): change to an abstract class & simplify interface + +* refactor(remote-hvcs-base): extend the base abstract class with common remote base class + +* refactor(github): adapt to new abstract base class + +* refactor(gitea): adapt to new abstract base class + +* refactor(gitlab): adapt to new abstract base class + +* refactor(bitbucket): adapt to new abstract base class + +* refactor(cmds): prevent hvcs from executing if not remote hosted vcs + +* feat(changelog): changelog filters are hvcs focused + +* test(hvcs): add validation for issue_url generation + +* feat(changelog-github): add issue url filter to changelog context + +* feat(changelog-gitea): add issue url filter to changelog context + +* refactor(cmd-version): consolidate asset uploads with release creation + +* style: resolve ruff errors + +* feat(changelog-context): add flag to jinja env for which hvcs is available + +* test(changelog-context): demonstrate per hvcs filters upon render + +* docs(changelog-context): explain new hvcs specific context filters + +* refactor(config): adjust default token resolution w/ subclasses ([`76ed593`](https://github.com/python-semantic-release/python-semantic-release/commit/76ed593ea33c851005994f0d1a6a33cc890fb908)) + +### Fix + +* fix(parser-custom): gracefully handle custom parser import errors ([`67f6038`](https://github.com/python-semantic-release/python-semantic-release/commit/67f60389e3f6e93443ea108c0e1b4d30126b8e06)) + +* fix: correct version `--prerelease` use & enable `--as-prerelease` (#647) + +* test(version): add validation of `--as-prerelease` and `--prerelease opts` + +* fix(version-cmd): correct `--prerelease` use + + Prior to this change, `--prerelease` performed the role of converting whichever forced +version into a prerelease version declaration, which was an unintentional breaking +change to the CLI compared to v7. + + `--prerelease` now forces the next version to increment the prerelease revision, +which makes it consistent with `--patch`, `--minor` and `--major`. Temporarily disabled +the ability to force a prerelease. + + Resolves: #639 + +* feat(version-cmd): add `--as-prerelease` option to force the next version to be a prerelease + + Prior to this change, `--prerelease` performed the role that `--as-prerelease` now does, +which was an unintentional breaking change to the CLI compared to v7. + + `--prerelease` is used to force the next version to increment the prerelease revision, +which makes it consistent with `--patch`, `--minor` and `--major`, while `--as-prerelease` +forces for the next version to be converted to a prerelease version type before it is +applied to the project regardless of the bump level. + + Resolves: #639 + +* docs(commands): update version command options definition about prereleases + +--------- + +Co-authored-by: codejedi365 <codejedi365@gmail.com> ([`2acb5ac`](https://github.com/python-semantic-release/python-semantic-release/commit/2acb5ac35ae79d7ae25ca9a03fb5c6a4a68b3673)) + + ## v9.5.0 (2024-04-23) ### Build diff --git a/pyproject.toml b/pyproject.toml index 14b94c1c9..a7008d635 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "python-semantic-release" -version = "9.5.0" +version = "9.6.0" description = "Automatic Semantic Versioning for Python projects" requires-python = ">=3.8" license = { text = "MIT" } diff --git a/semantic_release/__init__.py b/semantic_release/__init__.py index 092dfc3f4..c65a2b64d 100644 --- a/semantic_release/__init__.py +++ b/semantic_release/__init__.py @@ -24,7 +24,7 @@ tags_and_versions, ) -__version__ = "9.5.0" +__version__ = "9.6.0" def setup_hook(argv: list[str]) -> None: