Skip to content

Commit

Permalink
Add check command
Browse files Browse the repository at this point in the history
  • Loading branch information
mtkennerly committed Apr 16, 2019
1 parent 16f6e04 commit 0554102
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 60 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

## Unreleased

* Added the `dunamai check` command and the corresponding `check_version`
function.
* Added the option to check just the latest tag or to keep checking tags
until a match is found. The default behavior is now to keep checking.
* Added enforcement of Semantic Versioning rule against numeric segments
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ v0.2.0+7.g29045e8
$ dunamai from any --format "v{base}" --style pep440
Version 'v0.2.0' does not conform to the PEP 440 style

# Validate your own freeform versions:
$ dunamai check 0.01.0 --style semver
Version '0.01.0' does not conform to the Semantic Versioning style

# More info
$ dunamai --help
```
Expand Down
44 changes: 24 additions & 20 deletions dunamai/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__all__ = ["get_version", "Style", "Version"]
__all__ = ["check_version", "get_version", "Style", "Version"]

import os
import pkg_resources
Expand Down Expand Up @@ -221,7 +221,7 @@ def serialize(
dirty="dirty" if self.dirty else "clean",
)
if style is not None:
self._validate(out, style)
check_version(out, style)
return out

if style is None:
Expand Down Expand Up @@ -299,26 +299,9 @@ def serialize(
if metadata_segment:
out += "-{}".format(metadata_segment)

self._validate(out, style)
check_version(out, style)
return out

def _validate(self, serialized: str, style: Style) -> None:
if style is None:
return
groups = {
Style.Pep440: ("PEP 440", _VALID_PEP440),
Style.SemVer: ("Semantic Versioning", _VALID_SEMVER),
Style.Pvp: ("PVP", _VALID_PVP),
}
name, pattern = groups[style]
failure_message = "Version '{}' does not conform to the {} style".format(serialized, name)
if not re.search(pattern, serialized):
raise ValueError(failure_message)
if style == Style.SemVer:
parts = re.split(r"[.-]", serialized.split("+", 1)[0])
if any(re.search(r"^0[0-9]+$", x) for x in parts):
raise ValueError(failure_message)

@classmethod
def from_git(cls, pattern: str = _VERSION_PATTERN, latest_tag: bool = False) -> "Version":
r"""
Expand Down Expand Up @@ -522,6 +505,27 @@ def from_any_vcs(cls, pattern: str = None, latest_tag: bool = False) -> "Version
raise RuntimeError("Unable to detect version control system.")


def check_version(version: str, style: Style = Style.Pep440) -> None:
"""
Check if a version is valid for a style.
:param version: Version to check.
:param style: Style against which to check.
"""
name, pattern = {
Style.Pep440: ("PEP 440", _VALID_PEP440),
Style.SemVer: ("Semantic Versioning", _VALID_SEMVER),
Style.Pvp: ("PVP", _VALID_PVP),
}[style]
failure_message = "Version '{}' does not conform to the {} style".format(version, name)
if not re.search(pattern, version):
raise ValueError(failure_message)
if style == Style.SemVer:
parts = re.split(r"[.-]", version.split("+", 1)[0])
if any(re.search(r"^0[0-9]+$", x) for x in parts):
raise ValueError(failure_message)


def get_version(
name: str,
first_choice: Callable[[], Optional[Version]] = None,
Expand Down
36 changes: 34 additions & 2 deletions dunamai/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from enum import Enum
from typing import Mapping, Optional

from dunamai import Version, Style, _VERSION_PATTERN
from dunamai import check_version, Version, Style, _VERSION_PATTERN


class Vcs(Enum):
Expand Down Expand Up @@ -108,7 +108,24 @@ class Vcs(Enum):
],
},
},
}
},
"check": {
"description": "Check if a version is valid for a style",
"args": [
{
"triggers": [],
"dest": "version",
"help": "Version to check; may be piped in",
"nargs": "?",
},
{
"triggers": ["--style"],
"choices": [x.value for x in Style],
"default": Style.Pep440.value,
"help": "Style against which to check",
},
],
},
},
}

Expand Down Expand Up @@ -141,6 +158,16 @@ def parse_args(argv=None) -> argparse.Namespace:
return build_parser(cli_spec).parse_args(argv)


def from_stdin(value: Optional[str]) -> Optional[str]:
if value is not None:
return value

if not sys.stdin.isatty():
return sys.stdin.readline().strip()

return None


def from_vcs(
vcs: Vcs,
pattern: str,
Expand Down Expand Up @@ -180,6 +207,11 @@ def main() -> None:
args.latest_tag,
tag_dir,
)
elif args.command == "check":
version = from_stdin(args.version)
if version is None:
raise ValueError("A version must be specified")
check_version(version, Style(args.style))
except Exception as e:
print(e, file=sys.stderr)
sys.exit(1)
112 changes: 74 additions & 38 deletions tests/test_dunamai.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from dunamai import get_version, Version, Style, _run_cmd
from dunamai import check_version, get_version, Version, Style, _run_cmd


@contextmanager
Expand Down Expand Up @@ -351,46 +351,9 @@ def test__version__serialize__format():
)
== "2,1,a,3,4,5,abc,dirty"
)

assert Version("0.1.0").serialize(format="{base}", style=Style.Pep440) == "0.1.0"
with pytest.raises(ValueError):
Version("0.1.0").serialize(format="v{base}", style=Style.Pep440)

assert Version("0.1.0").serialize(format="{base}", style=Style.SemVer) == "0.1.0"
with pytest.raises(ValueError):
Version("0.1.0").serialize(format="v{base}", style=Style.SemVer)

# "-" is a valid identifier.
Version("0.1.0").serialize(style=Style.SemVer, format="0.1.0--")
Version("0.1.0").serialize(style=Style.SemVer, format="0.1.0--.-")


def test__version__serialize__error_conditions():
with pytest.raises(ValueError):
Version("x").serialize()
with pytest.raises(ValueError):
v = Version("1", pre=("a", 3))
v.pre_type = "x"
v.serialize()

# No leading zeroes in numeric segments:
with pytest.raises(ValueError):
Version("00.0.0").serialize(style=Style.SemVer)
with pytest.raises(ValueError):
Version("0.1.0").serialize(style=Style.SemVer, format="0.01.0")
with pytest.raises(ValueError):
Version("0.1.0").serialize(style=Style.SemVer, format="0.1.0-alpha.02")
# But leading zeroes are fine for non-numeric parts:
Version("0.1.0").serialize(style=Style.SemVer, format="0.1.0-alpha.02a")

# Identifiers can't be empty:
with pytest.raises(ValueError):
Version("0.1.0").serialize(style=Style.SemVer, format="0.1.0-.")
with pytest.raises(ValueError):
Version("0.1.0").serialize(style=Style.SemVer, format="0.1.0-a.")
with pytest.raises(ValueError):
Version("0.1.0").serialize(style=Style.SemVer, format="0.1.0-.a")


def test__get_version__from_name():
assert get_version("dunamai") == Version(pkg_resources.get_distribution("dunamai").version)
Expand Down Expand Up @@ -596,3 +559,76 @@ def test__version__from_any_vcs(tmp_path):
with chdir(tmp_path):
with pytest.raises(RuntimeError):
Version.from_any_vcs()


def test__check_version__pep440():
check_version("0.1.0")
check_version("0.01.0")

check_version("2!0.1.0")
check_version("0.1.0a1")
check_version("0.1.0b1")
check_version("0.1.0rc1")
with pytest.raises(ValueError):
check_version("0.1.0x1")

check_version("0.1.0.post0")
check_version("0.1.0.dev0")
check_version("0.1.0.post0.dev0")
with pytest.raises(ValueError):
check_version("0.1.0.other0")

check_version("0.1.0+abc.dirty")

check_version("2!0.1.0a1.post0.dev0+abc.dirty")


def test__check_version__semver():
style = Style.SemVer

check_version("0.1.0", style=style)
check_version("0.1.0-alpha.1", style=style)
check_version("0.1.0+abc", style=style)
check_version("0.1.0-alpha.1.beta.2+abc.dirty", style=style)

with pytest.raises(ValueError):
check_version("1", style=style)
with pytest.raises(ValueError):
check_version("0.1", style=style)
with pytest.raises(ValueError):
check_version("0.0.0.1", style=style)

# "-" is a valid identifier.
Version("0.1.0--").serialize(style=style)
Version("0.1.0--.-").serialize(style=style)

# No leading zeroes in numeric segments:
with pytest.raises(ValueError):
Version("00.0.0").serialize(style=style)
with pytest.raises(ValueError):
Version("0.01.0").serialize(style=style)
with pytest.raises(ValueError):
Version("0.1.0-alpha.02").serialize(style=style)
# But leading zeroes are fine for non-numeric parts:
Version("0.1.0-alpha.02a").serialize(style=style)

# Identifiers can't be empty:
with pytest.raises(ValueError):
Version("0.1.0-.").serialize(style=style)
with pytest.raises(ValueError):
Version("0.1.0-a.").serialize(style=style)
with pytest.raises(ValueError):
Version("0.1.0-.a").serialize(style=style)


def test__check_version__pvp():
style = Style.Pvp

check_version("1", style=style)
check_version("0.1", style=style)
check_version("0.0.1", style=style)
check_version("0.0.0.1", style=style)
check_version("0.1.0-alpha-1", style=style)

with pytest.raises(ValueError):
check_version("0.1.0-a.1", style=style)
23 changes: 23 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from argparse import Namespace

import pytest

from dunamai import _run_cmd
from dunamai.__main__ import parse_args, _VERSION_PATTERN


Expand Down Expand Up @@ -28,3 +31,23 @@ def test__parse_args__from():
assert parse_args(["from", "any", "--style", "semver"]).style == "semver"
assert parse_args(["from", "any", "--latest-tag"]).latest_tag is True
assert parse_args(["from", "subversion", "--tag-dir", "foo"]).tag_dir == "foo"

with pytest.raises(SystemExit):
parse_args(["from", "unknown"])


def test__parse_args__check():
assert parse_args(["check", "0.1.0"]) == Namespace(
command="check", version="0.1.0", style="pep440"
)
assert parse_args(["check", "0.1.0", "--style", "semver"]).style == "semver"
assert parse_args(["check", "0.1.0", "--style", "pvp"]).style == "pvp"

with pytest.raises(SystemExit):
parse_args(["check", "0.1.0", "--style", "unknown"])


def test__cli_check():
_run_cmd("dunamai check 0.01.0")
_run_cmd("dunamai check v0.1.0", codes=[1])
_run_cmd("dunamai check 0.01.0 --style semver", codes=[1])

0 comments on commit 0554102

Please sign in to comment.