Add more flexible parsing and formating

This commit is contained in:
Manuel Barkhau 2019-01-06 14:38:20 +01:00
parent 32447b03d4
commit 9eda61d95b
13 changed files with 932 additions and 359 deletions

View file

@ -75,15 +75,15 @@ push = True
[pycalver:file_patterns] [pycalver:file_patterns]
bootstrapit.sh = bootstrapit.sh =
PACKAGE_VERSION="{version}" PACKAGE_VERSION="{pycalver}"
setup.cfg = setup.cfg =
current_version = {version} current_version = {pycalver}
setup.py = setup.py =
version="{pep440_version}" version="{pep440_pycalver}"
src/pycalver/__init__.py = src/pycalver/__init__.py =
__version__ = "{version}" __version__ = "{pycalver}"
src/pycalver/__main__.py = src/pycalver/__main__.py =
click.version_option(version="{version}") click.version_option(version="{pycalver}")
README.md = README.md =
[PyCalVer {version}] [PyCalVer {version}]
https://img.shields.io/badge/PyCalVer-{calver}{build}-blue.svg https://img.shields.io/badge/PyCalVer-{calver}{build}-blue.svg

View file

@ -16,7 +16,6 @@ import logging
import typing as typ import typing as typ
from . import vcs from . import vcs
from . import parse
from . import config from . import config
from . import version from . import version
from . import rewrite from . import rewrite
@ -25,19 +24,22 @@ from . import rewrite
_VERBOSE = 0 _VERBOSE = 0
try: # try:
import backtrace # import backtrace
# To enable pretty tracebacks: # # To enable pretty tracebacks:
# echo "export ENABLE_BACKTRACE=1;" >> ~/.bashrc # # echo "export ENABLE_BACKTRACE=1;" >> ~/.bashrc
backtrace.hook(align=True, strip_path=True, enable_on_envvar_only=True) # backtrace.hook(align=True, strip_path=True, enable_on_envvar_only=True)
except ImportError: # except ImportError:
pass # pass
click.disable_unicode_literals_warning = True click.disable_unicode_literals_warning = True
VALID_RELEASE_VALUES = ("alpha", "beta", "dev", "rc", "post", "final")
log = logging.getLogger("pycalver.cli") log = logging.getLogger("pycalver.cli")
@ -57,11 +59,11 @@ def _init_logging(verbose: int = 0) -> None:
def _validate_release_tag(release: str) -> None: def _validate_release_tag(release: str) -> None:
if release in parse.VALID_RELEASE_VALUES: if release in VALID_RELEASE_VALUES:
return return
log.error(f"Invalid argument --release={release}") log.error(f"Invalid argument --release={release}")
log.error(f"Valid arguments are: {', '.join(parse.VALID_RELEASE_VALUES)}") log.error(f"Valid arguments are: {', '.join(VALID_RELEASE_VALUES)}")
sys.exit(1) sys.exit(1)
@ -77,22 +79,40 @@ def cli(verbose: int = 0):
@cli.command() @cli.command()
@click.argument("old_version") @click.argument("old_version")
@click.argument("pattern", default="{pycalver}")
@click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.") @click.option('-v', '--verbose', count=True, help="Control log level. -vv for debug level.")
@click.option( @click.option(
"--release", default=None, metavar="<name>", help="Override release name of current_version" "--release", default=None, metavar="<name>", help="Override release name of current_version"
) )
def test(old_version: str, verbose: int = 0, release: str = None) -> None: @click.option("--major", is_flag=True, default=False, help="Increment major component.")
@click.option("--minor", is_flag=True, default=False, help="Increment minor component.")
@click.option("--patch", is_flag=True, default=False, help="Increment patch component.")
def test(
old_version: str,
pattern : str = "{pycalver}",
verbose : int = 0,
release : str = None,
major : bool = False,
minor : bool = False,
patch : bool = False,
) -> None:
"""Increment a version number for demo purposes.""" """Increment a version number for demo purposes."""
_init_logging(verbose=max(_VERBOSE, verbose)) _init_logging(verbose=max(_VERBOSE, verbose))
if release: if release:
_validate_release_tag(release) _validate_release_tag(release)
new_version = version.incr(old_version, release=release) new_version = version.incr(
pep440_version = version.pycalver_to_pep440(new_version) old_version, pattern=pattern, release=release, major=major, minor=minor, patch=patch
)
if new_version is None:
log.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.")
sys.exit(1)
print("PyCalVer Version:", new_version) pep440_version = version.to_pep440(new_version)
print("PEP440 Version :", pep440_version)
print("New Version:", new_version)
print("PEP440 :", pep440_version)
def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config:
@ -103,12 +123,12 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config:
log.info(f"fetching tags from remote (to turn off use: -n / --no-fetch)") log.info(f"fetching tags from remote (to turn off use: -n / --no-fetch)")
_vcs.fetch() _vcs.fetch()
version_tags = [tag for tag in _vcs.ls_tags() if version.PYCALVER_RE.match(tag)] version_tags = [tag for tag in _vcs.ls_tags() if version.is_valid(tag, cfg.version_pattern)]
if version_tags: if version_tags:
version_tags.sort(reverse=True) version_tags.sort(reverse=True)
log.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") log.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
latest_version_tag = version_tags[0] latest_version_tag = version_tags[0]
latest_version_pep440 = version.pycalver_to_pep440(latest_version_tag) latest_version_pep440 = version.to_pep440(latest_version_tag)
if latest_version_tag > cfg.current_version: if latest_version_tag > cfg.current_version:
log.info(f"Working dir version : {cfg.current_version}") log.info(f"Working dir version : {cfg.current_version}")
log.info(f"Latest version from {_vcs.name:>3} tag: {latest_version_tag}") log.info(f"Latest version from {_vcs.name:>3} tag: {latest_version_tag}")
@ -143,7 +163,7 @@ def show(verbose: int = 0, fetch: bool = True) -> None:
cfg = _update_cfg_from_vcs(cfg, fetch=fetch) cfg = _update_cfg_from_vcs(cfg, fetch=fetch)
print(f"Current Version: {cfg.current_version}") print(f"Current Version: {cfg.current_version}")
print(f"PEP440 Version : {cfg.pep440_version}") print(f"PEP440 : {cfg.pep440_version}")
@cli.command() @cli.command()
@ -235,7 +255,7 @@ def _bump(cfg: config.Config, new_version: str, allow_dirty: bool = False) -> No
metavar="<name>", metavar="<name>",
help=( help=(
f"Override release name of current_version. Valid options are: " f"Override release name of current_version. Valid options are: "
f"{', '.join(parse.VALID_RELEASE_VALUES)}." f"{', '.join(VALID_RELEASE_VALUES)}."
), ),
) )
@click.option( @click.option(
@ -248,12 +268,18 @@ def _bump(cfg: config.Config, new_version: str, allow_dirty: bool = False) -> No
"to files with version strings." "to files with version strings."
), ),
) )
@click.option("--major", is_flag=True, default=False, help="Increment major component.")
@click.option("--minor", is_flag=True, default=False, help="Increment minor component.")
@click.option("--patch", is_flag=True, default=False, help="Increment patch component.")
def bump( def bump(
release : typ.Optional[str] = None, release : typ.Optional[str] = None,
verbose : int = 0, verbose : int = 0,
dry : bool = False, dry : bool = False,
allow_dirty: bool = False, allow_dirty: bool = False,
fetch : bool = True, fetch : bool = True,
major : bool = False,
minor : bool = False,
patch : bool = False,
) -> None: ) -> None:
"""Increment the current version string and update project files.""" """Increment the current version string and update project files."""
verbose = max(_VERBOSE, verbose) verbose = max(_VERBOSE, verbose)
@ -272,7 +298,17 @@ def bump(
cfg = _update_cfg_from_vcs(cfg, fetch=fetch) cfg = _update_cfg_from_vcs(cfg, fetch=fetch)
old_version = cfg.current_version old_version = cfg.current_version
new_version = version.incr(old_version, release=release) new_version = version.incr(
old_version,
pattern=cfg.version_pattern,
release=release,
major=major,
minor=minor,
patch=patch,
)
if new_version is None:
log.error(f"Invalid version '{old_version}' and/or pattern '{cfg.version_pattern}'.")
sys.exit(1)
log.info(f"Old Version: {old_version}") log.info(f"Old Version: {old_version}")
log.info(f"New Version: {new_version}") log.info(f"New Version: {new_version}")

View file

@ -77,6 +77,7 @@ class Config(typ.NamedTuple):
"""Container for parameters parsed from a config file.""" """Container for parameters parsed from a config file."""
current_version: str current_version: str
version_pattern: str
pep440_version : str pep440_version : str
commit: bool commit: bool
@ -90,6 +91,7 @@ def _debug_str(cfg: Config) -> str:
cfg_str_parts = [ cfg_str_parts = [
f"Config Parsed: Config(", f"Config Parsed: Config(",
f"current_version='{cfg.current_version}'", f"current_version='{cfg.current_version}'",
f"version_pattern='{{pycalver}}'",
f"pep440_version='{cfg.pep440_version}'", f"pep440_version='{cfg.pep440_version}'",
f"commit={cfg.commit}", f"commit={cfg.commit}",
f"tag={cfg.tag}", f"tag={cfg.tag}",
@ -179,17 +181,52 @@ def _parse_toml(cfg_buffer: typ.TextIO) -> RawConfig:
return raw_cfg return raw_cfg
def _normalize_file_patterns(raw_cfg: RawConfig) -> FilePatterns:
version_str = raw_cfg['current_version']
version_pattern = raw_cfg['version_pattern']
pep440_version = version.to_pep440(version_str)
file_patterns = raw_cfg['file_patterns']
for filepath, patterns in list(file_patterns.items()):
if not os.path.exists(filepath):
log.warning(f"Invalid config, no such file: {filepath}")
normalized_patterns: typ.List[str] = []
for pattern in patterns:
normalized_pattern = pattern.replace("{version}", version_pattern)
if version_pattern == "{pycalver}":
normalized_pattern = normalized_pattern.replace(
"{pep440_version}", "{pep440_pycalver}"
)
elif version_pattern == "{semver}":
normalized_pattern = normalized_pattern.replace("{pep440_version}", "{semver}")
elif "{pep440_version}" in pattern:
log.warning(f"Invalid config, cannot match '{pattern}' for '{filepath}'.")
log.warning(f"No mapping of '{version_pattern}' to '{pep440_version}'")
normalized_patterns.append(normalized_pattern)
file_patterns[filepath] = normalized_patterns
return file_patterns
def _parse_config(raw_cfg: RawConfig) -> Config: def _parse_config(raw_cfg: RawConfig) -> Config:
"""Parse configuration which was loaded from an .ini/.cfg or .toml file."""
if 'current_version' not in raw_cfg: if 'current_version' not in raw_cfg:
raise ValueError("Missing 'pycalver.current_version'") raise ValueError("Missing 'pycalver.current_version'")
version_str = raw_cfg['current_version'] version_str = raw_cfg['current_version']
version_str = raw_cfg['current_version'] = version_str.strip("'\" ") version_str = raw_cfg['current_version'] = version_str.strip("'\" ")
if version.PYCALVER_RE.match(version_str) is None: version_pattern = raw_cfg.get('version_pattern', "{pycalver}")
raise ValueError(f"Invalid current_version = {version_str}") version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ")
pep440_version = version.pycalver_to_pep440(version_str) # NOTE (mb 2019-01-05): trigger ValueError if version_pattern
# and current_version don't work together.
version.parse_version_info(version_str, version_pattern)
pep440_version = version.to_pep440(version_str)
commit = raw_cfg['commit'] commit = raw_cfg['commit']
tag = raw_cfg['tag'] tag = raw_cfg['tag']
@ -206,13 +243,9 @@ def _parse_config(raw_cfg: RawConfig) -> Config:
if push and not commit: if push and not commit:
raise ValueError("pycalver.commit = true required if pycalver.push = true") raise ValueError("pycalver.commit = true required if pycalver.push = true")
file_patterns = raw_cfg['file_patterns'] file_patterns = _normalize_file_patterns(raw_cfg)
for filepath in file_patterns.keys(): cfg = Config(version_str, version_pattern, pep440_version, tag, commit, push, file_patterns)
if not os.path.exists(filepath):
log.warning(f"Invalid configuration, no such file: {filepath}")
cfg = Config(version_str, pep440_version, tag, commit, push, file_patterns)
log.debug(_debug_str(cfg)) log.debug(_debug_str(cfg))
return cfg return cfg
@ -241,6 +274,7 @@ def parse(ctx: ProjectContext) -> MaybeConfig:
DEFAULT_CONFIGPARSER_BASE_TMPL = """ DEFAULT_CONFIGPARSER_BASE_TMPL = """
[pycalver] [pycalver]
current_version = "{initial_version}" current_version = "{initial_version}"
version_pattern = "{{pycalver}}"
commit = True commit = True
tag = True tag = True
push = True push = True
@ -279,6 +313,7 @@ README.md =
DEFAULT_TOML_BASE_TMPL = """ DEFAULT_TOML_BASE_TMPL = """
[pycalver] [pycalver]
current_version = "{initial_version}" current_version = "{initial_version}"
version_pattern = "{{pycalver}}"
commit = true commit = true
tag = true tag = true
push = true push = true

View file

@ -5,50 +5,14 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
"""Parse PyCalVer strings from files.""" """Parse PyCalVer strings from files."""
import re
import logging import logging
import typing as typ import typing as typ
from . import patterns
log = logging.getLogger("pycalver.parse") log = logging.getLogger("pycalver.parse")
VALID_RELEASE_VALUES = ("alpha", "beta", "dev", "rc", "post", "final")
PATTERN_ESCAPES = [
("\u005c", "\u005c\u005c"),
("-" , "\u005c-"),
("." , "\u005c."),
("+" , "\u005c+"),
("*" , "\u005c*"),
("{" , "\u005c{{"),
("}" , "\u005c}}"),
("[" , "\u005c["),
("]" , "\u005c]"),
("(" , "\u005c("),
(")" , "\u005c)"),
]
# NOTE (mb 2018-09-03): These are matchers for parts, which are
# used in the patterns, they're not for validation. This means
# that they may find strings, which are not valid pycalver
# strings, when parsed in their full context. For such cases,
# the patterns should be expanded.
RE_PATTERN_PARTS = {
'pep440_version': r"\d{6}\.[1-9]\d*(a|b|dev|rc|post)?\d*",
'version' : r"v\d{6}\.\d{4,}(\-(alpha|beta|dev|rc|post|final))?",
'calver' : r"v\d{6}",
'year' : r"\d{4}",
'month' : r"\d{2}",
'build' : r"\.\d{4,}",
'build_no' : r"\d{4,}",
'release' : r"(\-(alpha|beta|dev|rc|post|final))?",
'release_tag' : r"(alpha|beta|dev|rc|post|final)?",
}
class PatternMatch(typ.NamedTuple): class PatternMatch(typ.NamedTuple):
"""Container to mark a version string in a file.""" """Container to mark a version string in a file."""
@ -62,26 +26,10 @@ class PatternMatch(typ.NamedTuple):
PatternMatches = typ.Iterable[PatternMatch] PatternMatches = typ.Iterable[PatternMatch]
def compile_pattern(pattern: str) -> typ.Pattern[str]:
pattern_tmpl = pattern
for char, escaped in PATTERN_ESCAPES:
pattern_tmpl = pattern_tmpl.replace(char, escaped)
# undo escaping only for valid part names
for part_name in RE_PATTERN_PARTS.keys():
pattern_tmpl = pattern_tmpl.replace(
"\u005c{{" + part_name + "\u005c}}", "{" + part_name + "}"
)
pattern_str = pattern_tmpl.format(**RE_PATTERN_PARTS)
return re.compile(pattern_str)
def _iter_for_pattern(lines: typ.List[str], pattern: str) -> PatternMatches: def _iter_for_pattern(lines: typ.List[str], pattern: str) -> PatternMatches:
# The pattern is escaped, so that everything besides the format # The pattern is escaped, so that everything besides the format
# string variables is treated literally. # string variables is treated literally.
pattern_re = compile_pattern(pattern) pattern_re = patterns.compile_pattern(pattern)
for lineno, line in enumerate(lines): for lineno, line in enumerate(lines):
match = pattern_re.search(line) match = pattern_re.search(line)
@ -93,12 +41,12 @@ def iter_matches(lines: typ.List[str], patterns: typ.List[str]) -> PatternMatche
"""Iterate over all matches of any pattern on any line. """Iterate over all matches of any pattern on any line.
>>> lines = ["__version__ = 'v201712.0002-alpha'"] >>> lines = ["__version__ = 'v201712.0002-alpha'"]
>>> patterns = ["{version}", "{pep440_version}"] >>> patterns = ["{pycalver}", "{pep440_pycalver}"]
>>> matches = list(iter_matches(lines, patterns)) >>> matches = list(iter_matches(lines, patterns))
>>> assert matches[0] == PatternMatch( >>> assert matches[0] == PatternMatch(
... lineno = 0, ... lineno = 0,
... line = "__version__ = 'v201712.0002-alpha'", ... line = "__version__ = 'v201712.0002-alpha'",
... pattern= "{version}", ... pattern= "{pycalver}",
... span = (15, 33), ... span = (15, 33),
... match = "v201712.0002-alpha", ... match = "v201712.0002-alpha",
... ) ... )

200
src/pycalver/patterns.py Normal file
View file

@ -0,0 +1,200 @@
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018 Manuel Barkhau (@mbarkhau) - MIT License
# SPDX-License-Identifier: MIT
"""Compose Regular Expressions from Patterns.
>>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict()
>>> assert version_info == {
... "pycalver" : "v201712.0123-alpha",
... "vYYYYMM" : "v201712",
... "year" : "2017",
... "month" : "12",
... "build" : ".0123",
... "build_no" : "0123",
... "release" : "-alpha",
... "release_tag" : "alpha",
... }
>>>
>>> version_info = PYCALVER_RE.match("v201712.0033").groupdict()
>>> assert version_info == {
... "pycalver" : "v201712.0033",
... "vYYYYMM" : "v201712",
... "year" : "2017",
... "month" : "12",
... "build" : ".0033",
... "build_no" : "0033",
... "release" : None,
... "release_tag": None,
... }
"""
import re
import typing as typ
# https://regex101.com/r/fnj60p/10
PYCALVER_PATTERN = r"""
\b
(?P<pycalver>
(?P<vYYYYMM>
v # "v" version prefix
(?P<year>\d{4})
(?P<month>\d{2})
)
(?P<build>
\. # "." build nr prefix
(?P<build_no>\d{4,})
)
(?P<release>
\- # "-" release prefix
(?P<release_tag>alpha|beta|dev|rc|post)
)?
)(?:\s|$)
"""
PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE)
PATTERN_ESCAPES = [
("\u005c", "\u005c\u005c"),
("-" , "\u005c-"),
("." , "\u005c."),
("+" , "\u005c+"),
("*" , "\u005c*"),
("{" , "\u005c{"),
("}" , "\u005c}"),
("[" , "\u005c["),
("]" , "\u005c]"),
("(" , "\u005c("),
(")" , "\u005c)"),
]
COMPOSITE_PART_PATTERNS = {
'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?",
'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?",
'calver' : r"v{year}{month}",
'semver' : r"{MAJOR}\.{MINOR}\.{PATCH}",
'release_tag' : r"{tag}",
'build' : r"\.{bid}",
'release' : r"-{tag}",
# depricated
'pep440_version': r"{year}{month}\.{BID}(?:{pep440_tag})?",
}
PART_PATTERNS = {
'year' : r"\d{4}",
'month' : r"(?:0[0-9]|1[0-2])",
'build_no' : r"\d{4,}",
'pep440_tag': r"(?:a|b|dev|rc|post)?\d*",
'tag' : r"(?:alpha|beta|dev|rc|post|final)",
'yy' : r"\d{2}",
'yyyy' : r"\d{4}",
'quarter' : r"[1-4]",
'iso_week' : r"(?:[0-4]\d|5[0-3])",
'us_week' : r"(?:[0-4]\d|5[0-3])",
'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])",
'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
'MAJOR' : r"\d+",
'MINOR' : r"\d+",
'MM' : r"\d{2,}",
'MMM' : r"\d{3,}",
'MMMM' : r"\d{4,}",
'MMMMM' : r"\d{5,}",
'PATCH' : r"\d+",
'PP' : r"\d{2,}",
'PPP' : r"\d{3,}",
'PPPP' : r"\d{4,}",
'PPPPP' : r"\d{5,}",
'bid' : r"\d{4,}",
'BID' : r"[1-9]\d*",
'BB' : r"[1-9]\d{1,}",
'BBB' : r"[1-9]\d{2,}",
'BBBB' : r"[1-9]\d{3,}",
'BBBBB' : r"[1-9]\d{4,}",
'BBBBBB' : r"[1-9]\d{5,}",
'BBBBBBB' : r"[1-9]\d{6,}",
}
FULL_PART_FORMATS = {
'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}",
'pycalver' : "v{year}{month:02}.{bid}{release}",
'calver' : "v{year}{month:02}",
'semver' : "{MAJOR}.{MINOR}.{PATCH}",
'release_tag' : "{tag}",
'build' : ".{bid}",
# NOTE (mb 2019-01-04): since release is optional, it
# is treates specially in version.format
# 'release' : "-{tag}",
'month' : "{month:02}",
'build_no': "{bid}",
'iso_week': "{iso_week:02}",
'us_week' : "{us_week:02}",
'dom' : "{dom:02}",
'doy' : "{doy:03}",
# depricated
'pep440_version': "{year}{month:02}.{BID}{pep440_tag}",
'version' : "v{year}{month:02}.{bid}{release}",
}
PART_FORMATS = {
'major' : "[0-9]+",
'minor' : "[0-9]{3,}",
'patch' : "[0-9]{3,}",
'bid' : "[0-9]{4,}",
'MAJOR' : "[0-9]+",
'MINOR' : "[0-9]+",
'MM' : "[0-9]{2,}",
'MMM' : "[0-9]{3,}",
'MMMM' : "[0-9]{4,}",
'MMMMM' : "[0-9]{5,}",
'MMMMMM' : "[0-9]{6,}",
'MMMMMMM': "[0-9]{7,}",
'PATCH' : "[0-9]+",
'PP' : "[0-9]{2,}",
'PPP' : "[0-9]{3,}",
'PPPP' : "[0-9]{4,}",
'PPPPP' : "[0-9]{5,}",
'PPPPPP' : "[0-9]{6,}",
'PPPPPPP': "[0-9]{7,}",
'BID' : "[1-9][0-9]*",
'BB' : "[1-9][0-9]{1,}",
'BBB' : "[1-9][0-9]{2,}",
'BBBB' : "[1-9][0-9]{3,}",
'BBBBB' : "[1-9][0-9]{4,}",
'BBBBBB' : "[1-9][0-9]{5,}",
'BBBBBBB': "[1-9][0-9]{6,}",
}
def _replace_pattern_parts(pattern: str) -> str:
for part_name, part_pattern in PART_PATTERNS.items():
named_part_pattern = f"(?P<{part_name}>{part_pattern})"
placeholder = "\u005c{" + part_name + "\u005c}"
pattern = pattern.replace(placeholder, named_part_pattern)
return pattern
def _compile_pattern(pattern: str) -> str:
for char, escaped in PATTERN_ESCAPES:
pattern = pattern.replace(char, escaped)
return _replace_pattern_parts(pattern)
def compile_pattern(pattern: str) -> typ.Pattern[str]:
pattern_str = _compile_pattern(pattern)
return re.compile(pattern_str)
def _init_composite_patterns() -> None:
for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items():
part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}")
pattern_str = _replace_pattern_parts(part_pattern)
PART_PATTERNS[part_name] = pattern_str
_init_composite_patterns()

View file

@ -43,18 +43,20 @@ def rewrite_lines(
) -> typ.List[str]: ) -> typ.List[str]:
"""Replace occurances of patterns in old_lines with new_version. """Replace occurances of patterns in old_lines with new_version.
>>> old_lines = ['__version__ = "v201809.0002-beta"'] >>> patterns = ['__version__ = "{pycalver}"']
>>> patterns = ['__version__ = "{version}"'] >>> rewrite_lines(patterns, "v201811.0123-beta", ['__version__ = "v201809.0002-beta"'])
>>> new_lines = rewrite_lines(patterns, "v201811.0123-beta", old_lines) ['__version__ = "v201811.0123-beta"']
>>> assert new_lines == ['__version__ = "v201811.0123-beta"']
>>> patterns = ['__version__ = "{pep440_version}"']
>>> rewrite_lines(patterns, "v201811.0123-beta", ['__version__ = "201809.2b0"'])
['__version__ = "201811.123b0"']
""" """
new_version_nfo = version.parse_version_info(new_version) new_version_nfo = version.parse_version_info(new_version)
new_version_fmt_kwargs = new_version_nfo._asdict()
new_lines = old_lines[:] new_lines = old_lines[:]
for m in parse.iter_matches(old_lines, patterns): for m in parse.iter_matches(old_lines, patterns):
replacement = m.pattern.format(**new_version_fmt_kwargs) replacement = version.format_version(new_version_nfo, m.pattern)
span_l, span_r = m.span span_l, span_r = m.span
new_line = m.line[:span_l] + replacement + m.line[span_r:] new_line = m.line[:span_l] + replacement + m.line[span_r:]
new_lines[m.lineno] = new_line new_lines[m.lineno] = new_line
@ -74,10 +76,11 @@ class RewrittenFileData(typ.NamedTuple):
def rfd_from_content(patterns: typ.List[str], new_version: str, content: str) -> RewrittenFileData: def rfd_from_content(patterns: typ.List[str], new_version: str, content: str) -> RewrittenFileData:
r"""Rewrite pattern occurrences with version string. r"""Rewrite pattern occurrences with version string.
>>> patterns = ['__version__ = "{version}"'] >>> patterns = ['__version__ = "{pycalver}"']
>>> content = '__version__ = "v201809.0001-alpha"' >>> content = '__version__ = "v201809.0001-alpha"'
>>> rfd = rfd_from_content(patterns, "v201809.0123", content) >>> rfd = rfd_from_content(patterns, "v201809.0123", content)
>>> assert rfd.new_lines == ['__version__ = "v201809.0123"'] >>> rfd.new_lines
['__version__ = "v201809.0123"']
""" """
line_sep = detect_line_sep(content) line_sep = detect_line_sep(content)
old_lines = content.split(line_sep) old_lines = content.split(line_sep)
@ -90,7 +93,7 @@ def iter_rewritten(
) -> typ.Iterable[RewrittenFileData]: ) -> typ.Iterable[RewrittenFileData]:
r'''Iterate over files with version string replaced. r'''Iterate over files with version string replaced.
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{version}"']} >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
>>> rewritten_datas = iter_rewritten(file_patterns, "v201809.0123") >>> rewritten_datas = iter_rewritten(file_patterns, "v201809.0123")
>>> rfd = list(rewritten_datas)[0] >>> rfd = list(rewritten_datas)[0]
>>> assert rfd.new_lines == [ >>> assert rfd.new_lines == [
@ -135,7 +138,7 @@ def diff_lines(rfd: RewrittenFileData) -> typ.List[str]:
def diff(new_version: str, file_patterns: config.PatternsByFilePath) -> str: def diff(new_version: str, file_patterns: config.PatternsByFilePath) -> str:
r"""Generate diffs of rewritten files. r"""Generate diffs of rewritten files.
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{version}"']} >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
>>> diff_str = diff("v201809.0123", file_patterns) >>> diff_str = diff("v201809.0123", file_patterns)
>>> lines = diff_str.split("\n") >>> lines = diff_str.split("\n")
>>> lines[:2] >>> lines[:2]

View file

@ -3,164 +3,470 @@
# #
# Copyright (c) 2018 Manuel Barkhau (@mbarkhau) - MIT License # Copyright (c) 2018 Manuel Barkhau (@mbarkhau) - MIT License
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
"""Functions related to version string manipulation. """Functions related to version string manipulation."""
>>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict()
>>> assert version_info == {
... "version" : "v201712.0123-alpha",
... "calver" : "v201712",
... "year" : "2017",
... "month" : "12",
... "build" : ".0123",
... "build_no" : "0123",
... "release" : "-alpha",
... "release_tag" : "alpha",
... }
>>>
>>> version_info = PYCALVER_RE.match("v201712.0033").groupdict()
>>> assert version_info == {
... "version" : "v201712.0033",
... "calver" : "v201712",
... "year" : "2017",
... "month" : "12",
... "build" : ".0033",
... "build_no" : "0033",
... "release" : None,
... "release_tag": None,
... }
"""
import re
import logging import logging
import pkg_resources import pkg_resources
import typing as typ import typing as typ
import datetime as dt import datetime as dt
from . import lex_id from . import lex_id
from . import patterns
log = logging.getLogger("pycalver.version") log = logging.getLogger("pycalver.version")
# https://regex101.com/r/fnj60p/10 # The test suite may replace this.
PYCALVER_PATTERN = r""" TODAY = dt.datetime.utcnow().date()
\b
(?P<version>
(?P<calver>
v # "v" version prefix
(?P<year>\d{4})
(?P<month>\d{2})
)
(?P<build>
\. # "." build nr prefix
(?P<build_no>\d{4,})
)
(?P<release>
\- # "-" release prefix
(?P<release_tag>alpha|beta|dev|rc|post)
)?
)(?:\s|$)
"""
PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE)
PATTERN_PART_FIELDS = {
'year' : 'year',
'month' : 'month',
'pep440_tag': 'tag',
'tag' : 'tag',
'yy' : 'year',
'yyyy' : 'year',
'quarter' : 'quarter',
'iso_week' : 'iso_week',
'us_week' : 'us_week',
'dom' : 'dom',
'doy' : 'doy',
'MAJOR' : 'major',
'MINOR' : 'minor',
'MM' : 'minor',
'MMM' : 'minor',
'MMMM' : 'minor',
'MMMMM' : 'minor',
'PP' : 'patch',
'PPP' : 'patch',
'PPPP' : 'patch',
'PPPPP' : 'patch',
'PATCH' : 'patch',
'build_no' : 'bid',
'bid' : 'bid',
'BID' : 'bid',
'BB' : 'bid',
'BBB' : 'bid',
'BBBB' : 'bid',
'BBBBB' : 'bid',
'BBBBBB' : 'bid',
'BBBBBBB' : 'bid',
}
class CalendarInfo(typ.NamedTuple):
"""Container for calendar components of version strings."""
year : int
quarter : int
month : int
dom : int
doy : int
iso_week: int
us_week : int
def _date_from_doy(year: int, doy: int) -> dt.date:
"""Parse date from year and day of year (1 indexed).
>>> cases = [
... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30),
... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29),
... ]
>>> dates = [_date_from_doy(year, month) for year, month in cases]
>>> assert [(d.month, d.day) for d in dates] == [
... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1),
... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1),
... ]
"""
return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1)
def _quarter_from_month(month: int) -> int:
"""Calculate quarter (1 indexed) from month (1 indexed).
>>> [_quarter_from_month(month) for month in range(1, 13)]
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]
"""
return ((month - 1) // 3) + 1
def cal_info(date: dt.date = None) -> CalendarInfo:
"""Generate calendar components for current date.
>>> from datetime import date
>>> c = cal_info(date(2019, 1, 5))
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
(2019, 1, 1, 5, 5, 0, 0)
>>> c = cal_info(date(2019, 1, 6))
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
(2019, 1, 1, 6, 6, 0, 1)
>>> c = cal_info(date(2019, 1, 7))
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
(2019, 1, 1, 7, 7, 1, 1)
>>> c = cal_info(date(2019, 4, 7))
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
(2019, 2, 4, 7, 97, 13, 14)
"""
if date is None:
date = TODAY
kw = {
'year' : date.year,
'quarter' : _quarter_from_month(date.month),
'month' : date.month,
'dom' : date.day,
'doy' : int(date.strftime("%j"), base=10),
'iso_week': int(date.strftime("%W"), base=10),
'us_week' : int(date.strftime("%U"), base=10),
}
return CalendarInfo(**kw)
class VersionInfo(typ.NamedTuple): class VersionInfo(typ.NamedTuple):
"""Container for parsed version string.""" """Container for parsed version string."""
version : str year : typ.Optional[int]
pep440_version: str quarter : typ.Optional[int]
calver : str month : typ.Optional[int]
year : str dom : typ.Optional[int]
month : str doy : typ.Optional[int]
build : str iso_week: typ.Optional[int]
build_no : str us_week : typ.Optional[int]
release : typ.Optional[str] major : int
release_tag : typ.Optional[str] minor : int
patch : int
bid : str
tag : str
def parse_version_info(version_str: str) -> VersionInfo: def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool:
"""Parse a PyCalVer string. """Check pattern for any calendar based parts.
>>> vnfo = parse_version_info("v201712.0033-beta") >>> _is_calver(cal_info())
>>> assert vnfo == VersionInfo( True
... version ="v201712.0033-beta",
... pep440_version="201712.33b0", >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"})
... calver ="v201712", >>> _is_calver(vnfo)
... year ="2017", True
... month ="12",
... build =".0033", >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"})
... build_no ="0033", >>> _is_calver(vnfo)
... release ="-beta", False
... release_tag ="beta",
... )
""" """
match = PYCALVER_RE.match(version_str) for field in CalendarInfo._fields:
if match is None: if isinstance(getattr(nfo, field, None), int):
raise ValueError(f"Invalid PyCalVer string: {version_str}") return True
kwargs = match.groupdict() return False
kwargs['pep440_version'] = pycalver_to_pep440(kwargs['version'])
if kwargs['release'] is None:
kwargs['release'] = "-final"
if kwargs['release_tag'] is None:
kwargs['release_tag'] = "final"
return VersionInfo(**kwargs)
def current_calver() -> str: TAG_ALIASES: typ.Dict[str, str] = {'a': "alpha", 'b': "beta", 'pre': "rc"}
"""Generate calver version string based on current date.
example result: "v201812"
"""
return dt.date.today().strftime("v%Y%m")
def incr(old_version: str, *, release: str = None) -> str: PEP440_TAGS: typ.Dict[str, str] = {'alpha': "a", 'beta': "b", 'final': "", 'rc': "rc", 'dev': "dev"}
"""Increment a full PyCalVer version string.
Old_version is assumed to be a valid calver string,
already validated in pycalver.config.parse.
"""
old_ver = parse_version_info(old_version)
new_calver = current_calver() VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
if old_ver.calver > new_calver:
log.warning( def _parse_pattern_groups(pattern_groups: typ.Dict[str, str]) -> typ.Dict[str, str]:
f"'version.incr' called with '{old_version}', " for part_name in pattern_groups.keys():
+ f"which is from the future, " is_valid_part_name = (
+ f"maybe your system clock is out of sync." part_name in patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS
) )
# leave calver as is (don't go back in time) if not is_valid_part_name:
new_calver = old_ver.calver err_msg = f"Invalid part '{part_name}'"
raise ValueError(err_msg)
new_build = lex_id.next_id(old_ver.build[1:]) items = [
new_release: typ.Optional[str] = None (field_name, pattern_groups[part_name])
for part_name, field_name in PATTERN_PART_FIELDS.items()
if part_name in pattern_groups.keys()
]
all_fields = [field_name for field_name, _ in items]
unique_fields = set(all_fields)
duplicate_fields = [f for f in unique_fields if all_fields.count(f) > 1]
if release is None: if any(duplicate_fields):
if old_ver.release: err_msg = f"Multiple parts for same field {duplicate_fields}."
# preserve existing release raise ValueError(err_msg)
new_release = old_ver.release[1:]
else: return dict(items)
new_release = None
elif release == 'final':
new_release = None def _parse_version_info(pattern_groups: typ.Dict[str, str]) -> VersionInfo:
"""Parse normalized VersionInfo from groups of a matched pattern.
>>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"})
>>> (vnfo.year, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag)
(2018, 11, 4, '0099', 'final')
>>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"})
>>> (vnfo.year, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag)
(2018, 1, 11, '099', 'beta')
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"})
>>> (vnfo.major, vnfo.minor, vnfo.patch)
(1, 23, 45)
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"})
>>> (vnfo.major, vnfo.minor, vnfo.patch)
(1, 23, 45)
"""
kw = _parse_pattern_groups(pattern_groups)
tag = kw.get('tag')
if tag is None:
tag = "final"
tag = TAG_ALIASES.get(tag, tag)
assert tag is not None
bid = kw['bid'] if 'bid' in kw else "0001"
year = int(kw['year']) if 'year' in kw else None
doy = int(kw['doy' ]) if 'doy' in kw else None
month: typ.Optional[int]
dom : typ.Optional[int]
if year and doy:
date = _date_from_doy(year, doy)
month = date.month
dom = date.day
else: else:
new_release = release month = int(kw['month']) if 'month' in kw else None
dom = int(kw['dom' ]) if 'dom' in kw else None
if new_release == 'final': iso_week: typ.Optional[int]
new_release = None us_week : typ.Optional[int]
new_version = new_calver + "." + new_build if year and month and dom:
if new_release: date = dt.date(year, month, dom)
new_version += "-" + new_release doy = int(date.strftime("%j"), base=10)
return new_version iso_week = int(date.strftime("%W"), base=10)
us_week = int(date.strftime("%U"), base=10)
else:
iso_week = None
us_week = None
quarter = int(kw['quarter']) if 'quarter' in kw else None
if quarter is None and month:
quarter = _quarter_from_month(month)
major = int(kw['major']) if 'major' in kw else 0
minor = int(kw['minor']) if 'minor' in kw else 0
patch = int(kw['patch']) if 'patch' in kw else 0
return VersionInfo(
year=year,
quarter=quarter,
month=month,
dom=dom,
doy=doy,
iso_week=iso_week,
us_week=us_week,
major=major,
minor=minor,
patch=patch,
bid=bid,
tag=tag,
)
def pycalver_to_pep440(version: str) -> str: def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> VersionInfo:
"""Parse normalized VersionInfo.
>>> vnfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}")
>>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"})
>>> vnfo = parse_version_info("1.23.456", pattern="{semver}")
>>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"})
"""
regex = patterns.compile_pattern(pattern)
match = regex.match(version_str)
if match is None:
err_msg = (
f"Invalid version string '{version_str}' for pattern '{pattern}'/'{regex.pattern}'"
)
raise ValueError(err_msg)
return _parse_version_info(match.groupdict())
def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool:
"""Check if a version matches a pattern.
>>> is_valid("v201712.0033-beta", pattern="{pycalver}")
True
>>> is_valid("v201712.0033-beta", pattern="{semver}")
False
>>> is_valid("1.2.3", pattern="{semver}")
True
>>> is_valid("v201712.0033-beta", pattern="{semver}")
False
"""
try:
parse_version_info(version_str, pattern)
return True
except ValueError:
return False
ID_FIELDS_BY_PART = {
'MAJOR' : 'major',
'MINOR' : 'minor',
'MM' : 'minor',
'MMM' : 'minor',
'MMMM' : 'minor',
'MMMMM' : 'minor',
'MMMMMM' : 'minor',
'MMMMMMM': 'minor',
'PATCH' : 'patch',
'PP' : 'patch',
'PPP' : 'patch',
'PPPP' : 'patch',
'PPPPP' : 'patch',
'PPPPPP' : 'patch',
'PPPPPPP': 'patch',
'BID' : 'bid',
'BB' : 'bid',
'BBB' : 'bid',
'BBBB' : 'bid',
'BBBBB' : 'bid',
'BBBBBB' : 'bid',
'BBBBBBB': 'bid',
}
def format_version(ver_nfo: VersionInfo, pattern: str) -> str:
"""Generate version string.
>>> import datetime as dt
>>> ver_nfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}")
>>> ver_nfo_a = ver_nfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict())
>>> ver_nfo_b = ver_nfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict())
>>> ver_nfo_c = ver_nfo_b._replace(major=1, minor=2, patch=34, tag='final')
>>> format_version(ver_nfo_a, pattern="v{yy}.{BID}{release}")
'v17.33-beta'
>>> format_version(ver_nfo_a, pattern="{pep440_version}")
'201701.33b0'
>>> format_version(ver_nfo_a, pattern="{pycalver}")
'v201701.0033-beta'
>>> format_version(ver_nfo_b, pattern="{pycalver}")
'v201712.0033-beta'
>>> format_version(ver_nfo_a, pattern="v{year}w{iso_week}.{BID}{release}")
'v2017w00.33-beta'
>>> format_version(ver_nfo_b, pattern="v{year}w{iso_week}.{BID}{release}")
'v2017w52.33-beta'
>>> format_version(ver_nfo_a, pattern="v{year}d{doy}.{bid}{release}")
'v2017d001.0033-beta'
>>> format_version(ver_nfo_b, pattern="v{year}d{doy}.{bid}{release}")
'v2017d365.0033-beta'
>>> format_version(ver_nfo_c, pattern="v{year}w{iso_week}.{BID}-{tag}")
'v2017w52.33-final'
>>> format_version(ver_nfo_c, pattern="v{year}w{iso_week}.{BID}{release}")
'v2017w52.33'
>>> format_version(ver_nfo_c, pattern="v{MAJOR}.{MINOR}.{PATCH}")
'v1.2.34'
>>> format_version(ver_nfo_c, pattern="v{MAJOR}.{MM}.{PPP}")
'v1.02.034'
"""
full_pattern = pattern
for part_name, full_part_format in patterns.FULL_PART_FORMATS.items():
full_pattern = full_pattern.replace("{" + part_name + "}", full_part_format)
kw = ver_nfo._asdict()
if kw['tag'] == 'final':
kw['release' ] = ""
kw['pep440_tag'] = ""
else:
kw['release' ] = "-" + kw['tag']
kw['pep440_tag'] = PEP440_TAGS[kw['tag']] + "0"
kw['release_tag'] = kw['tag']
kw['yy' ] = str(kw['year'])[-2:]
kw['yyyy'] = kw['year']
kw['BID' ] = int(kw['bid'], 10)
for part_name, field in ID_FIELDS_BY_PART.items():
val = kw[field]
if part_name.lower() == field.lower():
if isinstance(val, str):
kw[part_name] = int(val, base=10)
else:
kw[part_name] = val
else:
assert len(set(part_name)) == 1
padded_len = len(part_name)
kw[part_name] = str(val).zfill(padded_len)
return full_pattern.format(**kw)
def incr(
old_version: str,
pattern : str = "{pycalver}",
*,
release: str = None,
major : bool = False,
minor : bool = False,
patch : bool = False,
) -> typ.Optional[str]:
"""Increment version string.
'old_version' is assumed to be a string that matches 'pattern'
"""
old_ver_nfo = parse_version_info(old_version, pattern)
cur_ver_nfo = old_ver_nfo
cur_cal_nfo = cal_info()
old_date = (old_ver_nfo.year or 0, old_ver_nfo.month or 0, old_ver_nfo.dom or 0)
cur_date = (cur_cal_nfo.year , cur_cal_nfo.month , cur_cal_nfo.dom)
if old_date <= cur_date:
cur_ver_nfo = cur_ver_nfo._replace(**cur_cal_nfo._asdict())
else:
log.warning(f"Version appears to be from the future '{old_version}'")
cur_ver_nfo = cur_ver_nfo._replace(bid=lex_id.next_id(cur_ver_nfo.bid))
if major:
cur_ver_nfo = cur_ver_nfo._replace(major=cur_ver_nfo.major + 1, minor=0, patch=0)
if minor:
cur_ver_nfo = cur_ver_nfo._replace(minor=cur_ver_nfo.minor + 1, patch=0)
if patch:
cur_ver_nfo = cur_ver_nfo._replace(patch=cur_ver_nfo.patch + 1)
if release:
cur_ver_nfo = cur_ver_nfo._replace(tag=release)
new_version = format_version(cur_ver_nfo, pattern)
if new_version == old_version:
log.error("Invalid arguments or pattern, version did not change.")
return None
else:
return new_version
def to_pep440(version: str) -> str:
"""Derive pep440 compliant version string from PyCalVer version string. """Derive pep440 compliant version string from PyCalVer version string.
>>> pycalver_to_pep440("v201811.0007-beta") >>> to_pep440("v201811.0007-beta")
'201811.7b0' '201811.7b0'
""" """
return str(pkg_resources.parse_version(version)) return str(pkg_resources.parse_version(version))

View file

@ -10,7 +10,7 @@ import pytest
from click.testing import CliRunner from click.testing import CliRunner
import pycalver.config as config import pycalver.config as config
import pycalver.version as version import pycalver.patterns as patterns
import pycalver.__main__ as pycalver import pycalver.__main__ as pycalver
@ -80,18 +80,52 @@ def test_version(runner):
result = runner.invoke(pycalver.cli, ['--version', "--verbose"]) result = runner.invoke(pycalver.cli, ['--version', "--verbose"])
assert result.exit_code == 0 assert result.exit_code == 0
assert " version v20" in result.output assert " version v20" in result.output
match = version.PYCALVER_RE.search(result.output) match = patterns.PYCALVER_RE.search(result.output)
assert match assert match
def test_incr(runner): def test_incr_default(runner):
old_version = "v201701.0999-alpha" old_version = "v201701.0999-alpha"
initial_version = config._initial_version() initial_version = config._initial_version()
result = runner.invoke(pycalver.cli, ['test', old_version, "--verbose"]) result = runner.invoke(pycalver.cli, ['test', "--verbose", old_version])
assert result.exit_code == 0 assert result.exit_code == 0
new_version = initial_version.replace(".0001-alpha", ".11000-alpha") new_version = initial_version.replace(".0001-alpha", ".11000-alpha")
assert f"PyCalVer Version: {new_version}\n" in result.output assert f"Version: {new_version}\n" in result.output
def test_incr_semver(runner):
semver_pattern = "{MAJOR}.{MINOR}.{PATCH}"
old_version = "0.1.0"
new_version = "0.1.1"
result = runner.invoke(pycalver.cli, ['test', "--verbose", "--patch", old_version, "{semver}"])
assert result.exit_code == 0
assert f"Version: {new_version}\n" in result.output
result = runner.invoke(
pycalver.cli, ['test', "--verbose", "--patch", old_version, semver_pattern]
)
assert result.exit_code == 0
assert f"Version: {new_version}\n" in result.output
old_version = "0.1.1"
new_version = "0.2.0"
result = runner.invoke(
pycalver.cli, ['test', "--verbose", "--minor", old_version, semver_pattern]
)
assert result.exit_code == 0
assert f"Version: {new_version}\n" in result.output
old_version = "0.1.1"
new_version = "1.0.0"
result = runner.invoke(
pycalver.cli, ['test', "--verbose", "--major", old_version, semver_pattern]
)
assert result.exit_code == 0
assert f"Version: {new_version}\n" in result.output
def test_incr_to_beta(runner): def test_incr_to_beta(runner):
@ -101,7 +135,7 @@ def test_incr_to_beta(runner):
result = runner.invoke(pycalver.cli, ['test', old_version, "--verbose", "--release", "beta"]) result = runner.invoke(pycalver.cli, ['test', old_version, "--verbose", "--release", "beta"])
assert result.exit_code == 0 assert result.exit_code == 0
new_version = initial_version.replace(".0001-alpha", ".11000-beta") new_version = initial_version.replace(".0001-alpha", ".11000-beta")
assert f"PyCalVer Version: {new_version}\n" in result.output assert f"Version: {new_version}\n" in result.output
def test_incr_to_final(runner): def test_incr_to_final(runner):
@ -111,7 +145,7 @@ def test_incr_to_final(runner):
result = runner.invoke(pycalver.cli, ['test', old_version, "--verbose", "--release", "final"]) result = runner.invoke(pycalver.cli, ['test', old_version, "--verbose", "--release", "final"])
assert result.exit_code == 0 assert result.exit_code == 0
new_version = initial_version.replace(".0001-alpha", ".11000") new_version = initial_version.replace(".0001-alpha", ".11000")
assert f"PyCalVer Version: {new_version}\n" in result.output assert f"Version: {new_version}\n" in result.output
def test_incr_invalid(runner, caplog): def test_incr_invalid(runner, caplog):
@ -164,7 +198,7 @@ def test_novcs_nocfg_init(runner):
result = runner.invoke(pycalver.cli, ['show', "--verbose"]) result = runner.invoke(pycalver.cli, ['show', "--verbose"])
assert result.exit_code == 0 assert result.exit_code == 0
assert f"Current Version: {config._initial_version()}\n" in result.output assert f"Current Version: {config._initial_version()}\n" in result.output
assert f"PEP440 Version : {config._initial_version_pep440()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output
def test_novcs_setupcfg_init(runner): def test_novcs_setupcfg_init(runner):
@ -184,7 +218,7 @@ def test_novcs_setupcfg_init(runner):
result = runner.invoke(pycalver.cli, ['show', "--verbose"]) result = runner.invoke(pycalver.cli, ['show', "--verbose"])
assert result.exit_code == 0 assert result.exit_code == 0
assert f"Current Version: {config._initial_version()}\n" in result.output assert f"Current Version: {config._initial_version()}\n" in result.output
assert f"PEP440 Version : {config._initial_version_pep440()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output
def test_novcs_pyproject_init(runner): def test_novcs_pyproject_init(runner):
@ -202,7 +236,7 @@ def test_novcs_pyproject_init(runner):
result = runner.invoke(pycalver.cli, ['show']) result = runner.invoke(pycalver.cli, ['show'])
assert result.exit_code == 0 assert result.exit_code == 0
assert f"Current Version: {config._initial_version()}\n" in result.output assert f"Current Version: {config._initial_version()}\n" in result.output
assert f"PEP440 Version : {config._initial_version_pep440()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output
def _vcs_init(vcs): def _vcs_init(vcs):
@ -224,7 +258,7 @@ def test_git_init(runner):
result = runner.invoke(pycalver.cli, ['show']) result = runner.invoke(pycalver.cli, ['show'])
assert result.exit_code == 0 assert result.exit_code == 0
assert f"Current Version: {config._initial_version()}\n" in result.output assert f"Current Version: {config._initial_version()}\n" in result.output
assert f"PEP440 Version : {config._initial_version_pep440()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output
def test_hg_init(runner): def test_hg_init(runner):
@ -237,7 +271,7 @@ def test_hg_init(runner):
result = runner.invoke(pycalver.cli, ['show']) result = runner.invoke(pycalver.cli, ['show'])
assert result.exit_code == 0 assert result.exit_code == 0
assert f"Current Version: {config._initial_version()}\n" in result.output assert f"Current Version: {config._initial_version()}\n" in result.output
assert f"PEP440 Version : {config._initial_version_pep440()}\n" in result.output assert f"PEP440 : {config._initial_version_pep440()}\n" in result.output
def test_git_tag_eval(runner): def test_git_tag_eval(runner):
@ -257,7 +291,7 @@ def test_git_tag_eval(runner):
result = runner.invoke(pycalver.cli, ['show', "--verbose"]) result = runner.invoke(pycalver.cli, ['show', "--verbose"])
assert result.exit_code == 0 assert result.exit_code == 0
assert f"Current Version: {tag_version}\n" in result.output assert f"Current Version: {tag_version}\n" in result.output
assert f"PEP440 Version : {tag_version_pep440}\n" in result.output assert f"PEP440 : {tag_version_pep440}\n" in result.output
def test_hg_tag_eval(runner): def test_hg_tag_eval(runner):
@ -277,7 +311,7 @@ def test_hg_tag_eval(runner):
result = runner.invoke(pycalver.cli, ['show', "--verbose"]) result = runner.invoke(pycalver.cli, ['show', "--verbose"])
assert result.exit_code == 0 assert result.exit_code == 0
assert f"Current Version: {tag_version}\n" in result.output assert f"Current Version: {tag_version}\n" in result.output
assert f"PEP440 Version : {tag_version_pep440}\n" in result.output assert f"PEP440 : {tag_version_pep440}\n" in result.output
def test_novcs_bump(runner): def test_novcs_bump(runner):

View file

@ -58,8 +58,8 @@ def test_parse_toml():
assert cfg.push is True assert cfg.push is True
assert "pycalver.toml" in cfg.file_patterns assert "pycalver.toml" in cfg.file_patterns
assert cfg.file_patterns["README.md" ] == ["{version}", "{pep440_version}"] assert cfg.file_patterns["README.md" ] == ["{pycalver}", "{pep440_pycalver}"]
assert cfg.file_patterns["pycalver.toml"] == ['current_version = "{version}"'] assert cfg.file_patterns["pycalver.toml"] == ['current_version = "{pycalver}"']
def test_parse_cfg(): def test_parse_cfg():
@ -74,8 +74,8 @@ def test_parse_cfg():
assert cfg.push is True assert cfg.push is True
assert "setup.cfg" in cfg.file_patterns assert "setup.cfg" in cfg.file_patterns
assert cfg.file_patterns["setup.py" ] == ["{version}", "{pep440_version}"] assert cfg.file_patterns["setup.py" ] == ["{pycalver}", "{pep440_pycalver}"]
assert cfg.file_patterns["setup.cfg"] == ['current_version = "{version}"'] assert cfg.file_patterns["setup.cfg"] == ['current_version = "{pycalver}"']
def test_parse_default_toml(): def test_parse_default_toml():
@ -168,8 +168,8 @@ def test_parse_toml_file(tmpdir):
assert cfg.push is True assert cfg.push is True
assert cfg.file_patterns == { assert cfg.file_patterns == {
"README.md" : ["{version}", "{pep440_version}"], "README.md" : ["{pycalver}", "{pep440_pycalver}"],
"pycalver.toml": ['current_version = "{version}"'], "pycalver.toml": ['current_version = "{pycalver}"'],
} }
@ -190,8 +190,8 @@ def test_parse_cfg_file(tmpdir):
assert cfg.push is True assert cfg.push is True
assert cfg.file_patterns == { assert cfg.file_patterns == {
"setup.py" : ["{version}", "{pep440_version}"], "setup.py" : ["{pycalver}", "{pep440_pycalver}"],
"setup.cfg": ['current_version = "{version}"'], "setup.cfg": ['current_version = "{pycalver}"'],
} }

View file

@ -1,50 +1,6 @@
import re
from pycalver import parse from pycalver import parse
def test_re_pattern_parts():
part_re_by_name = {
part_name: re.compile(part_re_str)
for part_name, part_re_str in parse.RE_PATTERN_PARTS.items()
}
cases = [
("pep440_version", "201712.31" , "201712.31"),
("pep440_version", "v201712.0032" , None),
("pep440_version", "201712.0033-alpha" , None),
("version" , "v201712.0034" , "v201712.0034"),
("version" , "v201712.0035-alpha" , "v201712.0035-alpha"),
("version" , "v201712.0036-alpha0", "v201712.0036-alpha"),
("version" , "v201712.0037-pre" , "v201712.0037"),
("version" , "201712.38a0" , None),
("version" , "201712.0039" , None),
("calver" , "v201712" , "v201712"),
("calver" , "v201799" , "v201799"), # maybe date validation should be a thing
("calver" , "201712" , None),
("calver" , "v20171" , None),
("build" , ".0012" , ".0012"),
("build" , ".11012" , ".11012"),
("build" , ".012" , None),
("build" , "11012" , None),
("release" , "-alpha" , "-alpha"),
("release" , "-beta" , "-beta"),
("release" , "-dev" , "-dev"),
("release" , "-rc" , "-rc"),
("release" , "-post" , "-post"),
("release" , "-pre" , ""),
("release" , "alpha" , ""),
]
for part_name, line, expected in cases:
part_re = part_re_by_name[part_name]
result = part_re.search(line)
if result is None:
assert expected is None, (part_name, line)
else:
result_val = result.group(0)
assert result_val == expected, (part_name, line)
SETUP_PY_FIXTURE = """ SETUP_PY_FIXTURE = """
# setup.py # setup.py
import setuptools import setuptools
@ -57,7 +13,7 @@ setuptools.setup(
def test_default_parse_patterns(): def test_default_parse_patterns():
lines = SETUP_PY_FIXTURE.splitlines() lines = SETUP_PY_FIXTURE.splitlines()
patterns = ["{version}", "{pep440_version}"] patterns = ["{pycalver}", "{pep440_pycalver}"]
matches = list(parse.iter_matches(lines, patterns)) matches = list(parse.iter_matches(lines, patterns))
assert len(matches) == 2 assert len(matches) == 2
@ -75,7 +31,7 @@ def test_default_parse_patterns():
def test_explicit_parse_patterns(): def test_explicit_parse_patterns():
lines = SETUP_PY_FIXTURE.splitlines() lines = SETUP_PY_FIXTURE.splitlines()
patterns = ["__version__ = '{version}'", "version='{pep440_version}'"] patterns = ["__version__ = '{pycalver}'", "version='{pep440_pycalver}'"]
matches = list(parse.iter_matches(lines, patterns)) matches = list(parse.iter_matches(lines, patterns))
assert len(matches) == 2 assert len(matches) == 2
@ -102,7 +58,7 @@ README_RST_FIXTURE = """
def test_badge_parse_patterns(): def test_badge_parse_patterns():
lines = README_RST_FIXTURE.splitlines() lines = README_RST_FIXTURE.splitlines()
patterns = ["badge/CalVer-{calver}{build}-{release}-blue.svg", ":alt: CalVer {version}"] patterns = ["badge/CalVer-{calver}{build}-{release}-blue.svg", ":alt: CalVer {pycalver}"]
matches = list(parse.iter_matches(lines, patterns)) matches = list(parse.iter_matches(lines, patterns))
assert len(matches) == 2 assert len(matches) == 2
@ -115,30 +71,3 @@ def test_badge_parse_patterns():
assert matches[0].match == "badge/CalVer-v201809.0002--beta-blue.svg" assert matches[0].match == "badge/CalVer-v201809.0002--beta-blue.svg"
assert matches[1].match == ":alt: CalVer v201809.0002-beta" assert matches[1].match == ":alt: CalVer v201809.0002-beta"
CLI_MAIN_FIXTURE = """
@click.group()
@click.version_option(version="v201812.0123-beta")
@click.help_option()
"""
def test_pattern_escapes():
pattern_re = parse.compile_pattern(r'click.version_option(version="{version}")')
match = pattern_re.search(CLI_MAIN_FIXTURE)
assert match.group(0) == 'click.version_option(version="v201812.0123-beta")'
CURLY_BRACE_FIXTURE = """
package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"}
"""
def test_curly_escapes():
pattern = r'package_metadata = {"name": "mypackage", "version": "{version}"}'
pattern_re = parse.compile_pattern(pattern)
match = pattern_re.search(CURLY_BRACE_FIXTURE)
assert (
match.group(0) == 'package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"}'
)

81
test/test_patterns.py Normal file
View file

@ -0,0 +1,81 @@
import re
import pytest
from pycalver import patterns
def _part_re_by_name(name):
return re.compile(patterns.PART_PATTERNS[name])
@pytest.mark.parametrize("part_name", patterns.PART_PATTERNS.keys())
def test_part_compilation(part_name):
assert _part_re_by_name(part_name)
PATTERN_PART_CASES = [
("pep440_pycalver", "201712.31" , "201712.31"),
("pep440_pycalver", "v201712.0032" , None),
("pep440_pycalver", "201712.0033-alpha" , None),
("pycalver" , "v201712.0034" , "v201712.0034"),
("pycalver" , "v201712.0035-alpha" , "v201712.0035-alpha"),
("pycalver" , "v201712.0036-alpha0", "v201712.0036-alpha"),
("pycalver" , "v201712.0037-pre" , "v201712.0037"),
("pycalver" , "201712.38a0" , None),
("pycalver" , "201712.0039" , None),
("semver" , "1.23.456" , "1.23.456"),
("calver" , "v201712" , "v201712"),
("calver" , "v201799" , None), # invalid date
("calver" , "201712" , None),
("calver" , "v20171" , None),
("build" , ".0012" , ".0012"),
("build" , ".11012" , ".11012"),
("build" , ".012" , None),
("build" , "11012" , None),
("release" , "-alpha" , "-alpha"),
("release" , "-beta" , "-beta"),
("release" , "-dev" , "-dev"),
("release" , "-rc" , "-rc"),
("release" , "-post" , "-post"),
("release" , "-pre" , None),
("release" , "alpha" , None),
]
@pytest.mark.parametrize("part_name, line, expected", PATTERN_PART_CASES)
def test_re_pattern_parts(part_name, line, expected):
part_re = _part_re_by_name(part_name)
result = part_re.search(line)
if result is None:
assert expected is None, (part_name, line)
else:
result_val = result.group(0)
assert result_val == expected, (part_name, line)
CLI_MAIN_FIXTURE = """
@click.group()
@click.version_option(version="v201812.0123-beta")
@click.help_option()
"""
def test_pattern_escapes():
pattern = 'click.version_option(version="{pycalver}")'
pattern_re = patterns.compile_pattern(pattern)
match = pattern_re.search(CLI_MAIN_FIXTURE)
expected = 'click.version_option(version="v201812.0123-beta")'
assert match.group(0) == expected
CURLY_BRACE_FIXTURE = """
package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"}
"""
def test_curly_escapes():
pattern = 'package_metadata = {"name": "mypackage", "version": "{pycalver}"}'
pattern_re = patterns.compile_pattern(pattern)
match = pattern_re.search(CURLY_BRACE_FIXTURE)
expected = 'package_metadata = {"name": "mypackage", "version": "v201812.0123-beta"}'
assert match.group(0) == expected

View file

@ -9,7 +9,7 @@ __version__ = "v201809.0002-beta"
def test_rewrite_lines(): def test_rewrite_lines():
old_lines = REWRITE_FIXTURE.splitlines() old_lines = REWRITE_FIXTURE.splitlines()
patterns = ['__version__ = "{version}"'] patterns = ['__version__ = "{pycalver}"']
new_lines = rewrite.rewrite_lines(patterns, "v201911.0003", old_lines) new_lines = rewrite.rewrite_lines(patterns, "v201911.0003", old_lines)
assert len(new_lines) == len(old_lines) assert len(new_lines) == len(old_lines)

View file

@ -2,18 +2,11 @@ import random
import datetime as dt import datetime as dt
from pycalver import version from pycalver import version
from pycalver import patterns
def test_current_calver():
v = version.current_calver()
assert len(v) == 7
assert v.startswith("v")
assert v[1:].isdigit()
def test_bump_beta(): def test_bump_beta():
calver = version.current_calver() cur_version = "v201712.0001-beta"
cur_version = calver + ".0001-beta"
assert cur_version < version.incr(cur_version) assert cur_version < version.incr(cur_version)
assert version.incr(cur_version).endswith("-beta") assert version.incr(cur_version).endswith("-beta")
assert version.incr(cur_version, release="alpha").endswith("-alpha") assert version.incr(cur_version, release="alpha").endswith("-alpha")
@ -21,8 +14,7 @@ def test_bump_beta():
def test_bump_final(): def test_bump_final():
calver = version.current_calver() cur_version = "v201712.0001"
cur_version = calver + ".0001"
assert cur_version < version.incr(cur_version) assert cur_version < version.incr(cur_version)
assert version.incr(cur_version).endswith(".0002") assert version.incr(cur_version).endswith(".0002")
assert version.incr(cur_version, release="alpha").endswith("-alpha") assert version.incr(cur_version, release="alpha").endswith("-alpha")
@ -34,20 +26,19 @@ def test_bump_final():
def test_bump_future(): def test_bump_future():
"""Test that versions don't go back in time."""
future_date = dt.datetime.today() + dt.timedelta(days=300) future_date = dt.datetime.today() + dt.timedelta(days=300)
future_calver = future_date.strftime("v%Y%m") future_calver = future_date.strftime("v%Y%m")
cur_version = future_calver + ".0001" cur_version = future_calver + ".0001"
assert cur_version < version.incr(cur_version) new_version = version.incr(cur_version)
assert cur_version < new_version
def test_bump_random(monkeypatch): def test_bump_random(monkeypatch):
cur_date = dt.date.today() cur_date = dt.date(2016, 1, 1) + dt.timedelta(days=random.randint(1, 2000))
cur_version = cur_date.strftime("v%Y%m") + ".0001-dev" cur_version = cur_date.strftime("v%Y%m") + ".0001-dev"
def _mock_current_calver(): monkeypatch.setattr(version, 'TODAY', cur_date)
return cur_date.strftime("v%Y%m")
monkeypatch.setattr(version, 'current_calver', _mock_current_calver)
for i in range(1000): for i in range(1000):
cur_date += dt.timedelta(days=int((1 + random.random()) ** 10)) cur_date += dt.timedelta(days=int((1 + random.random()) ** 10))
@ -62,37 +53,31 @@ def test_parse_version_info():
version_str = "v201712.0001-alpha" version_str = "v201712.0001-alpha"
version_nfo = version.parse_version_info(version_str) version_nfo = version.parse_version_info(version_str)
assert version_nfo.pep440_version == "201712.1a0" # assert version_nfo.pep440_version == "201712.1a0"
assert version_nfo.version == "v201712.0001-alpha" # assert version_nfo.version == "v201712.0001-alpha"
assert version_nfo.calver == "v201712" assert version_nfo.year == 2017
assert version_nfo.year == "2017" assert version_nfo.month == 12
assert version_nfo.month == "12" assert version_nfo.bid == "0001"
assert version_nfo.build == ".0001" assert version_nfo.tag == "alpha"
assert version_nfo.release == "-alpha"
assert version_nfo.build_no == "0001"
assert version_nfo.release_tag == "alpha"
version_str = "v201712.0001" version_str = "v201712.0001"
version_nfo = version.parse_version_info(version_str) version_nfo = version.parse_version_info(version_str)
assert version_nfo.pep440_version == "201712.1" # assert version_nfo.pep440_version == "201712.1"
assert version_nfo.version == "v201712.0001" # assert version_nfo.version == "v201712.0001"
assert version_nfo.calver == "v201712" assert version_nfo.year == 2017
assert version_nfo.year == "2017" assert version_nfo.month == 12
assert version_nfo.month == "12" assert version_nfo.bid == "0001"
assert version_nfo.build == ".0001" assert version_nfo.tag == "final"
assert version_nfo.release == "-final"
assert version_nfo.build_no == "0001"
assert version_nfo.release_tag == "final"
def test_readme_pycalver1(): def test_readme_pycalver1():
version_str = "v201712.0001-alpha" version_str = "v201712.0001-alpha"
version_info = version.PYCALVER_RE.match(version_str).groupdict() version_info = patterns.PYCALVER_RE.match(version_str).groupdict()
assert version_info == { assert version_info == {
'version' : "v201712.0001-alpha", 'pycalver' : "v201712.0001-alpha",
'calver' : "v201712", 'vYYYYMM' : "v201712",
'year' : "2017", 'year' : "2017",
'month' : "12", 'month' : "12",
'build' : ".0001", 'build' : ".0001",
@ -104,11 +89,11 @@ def test_readme_pycalver1():
def test_readme_pycalver2(): def test_readme_pycalver2():
version_str = "v201712.0033" version_str = "v201712.0033"
version_info = version.PYCALVER_RE.match(version_str).groupdict() version_info = patterns.PYCALVER_RE.match(version_str).groupdict()
assert version_info == { assert version_info == {
'version' : "v201712.0033", 'pycalver' : "v201712.0033",
'calver' : "v201712", 'vYYYYMM' : "v201712",
'year' : "2017", 'year' : "2017",
'month' : "12", 'month' : "12",
'build' : ".0033", 'build' : ".0033",
@ -140,3 +125,19 @@ def test_parse_error_nopadding():
assert False assert False
except ValueError as err: except ValueError as err:
pass pass
def test_part_field_mapping():
a_names = set(version.PATTERN_PART_FIELDS.keys())
b_names = set(patterns.PART_PATTERNS.keys())
c_names = set(patterns.COMPOSITE_PART_PATTERNS.keys())
extra_names = a_names - b_names
assert not any(extra_names)
missing_names = b_names - a_names
assert missing_names == c_names
a_fields = set(version.PATTERN_PART_FIELDS.values())
b_fields = set(version.VersionInfo._fields)
assert a_fields == b_fields