diff --git a/CHANGELOG.md b/CHANGELOG.md index 396025d..1f9239e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and it could be incorrect for a monorepo with different tags for different packages. Now, Dunamai will use 0.0.0 in this case as well, unless strict mode is enabled. +* You can now specify a pattern prefix. + For example, `--pattern default --pattern-prefix some-package-` + would match tags like `some-package-v1.2.3`. + This is useful if you just want a custom prefix without writing a whole pattern. ## v1.20.0 (2024-04-12) diff --git a/dunamai/__init__.py b/dunamai/__init__.py index baacd69..fdefa09 100644 --- a/dunamai/__init__.py +++ b/dunamai/__init__.py @@ -113,20 +113,26 @@ class Pattern(Enum): DefaultUnprefixed = "default-unprefixed" """Default pattern, but without `v` prefix.""" - def regex(self) -> str: + def regex(self, prefix: Optional[str] = None) -> str: """ Get the regular expression for this preset pattern. + :param prefix: Insert this after the pattern's start anchor (`^`). :returns: Regular expression. """ variants = { Pattern.Default: VERSION_SOURCE_PATTERN, Pattern.DefaultUnprefixed: VERSION_SOURCE_PATTERN.replace("^v", "^v?", 1), } - return variants[self] + + out = variants[self] + if prefix: + out = out.replace("^", "^{}".format(prefix), 1) + + return out @staticmethod - def parse(pattern: Union[str, "Pattern"]) -> str: + def parse(pattern: Union[str, "Pattern"], prefix: Optional[str] = None) -> str: """ Parse a pattern string into a regular expression. @@ -135,10 +141,14 @@ def parse(pattern: Union[str, "Pattern"]) -> str: `Pattern` enum. :param pattern: Pattern to parse. + :param prefix: Insert this after the pattern's start anchor (`^`). :returns: Regular expression. """ if isinstance(pattern, str) and "?P" in pattern: - return pattern + if prefix: + return pattern.replace("^", "^{}".format(prefix), 1) + else: + return pattern try: pattern = Pattern(pattern) @@ -152,7 +162,7 @@ def parse(pattern: Union[str, "Pattern"]) -> str: pattern, ) ) - return pattern.regex() + return pattern.regex(prefix) class Concern(Enum): @@ -226,7 +236,11 @@ def _run_cmd( def _match_version_pattern( - pattern: Union[str, Pattern], sources: Sequence[str], latest_source: bool, strict: bool + pattern: Union[str, Pattern], + sources: Sequence[str], + latest_source: bool, + strict: bool, + pattern_prefix: Optional[str], ) -> Optional[_MatchedVersionPattern]: """ :returns: Tuple of: @@ -245,7 +259,7 @@ def _match_version_pattern( tagged_metadata = None epoch = None # type: Optional[Union[str, int]] - pattern = Pattern.parse(pattern) + pattern = Pattern.parse(pattern, pattern_prefix) for source in sources[:1] if latest_source else sources: try: @@ -849,7 +863,9 @@ def parse(cls, version: str, pattern: Union[str, Pattern] = Pattern.Default) -> failed = False try: - matched_pattern = _match_version_pattern(pattern, [normalized], True, strict=True) + matched_pattern = _match_version_pattern( + pattern, [normalized], True, strict=True, pattern_prefix=None + ) except ValueError: failed = True @@ -998,6 +1014,7 @@ def from_git( full_commit: bool = False, strict: bool = False, path: Optional[Path] = None, + pattern_prefix: Optional[str] = None, ) -> "Version": r""" Determine a version based on Git tags. @@ -1013,6 +1030,7 @@ def from_git( :param strict: Elevate warnings to errors. When there are no tags, fail instead of falling back to 0.0.0. :param path: Directory to inspect, if not the current working directory. + :param pattern_prefix: Insert this after the pattern's start anchor (`^`). :returns: Detected version. """ vcs = Vcs.Git @@ -1066,7 +1084,9 @@ def from_git( vcs=vcs, ) - matched_pattern = _match_version_pattern(pattern, [tag], latest_tag, strict) + matched_pattern = _match_version_pattern( + pattern, [tag], latest_tag, strict, pattern_prefix + ) if matched_pattern is None: return cls._fallback( strict, @@ -1174,7 +1194,9 @@ def from_git( vcs=vcs, ) tags = [line.replace("refs/tags/", "") for line in msg.splitlines()] - matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + matched_pattern = _match_version_pattern( + pattern, tags, latest_tag, strict, pattern_prefix + ) else: code, msg = _run_cmd( 'git for-each-ref "refs/tags/**" --merged {}'.format(tag_branch) @@ -1213,7 +1235,9 @@ def from_git( detailed_tags.append(_GitRefInfo(*parts).with_tag_topo_lookup(tag_topo_lookup)) tags = [t.ref for t in sorted(detailed_tags, key=lambda x: x.sort_key, reverse=True)] - matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + matched_pattern = _match_version_pattern( + pattern, tags, latest_tag, strict, pattern_prefix + ) if matched_pattern is None: distance = 0 @@ -1264,6 +1288,7 @@ def from_mercurial( full_commit: bool = False, strict: bool = False, path: Optional[Path] = None, + pattern_prefix: Optional[str] = None, ) -> "Version": r""" Determine a version based on Mercurial tags. @@ -1277,6 +1302,7 @@ def from_mercurial( :param strict: Elevate warnings to errors. When there are no tags, fail instead of falling back to 0.0.0. :param path: Directory to inspect, if not the current working directory. + :param pattern_prefix: Insert this after the pattern's start anchor (`^`). :returns: Detected version. """ vcs = Vcs.Mercurial @@ -1314,7 +1340,9 @@ def from_mercurial( continue all_tags.append(parts[1]) - matched_pattern = _match_version_pattern(pattern, all_tags, latest_tag, strict) + matched_pattern = _match_version_pattern( + pattern, all_tags, latest_tag, strict, pattern_prefix + ) if matched_pattern is None: return cls._fallback( strict, @@ -1378,7 +1406,7 @@ def from_mercurial( ) tags = [tag for tags in [line.split(":") for line in msg.splitlines()] for tag in tags] - matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict, pattern_prefix) if matched_pattern is None: return cls._fallback( strict, @@ -1418,6 +1446,7 @@ def from_darcs( latest_tag: bool = False, strict: bool = False, path: Optional[Path] = None, + pattern_prefix: Optional[str] = None, ) -> "Version": r""" Determine a version based on Darcs tags. @@ -1430,6 +1459,7 @@ def from_darcs( :param strict: Elevate warnings to errors. When there are no tags, fail instead of falling back to 0.0.0. :param path: Directory to inspect, if not the current working directory. + :param pattern_prefix: Insert this after the pattern's start anchor (`^`). :returns: Detected version. """ vcs = Vcs.Darcs @@ -1459,7 +1489,7 @@ def from_darcs( ) tags = msg.splitlines() - matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict, pattern_prefix) if matched_pattern is None: return cls._fallback( strict, @@ -1498,6 +1528,7 @@ def from_subversion( tag_dir: str = "tags", strict: bool = False, path: Optional[Path] = None, + pattern_prefix: Optional[str] = None, ) -> "Version": r""" Determine a version based on Subversion tags. @@ -1511,6 +1542,7 @@ def from_subversion( :param strict: Elevate warnings to errors. When there are no tags, fail instead of falling back to 0.0.0. :param path: Directory to inspect, if not the current working directory. + :param pattern_prefix: Insert this after the pattern's start anchor (`^`). :returns: Detected version. """ vcs = Vcs.Subversion @@ -1562,7 +1594,7 @@ def from_subversion( tags_to_sources_revs[tag] = (source, rev) tags = sorted(tags_to_sources_revs, key=lambda x: tags_to_sources_revs[x], reverse=True) - matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict, pattern_prefix) if matched_pattern is None: return cls._fallback( strict, @@ -1600,6 +1632,7 @@ def from_bazaar( latest_tag: bool = False, strict: bool = False, path: Optional[Path] = None, + pattern_prefix: Optional[str] = None, ) -> "Version": r""" Determine a version based on Bazaar tags. @@ -1612,6 +1645,7 @@ def from_bazaar( :param strict: Elevate warnings to errors. When there are no tags, fail instead of falling back to 0.0.0. :param path: Directory to inspect, if not the current working directory. + :param pattern_prefix: Insert this after the pattern's start anchor (`^`). :returns: Detected version. """ vcs = Vcs.Bazaar @@ -1659,7 +1693,7 @@ def from_bazaar( } tags = [x[1] for x in sorted([(v, k) for k, v in tags_to_revs.items()], reverse=True)] - matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict, pattern_prefix) if matched_pattern is None: return cls._fallback( strict, @@ -1697,6 +1731,7 @@ def from_fossil( latest_tag: bool = False, strict: bool = False, path: Optional[Path] = None, + pattern_prefix: Optional[str] = None, ) -> "Version": r""" Determine a version based on Fossil tags. @@ -1708,6 +1743,7 @@ def from_fossil( :param strict: Elevate warnings to errors. When there are no tags, fail instead of falling back to 0.0.0. :param path: Directory to inspect, if not the current working directory. + :param pattern_prefix: Insert this after the pattern's start anchor (`^`). :returns: Detected version. """ vcs = Vcs.Fossil @@ -1793,7 +1829,7 @@ def from_fossil( ] matched_pattern = _match_version_pattern( - pattern, [t for t, d in tags_to_distance], latest_tag, strict + pattern, [t for t, d in tags_to_distance], latest_tag, strict, pattern_prefix ) if matched_pattern is None: return cls._fallback( @@ -1832,6 +1868,7 @@ def from_pijul( latest_tag: bool = False, strict: bool = False, path: Optional[Path] = None, + pattern_prefix: Optional[str] = None, ) -> "Version": r""" Determine a version based on Pijul tags. @@ -1844,6 +1881,7 @@ def from_pijul( :param strict: Elevate warnings to errors. When there are no tags, fail instead of falling back to 0.0.0. :param path: Directory to inspect, if not the current working directory. + :param pattern_prefix: Insert this after the pattern's start anchor (`^`). :returns: Detected version. """ vcs = Vcs.Pijul @@ -1934,7 +1972,7 @@ def from_pijul( for t in sorted(tag_meta_by_msg.values(), key=lambda x: x["timestamp"], reverse=True) ] - matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict) + matched_pattern = _match_version_pattern(pattern, tags, latest_tag, strict, pattern_prefix) if matched_pattern is None: return cls._fallback( strict, @@ -1983,6 +2021,7 @@ def from_any_vcs( full_commit: bool = False, strict: bool = False, path: Optional[Path] = None, + pattern_prefix: Optional[str] = None, ) -> "Version": r""" Determine a version based on a detected version control system. @@ -2011,6 +2050,7 @@ def from_any_vcs( :param strict: Elevate warnings to errors. When there are no tags, fail instead of falling back to 0.0.0. :param path: Directory to inspect, if not the current working directory. + :param pattern_prefix: Insert this after the pattern's start anchor (`^`). :returns: Detected version. """ vcs = _detect_vcs_from_archival(path) @@ -2031,6 +2071,7 @@ def from_vcs( full_commit: bool = False, strict: bool = False, path: Optional[Path] = None, + pattern_prefix: Optional[str] = None, ) -> "Version": r""" Determine a version based on a specific VCS setting. @@ -2053,10 +2094,11 @@ def from_vcs( :param strict: Elevate warnings to errors. When there are no tags, fail instead of falling back to 0.0.0. :param path: Directory to inspect, if not the current working directory. + :param pattern_prefix: Insert this after the pattern's start anchor (`^`). :returns: Detected version. """ return cls._do_vcs_callback( - vcs, pattern, latest_tag, tag_dir, tag_branch, full_commit, strict, path + vcs, pattern, latest_tag, tag_dir, tag_branch, full_commit, strict, path, pattern_prefix ) @classmethod @@ -2070,6 +2112,7 @@ def _do_vcs_callback( full_commit: bool, strict: bool, path: Optional[Path], + pattern_prefix: Optional[str] = None, ) -> "Version": mapping = { Vcs.Any: cls.from_any_vcs, @@ -2091,6 +2134,7 @@ def _do_vcs_callback( ("full_commit", full_commit), ("strict", strict), ("path", path), + ("pattern_prefix", pattern_prefix), ]: if kwarg in inspect.getfullargspec(callback).args: kwargs[kwarg] = value diff --git a/dunamai/__main__.py b/dunamai/__main__.py index e3dd209..3c271ec 100644 --- a/dunamai/__main__.py +++ b/dunamai/__main__.py @@ -50,6 +50,10 @@ " as a named preset, which may be one of the following: {}" ).format(", ".join(["`{}`".format(x.value) for x in Pattern])), }, + { + "triggers": ["--pattern-prefix"], + "help": "Insert this after the pattern's start anchor (`^`).", + }, { "triggers": ["--format"], "help": ( @@ -256,9 +260,10 @@ def from_vcs( full_commit: bool, strict: bool, path: Optional[Path], + pattern_prefix: Optional[str], ) -> None: version = Version.from_vcs( - vcs, pattern, latest_tag, tag_dir, tag_branch, full_commit, strict, path + vcs, pattern, latest_tag, tag_dir, tag_branch, full_commit, strict, path, pattern_prefix ) for concern in version.concerns: @@ -294,6 +299,7 @@ def main() -> None: full_commit, args.strict, Path(args.path) if args.path is not None else None, + args.pattern_prefix, ) elif args.command == "check": version = from_stdin(args.version) diff --git a/tests/integration/test_dunamai.py b/tests/integration/test_dunamai.py index 9403c1c..5df8043 100644 --- a/tests/integration/test_dunamai.py +++ b/tests/integration/test_dunamai.py @@ -167,6 +167,7 @@ def test__version__from_git__with_annotated_tags(tmp_path) -> None: # Verify tags with '/' work run("git tag test/v0.1.0") assert run(r'dunamai from any --pattern "^test/v(?P\d\.\d\.\d)"') == "0.1.0" + assert run('dunamai from any --pattern-prefix "test/"') == "0.1.0" (vcs / "foo.txt").write_text("bye") assert from_vcs() == Version("0.1.0", dirty=True, branch=b) diff --git a/tests/unit/test_dunamai.py b/tests/unit/test_dunamai.py index b097f1e..dc143b9 100644 --- a/tests/unit/test_dunamai.py +++ b/tests/unit/test_dunamai.py @@ -63,13 +63,19 @@ def inner(*args, **kwargs): def test__pattern__regex() -> None: assert Pattern.Default.regex() == VERSION_SOURCE_PATTERN - assert Pattern.DefaultUnprefixed.regex() != VERSION_SOURCE_PATTERN + assert Pattern.DefaultUnprefixed.regex() == VERSION_SOURCE_PATTERN.replace("^v", "^v?", 1) + assert Pattern.Default.regex("foo-") == VERSION_SOURCE_PATTERN.replace("^", "^foo-", 1) def test__pattern__parse() -> None: assert Pattern.parse(r"(?P\d+)") == r"(?P\d+)" + assert Pattern.parse(r"(?P\d+)", "foo-") == r"(?P\d+)" + assert Pattern.parse(r"^(?P\d+)", "foo-") == r"^foo-(?P\d+)" + assert Pattern.parse("default") == Pattern.Default.regex() assert Pattern.parse("default-unprefixed") == Pattern.DefaultUnprefixed.regex() + assert Pattern.parse("default", "foo-") == Pattern.Default.regex("foo-") + with pytest.raises(ValueError): Pattern.parse(r"foo") diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index b3a4461..3701ee7 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -24,6 +24,7 @@ def test__parse_args__from(): full_commit=False, strict=False, path=None, + pattern_prefix=None, ) assert parse_args(["from", "git"]).vcs == "git" assert parse_args(["from", "git", "--tag-branch", "foo"]).tag_branch == "foo" @@ -51,6 +52,7 @@ def test__parse_args__from(): assert parse_args(["from", "any", "--tagged-metadata"]).tagged_metadata is True assert parse_args(["from", "any", "--strict"]).strict is True assert parse_args(["from", "any", "--path", "/tmp"]).path == "/tmp" + assert parse_args(["from", "any", "--pattern-prefix", "foo-"]).pattern_prefix == "foo-" assert parse_args(["from", "subversion", "--tag-dir", "foo"]).tag_dir == "foo" with pytest.raises(SystemExit):