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):