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