diff --git a/.gitignore b/.gitignore
index 204f712..ade162e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,5 @@ build/
dist/
htmlcov/
pip-wheel-metadata/
+# Used by github codespaces
+pythonenv*/
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 379fd21..0f629cf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,4 +6,10 @@
"python.linting.pylintEnabled": false,
"python.pythonPath": "${workspaceFolder}/.venv",
"yaml.format.enable": true,
+ "python.testing.pytestArgs": [
+ "."
+ ],
+ "python.testing.unittestEnabled": false,
+ "python.testing.nosetestsEnabled": false,
+ "python.testing.pytestEnabled": true,
}
diff --git a/dunamai/__init__.py b/dunamai/__init__.py
index d464760..c8d8365 100644
--- a/dunamai/__init__.py
+++ b/dunamai/__init__.py
@@ -10,9 +10,15 @@
from enum import Enum
from functools import total_ordering
from pathlib import Path
-from typing import Any, Callable, Mapping, Optional, Sequence, Tuple, TypeVar, Union
+from typing import Any, Callable, Mapping, Optional, Sequence, Tuple, TypeVar, Union, NamedTuple
+
+_VERSION_PATTERN = r"""
+ (?x) (?# ignore whitespace)
+ ^v(?P\d+\.\d+\.\d+) (?# v1.2.3)
+ (-?((?P[a-zA-Z]+)\.?(?P\d+)?))? (?# b0)
+ (\+(?P.+))?$ (?# +linux)
+ """
-_VERSION_PATTERN = r"^v(?P\d+\.\d+\.\d+)(-?((?P[a-zA-Z]+)\.?(?P\d+)?))?$"
# PEP 440: [N!]N(.N)*[{a|b|rc}N][.postN][.devN][+]
_VALID_PEP440 = r"^(\d!)?\d+(\.\d+)*((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?(\+.+)?$"
_VALID_SEMVER = (
@@ -59,9 +65,21 @@ def _run_cmd(
return (result.returncode, output)
+_match_version_pattern_res = NamedTuple(
+ "_match_version_pattern_res",
+ [
+ ("matched_tag", str),
+ ("base", str),
+ ("stage_revision", Optional[Tuple[str, Optional[int]]]),
+ ("newer_tags", Sequence[str]),
+ ("tagged_metadata", Optional[str]),
+ ],
+)
+
+
def _match_version_pattern(
pattern: str, sources: Sequence[str], latest_source: bool
-) -> Tuple[str, str, Optional[Tuple[str, Optional[int]]], Sequence[str]]:
+) -> _match_version_pattern_res:
"""
:return: Tuple of:
* matched tag
@@ -70,11 +88,13 @@ def _match_version_pattern(
* stage
* revision
* any newer unmatched tags
+ * tagged_metadata matched section
"""
pattern_match = None
base = None
stage_revision = None
newer_unmatched_tags = []
+ tagged_metadata = None
for source in sources[:1] if latest_source else sources:
pattern_match = re.search(pattern, source)
@@ -102,12 +122,15 @@ def _match_version_pattern(
try:
stage = pattern_match.group("stage")
revision = pattern_match.group("revision")
+ tagged_metadata = pattern_match.group("tagged_metadata")
if stage is not None:
stage_revision = (stage, None) if revision is None else (stage, int(revision))
except IndexError:
pass
- return (source, base, stage_revision, newer_unmatched_tags)
+ return _match_version_pattern_res(
+ source, base, stage_revision, newer_unmatched_tags, tagged_metadata
+ )
def _blank(value: Optional[_T], default: _T) -> _T:
@@ -155,7 +178,8 @@ def __init__(
stage: Tuple[str, Optional[int]] = None,
distance: int = 0,
commit: str = None,
- dirty: bool = None
+ dirty: bool = None,
+ tagged_metadata: Optional[str] = None
) -> None:
"""
:param base: Release segment, such as 0.1.0.
@@ -179,6 +203,8 @@ def __init__(
self.commit = commit
#: Whether there are uncommitted changes.
self.dirty = dirty
+ #: The version contains baked in tagged_metadata metadata
+ self.tagged_metadata = tagged_metadata
self._matched_tag = None # type: Optional[str]
self._newer_unmatched_tags = None # type: Optional[Sequence[str]]
@@ -227,12 +253,13 @@ def serialize(
format: str = None,
style: Style = None,
bump: bool = False,
+ tagged_metadata: bool = False,
) -> str:
"""
Create a string from the version info.
:param metadata: Metadata (commit, dirty) is normally included in
- the local version part if post or dev are set. Set this to True to
+ the tagged_metadata version part if post or dev are set. Set this to True to
always include metadata, or set it to False to always exclude it.
:param dirty: Set this to True to include a dirty flag in the
metadata if applicable. Inert when metadata=False.
@@ -244,6 +271,7 @@ def serialize(
* {revision}
* {distance}
* {commit}
+ * {tagged_metadata}
* {dirty} which expands to either "dirty" or "clean"
:param style: Built-in output formats. Will default to PEP 440 if not
set and no custom format given. If you specify both a style and a
@@ -252,6 +280,8 @@ def serialize(
:param bump: If true, increment the last part of the `base` by 1,
unless `stage` is set, in which case either increment `revision`
by 1 or set it to a default of 2 if there was no revision.
+ :param tagged_metadata: If true use the tagged_metadata in the version as the first
+ segment of metadata.
"""
base = self.base
revision = self.revision
@@ -271,6 +301,7 @@ def serialize(
revision=_blank(revision, ""),
distance=_blank(self.distance, ""),
commit=_blank(self.commit, ""),
+ tagged_metadata=_blank(self.tagged_metadata, ""),
dirty="dirty" if self.dirty else "clean",
)
if style is not None:
@@ -283,6 +314,9 @@ def serialize(
meta_parts = []
if metadata is not False:
+ # treat tagged_metadata segments as the first meta_part
+ if tagged_metadata and self.tagged_metadata:
+ meta_parts.append(self.tagged_metadata)
if (metadata or self.distance > 0) and self.commit is not None:
meta_parts.append(self.commit)
if dirty and self.dirty:
@@ -374,12 +408,21 @@ def from_git(cls, pattern: str = _VERSION_PATTERN, latest_tag: bool = False) ->
t[0]
for t in reversed(sorted(detailed_tags, key=lambda x: x[1] if x[2] is None else x[2]))
]
- tag, base, stage, unmatched = _match_version_pattern(pattern, tags, latest_tag)
+ tag, base, stage, unmatched, tagged_metadata = _match_version_pattern(
+ pattern, tags, latest_tag
+ )
code, msg = _run_cmd("git rev-list --count refs/tags/{}..HEAD".format(tag))
distance = int(msg)
- version = cls(base, stage=stage, distance=distance, commit=commit, dirty=dirty)
+ version = cls(
+ base,
+ stage=stage,
+ distance=distance,
+ commit=commit,
+ dirty=dirty,
+ tagged_metadata=tagged_metadata,
+ )
version._matched_tag = tag
version._newer_unmatched_tags = unmatched
return version
@@ -420,13 +463,22 @@ def from_mercurial(cls, pattern: str = _VERSION_PATTERN, latest_tag: bool = Fals
distance = 0
return cls("0.0.0", distance=distance, commit=commit, dirty=dirty)
tags = [tag for tags in [line.split(":") for line in msg.splitlines()] for tag in tags]
- tag, base, stage, unmatched = _match_version_pattern(pattern, tags, latest_tag)
+ tag, base, stage, unmatched, tagged_metadata = _match_version_pattern(
+ pattern, tags, latest_tag
+ )
code, msg = _run_cmd('hg log -r "{0}::{1} - {0}" --template "."'.format(tag, commit))
# The tag itself is in the list, so offset by 1.
distance = max(len(msg) - 1, 0)
- version = cls(base, stage=stage, distance=distance, commit=commit, dirty=dirty)
+ version = cls(
+ base,
+ stage=stage,
+ distance=distance,
+ commit=commit,
+ dirty=dirty,
+ tagged_metadata=tagged_metadata,
+ )
version._matched_tag = tag
version._newer_unmatched_tags = unmatched
return version
@@ -463,13 +515,22 @@ def from_darcs(cls, pattern: str = _VERSION_PATTERN, latest_tag: bool = False) -
distance = 0
return cls("0.0.0", distance=distance, commit=commit, dirty=dirty)
tags = msg.splitlines()
- tag, base, stage, unmatched = _match_version_pattern(pattern, tags, latest_tag)
+ tag, base, stage, unmatched, tagged_metadata = _match_version_pattern(
+ pattern, tags, latest_tag
+ )
code, msg = _run_cmd("darcs log --from-tag {} --count".format(tag))
# The tag itself is in the list, so offset by 1.
distance = int(msg) - 1
- version = cls(base, stage=stage, distance=distance, commit=commit, dirty=dirty)
+ version = cls(
+ base,
+ stage=stage,
+ distance=distance,
+ commit=commit,
+ dirty=dirty,
+ tagged_metadata=tagged_metadata,
+ )
version._matched_tag = tag
version._newer_unmatched_tags = unmatched
return version
@@ -528,13 +589,22 @@ def from_subversion(
source = int(match.group(1))
tags_to_sources_revs[tag] = (source, rev)
tags = sorted(tags_to_sources_revs, key=lambda x: tags_to_sources_revs[x], reverse=True)
- tag, base, stage, unmatched = _match_version_pattern(pattern, tags, latest_tag)
+ tag, base, stage, unmatched, tagged_metadata = _match_version_pattern(
+ pattern, tags, latest_tag
+ )
source, rev = tags_to_sources_revs[tag]
# The tag itself is in the list, so offset by 1.
distance = int(commit) - 1 - source
- version = cls(base, stage=stage, distance=distance, commit=commit, dirty=dirty)
+ version = cls(
+ base,
+ stage=stage,
+ distance=distance,
+ commit=commit,
+ dirty=dirty,
+ tagged_metadata=tagged_metadata,
+ )
version._matched_tag = tag
version._newer_unmatched_tags = unmatched
return version
@@ -575,11 +645,20 @@ def from_bazaar(cls, pattern: str = _VERSION_PATTERN, latest_tag: bool = False)
if line.split()[1] != "?"
}
tags = [x[1] for x in sorted([(v, k) for k, v in tags_to_revs.items()], reverse=True)]
- tag, base, stage, unmatched = _match_version_pattern(pattern, tags, latest_tag)
+ tag, base, stage, unmatched, tagged_metadata = _match_version_pattern(
+ pattern, tags, latest_tag
+ )
distance = int(commit) - tags_to_revs[tag]
- version = cls(base, stage=stage, distance=distance, commit=commit, dirty=dirty)
+ version = cls(
+ base,
+ stage=stage,
+ distance=distance,
+ commit=commit,
+ dirty=dirty,
+ tagged_metadata=tagged_metadata,
+ )
version._matched_tag = tag
version._newer_unmatched_tags = unmatched
return version
@@ -651,12 +730,19 @@ def from_fossil(cls, pattern: str = _VERSION_PATTERN, latest_tag: bool = False)
(line.rsplit(",", 1)[0][5:-1], int(line.rsplit(",", 1)[1]) - 1)
for line in msg.splitlines()
]
- tag, base, stage, unmatched = _match_version_pattern(
+ tag, base, stage, unmatched, tagged_metadata = _match_version_pattern(
pattern, [t for t, d in tags_to_distance], latest_tag
)
distance = dict(tags_to_distance)[tag]
- version = cls(base, stage=stage, distance=distance, commit=commit, dirty=dirty)
+ version = cls(
+ base,
+ stage=stage,
+ distance=distance,
+ commit=commit,
+ dirty=dirty,
+ tagged_metadata=tagged_metadata,
+ )
version._matched_tag = tag
version._newer_unmatched_tags = unmatched
return version
diff --git a/dunamai/__main__.py b/dunamai/__main__.py
index 55c3150..05fe36f 100644
--- a/dunamai/__main__.py
+++ b/dunamai/__main__.py
@@ -26,6 +26,12 @@
"dest": "dirty",
"help": "Include dirty flag if applicable",
},
+ {
+ "triggers": ["--tagged-metadata"],
+ "action": "store_true",
+ "dest": "tagged_metadata",
+ "help": "Include tagged metadata if applicable",
+ },
{
"triggers": ["--pattern"],
"default": _VERSION_PATTERN,
@@ -202,9 +208,10 @@ def from_vcs(
tag_dir: str,
debug: bool,
bump: bool,
+ tagged_metadata: bool,
) -> None:
version = Version.from_vcs(vcs, pattern, latest_tag, tag_dir)
- print(version.serialize(metadata, dirty, format, style, bump))
+ print(version.serialize(metadata, dirty, format, style, bump, tagged_metadata=tagged_metadata))
if debug:
print("# Matched tag: {}".format(version._matched_tag), file=sys.stderr)
print("# Newer unmatched tags: {}".format(version._newer_unmatched_tags), file=sys.stderr)
@@ -226,6 +233,7 @@ def main() -> None:
tag_dir,
args.debug,
args.bump,
+ args.tagged_metadata,
)
elif args.command == "check":
version = from_stdin(args.version)
diff --git a/tests/unit/test_dunamai.py b/tests/unit/test_dunamai.py
index 15e5e36..1db1af2 100644
--- a/tests/unit/test_dunamai.py
+++ b/tests/unit/test_dunamai.py
@@ -56,13 +56,14 @@ def inner(*args, **kwargs):
def test__version__init() -> None:
- v = Version("1", stage=("a", 2), distance=3, commit="abc", dirty=True)
+ v = Version("1", stage=("a", 2), distance=3, commit="abc", dirty=True, tagged_metadata="def")
assert v.base == "1"
assert v.stage == "a"
assert v.revision == 2
assert v.distance == 3
assert v.commit == "abc"
assert v.dirty
+ assert v.tagged_metadata == "def"
def test__version__str() -> None:
@@ -270,6 +271,10 @@ def test__version__serialize__pep440_metadata() -> None:
Version("0.1.0", distance=1, commit="abc").serialize(metadata=False) == "0.1.0.post1.dev0"
)
+ v = Version("0.1.0", distance=1, commit="abc", tagged_metadata="def")
+ serialized = v.serialize(tagged_metadata=True)
+ assert serialized == "0.1.0.post1.dev0+def.abc"
+
def test__version__serialize__semver_with_metadata() -> None:
style = Style.SemVer
@@ -490,15 +495,22 @@ def test__check_version__pvp() -> None:
def test__default_version_pattern() -> None:
- def check_re(tag: str, base: str = None, stage: str = None, revision: str = None) -> None:
+ def check_re(
+ tag: str,
+ base: str = None,
+ stage: str = None,
+ revision: str = None,
+ tagged_metadata: str = None,
+ ) -> None:
result = re.search(_VERSION_PATTERN, tag)
if result is None:
if any(x is not None for x in [base, stage, revision]):
- raise ValueError("Pattern did not match")
+ raise ValueError("Pattern did not match, {tag}".format(tag=tag))
else:
assert result.group("base") == base
assert result.group("stage") == stage
assert result.group("revision") == revision
+ assert result.group("tagged_metadata") == tagged_metadata
check_re("v0.1.0", "0.1.0")
check_re("av0.1.0")
@@ -515,6 +527,8 @@ def check_re(tag: str, base: str = None, stage: str = None, revision: str = None
check_re("v0.1.0rc.4", "0.1.0", "rc", "4")
check_re("v0.1.0-beta", "0.1.0", "beta")
+ check_re("v0.1.0rc.4+specifier", "0.1.0", "rc", "4", tagged_metadata="specifier")
+
def test__serialize_pep440():
assert serialize_pep440("1.2.3") == "1.2.3"
diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py
index e10543c..8e9b3fa 100644
--- a/tests/unit/test_main.py
+++ b/tests/unit/test_main.py
@@ -19,6 +19,7 @@ def test__parse_args__from():
tag_dir="tags",
debug=False,
bump=False,
+ tagged_metadata=False,
)
assert parse_args(["from", "git"]).vcs == "git"
assert parse_args(["from", "mercurial"]).vcs == "mercurial"
@@ -37,6 +38,7 @@ def test__parse_args__from():
assert parse_args(["from", "any", "--latest-tag"]).latest_tag is True
assert parse_args(["from", "any", "--tag-dir", "foo"]).tag_dir == "foo"
assert parse_args(["from", "any", "--debug"]).debug is True
+ assert parse_args(["from", "any", "--tagged-metadata"]).tagged_metadata is True
assert parse_args(["from", "subversion", "--tag-dir", "foo"]).tag_dir == "foo"
with pytest.raises(SystemExit):