wip: add v2 module placeholders

This commit is contained in:
Manuel Barkhau 2020-09-06 20:20:36 +00:00
parent 7962a7cb9f
commit 669e8944e9
27 changed files with 1361 additions and 393 deletions

View file

@ -0,0 +1,8 @@
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""PyCalVer: CalVer for Python Packages."""
__version__ = "v202007.1036"

53
src/pycalver2/cli.py Normal file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env python
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""
CLI module for PyCalVer.
Provided subcommands: show, test, init, bump
"""
import typing as typ
import logging
import pycalver2.rewrite as v2rewrite
import pycalver2.version as v2version
from pycalver import config
logger = logging.getLogger("pycalver2.cli")
def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config:
version_tags = [tag for tag in all_tags if v2version.is_valid(tag, cfg.version_pattern)]
if not version_tags:
logger.debug("no vcs tags found")
return cfg
version_tags.sort(reverse=True)
logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
latest_version_tag = version_tags[0]
latest_version_pep440 = v2version.to_pep440(latest_version_tag)
if latest_version_tag <= cfg.current_version:
return cfg
logger.info(f"Working dir version : {cfg.current_version}")
logger.info(f"Latest version from VCS tag: {latest_version_tag}")
return cfg._replace(
current_version=latest_version_tag,
pep440_version=latest_version_pep440,
)
def rewrite(
cfg : config.Config,
new_version: str,
) -> None:
new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern)
v2rewrite.rewrite(cfg.file_patterns, new_vinfo)
def get_diff(cfg: config.Config, new_version: str) -> str:
new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern)
return v2rewrite.diff(new_vinfo, cfg.file_patterns)

205
src/pycalver2/patterns.py Normal file
View file

@ -0,0 +1,205 @@
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - 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("),
(")" , "\u005c)"),
]
# NOTE (mb 2020-09-04): These are depricated in favour of explicit patterns
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 = {
# recommended (based on calver.org)
'YYYY': r"[1-9]\d{3}",
'YY' : r"\d{1,2}",
'0Y' : r"\d{2}",
'Q' : r"[1-4]",
'MM' : r"(?:[1-9]|1[0-2])",
'0M' : r"(?:0[1-9]|1[0-2])",
'DD' : r"([1-9]|[1-2][0-9]|3[0-1])",
'0D' : r"(0[1-9]|[1-2][0-9]|3[0-1])",
'JJJ' : r"(?:[1-9]\d|[1-9]|[1-2]\d\d|3[0-5][0-9]|36[0-6])",
'00J' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
'WW' : r"(?:[1-9]|[1-4]\d|5[0-2])",
'0W' : r"(?:[0-4]\d|5[0-2])",
'UU' : r"(?:[1-9]|[0-4]\d|5[0-2])",
'0U' : r"(?:[0-4]\d|5[0-2])",
'VV' : r"(?:[1-9]|[1-4]\d|5[0-3])",
'0V' : r"(?:[0-4]\d|5[0-3])",
'GGGG': r"[1-9]\d{3}",
'GG' : r"\d{1,2}",
'0G' : r"\d{2}",
# non calver parts
'MAJOR': r"\d+",
'MINOR': r"\d+",
'PATCH': r"\d+",
'MICRO': r"\d+",
'BUILD': r"\d+",
'TAG' : r"(?:alpha|beta|dev|rc|post|final)",
'PYTAG': r"(?:a|b|dev|rc|post)?\d*",
# supported (but legacy)
'year' : r"\d{4}",
'month' : r"(?:0[0-9]|1[0-2])",
'month_short': r"(?:1[0-2]|[1-9])",
'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])",
'dom_short' : r"([1-9]|[1-2][0-9]|3[0-1])",
'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
'doy_short' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
'bid' : r"\d{4,}",
# dropped support (never documented)
# 'BID' : r"[1-9]\d*",
# 'MM' : r"\d{2,}",
# 'MMM' : r"\d{3,}",
# 'MMMM' : r"\d{4,}",
# 'MMMMM' : r"\d{5,}",
# 'PP' : r"\d{2,}",
# 'PPP' : r"\d{3,}",
# 'PPPP' : r"\d{4,}",
# 'PPPPP' : r"\d{5,}",
# '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 treated specially in version.format
# 'release' : "-{tag}",
'month' : "{month:02}",
'month_short': "{month}",
'build_no' : "{bid}",
'iso_week' : "{iso_week:02}",
'us_week' : "{us_week:02}",
'dom' : "{dom:02}",
'doy' : "{doy:03}",
'dom_short' : "{dom}",
'doy_short' : "{doy}",
# depricated
'pep440_version': "{year}{month:02}.{BID}{pep440_tag}",
'version' : "v{year}{month:02}.{bid}{release}",
}
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_str(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_str(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()

175
src/pycalver2/rewrite.py Normal file
View file

@ -0,0 +1,175 @@
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Rewrite files, updating occurences of version strings."""
import io
import typing as typ
import logging
from pycalver import parse
from pycalver import config
from pycalver import rewrite as v1rewrite
from pycalver2 import version
from pycalver2 import patterns
logger = logging.getLogger("pycalver2.rewrite")
def rewrite_lines(
pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, old_lines: typ.List[str]
) -> typ.List[str]:
"""TODO reenable doctest"""
pass
"""Replace occurances of pattern_strs in old_lines with new_vinfo.
>>> new_vinfo = version.parse_version_info("v201811.0123-beta")
>>> pattern_strs = ['__version__ = "{pycalver}"']
>>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"'])
['__version__ = "v201811.0123-beta"']
>>> pattern_strs = ['__version__ = "{pep440_version}"']
>>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "201809.2b0"'])
['__version__ = "201811.123b0"']
"""
new_lines = old_lines[:]
found_patterns = set()
re_patterns = [patterns.compile_pattern(p) for p in pattern_strs]
for match in parse.iter_matches(old_lines, re_patterns):
found_patterns.add(match.pattern)
replacement = version.format_version(new_vinfo, match.pattern)
span_l, span_r = match.span
new_line = match.line[:span_l] + replacement + match.line[span_r:]
new_lines[match.lineno] = new_line
non_matched_patterns = set(pattern_strs) - found_patterns
if non_matched_patterns:
for non_matched_pattern in non_matched_patterns:
logger.error(f"No match for pattern '{non_matched_pattern}'")
compiled_pattern_str = patterns.compile_pattern_str(non_matched_pattern)
logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'")
raise v1rewrite.NoPatternMatch("Invalid pattern(s)")
else:
return new_lines
def rfd_from_content(
pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, content: str
) -> v1rewrite.RewrittenFileData:
"""TODO reenable doctest"""
pass
r"""Rewrite pattern occurrences with version string.
>>> new_vinfo = version.parse_version_info("v201809.0123")
>>> pattern_strs = ['__version__ = "{pycalver}"']
>>> content = '__version__ = "v201809.0001-alpha"'
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
>>> rfd.new_lines
['__version__ = "v201809.0123"']
>>>
>>> new_vinfo = version.parse_version_info("v1.2.3", "v{semver}")
>>> pattern_strs = ['__version__ = "v{semver}"']
>>> content = '__version__ = "v1.2.2"'
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
>>> rfd.new_lines
['__version__ = "v1.2.3"']
"""
line_sep = v1rewrite.detect_line_sep(content)
old_lines = content.split(line_sep)
new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines)
return v1rewrite.RewrittenFileData("<path>", line_sep, old_lines, new_lines)
def iter_rewritten(
file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo
) -> typ.Iterable[v1rewrite.RewrittenFileData]:
"""TODO reenable doctest"""
pass
r'''Iterate over files with version string replaced.
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
>>> new_vinfo = version.parse_version_info("v201809.0123")
>>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo)
>>> rfd = list(rewritten_datas)[0]
>>> assert rfd.new_lines == [
... '# This file is part of the pycalver project',
... '# https://gitlab.com/mbarkhau/pycalver',
... '#',
... '# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License',
... '# SPDX-License-Identifier: MIT',
... '"""PyCalVer: CalVer for Python Packages."""',
... '',
... '__version__ = "v201809.0123"',
... '',
... ]
>>>
'''
fobj: typ.IO[str]
for file_path, pattern_strs in v1rewrite.iter_file_paths(file_patterns):
with file_path.open(mode="rt", encoding="utf-8") as fobj:
content = fobj.read()
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
yield rfd._replace(path=str(file_path))
def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -> str:
"""TODO reenable doctest"""
pass
r"""Generate diffs of rewritten files.
>>> new_vinfo = version.parse_version_info("v201809.0123")
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
>>> diff_str = diff(new_vinfo, file_patterns)
>>> lines = diff_str.split("\n")
>>> lines[:2]
['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py']
>>> assert lines[6].startswith('-__version__ = "v2')
>>> assert not lines[6].startswith('-__version__ = "v201809.0123"')
>>> lines[7]
'+__version__ = "v201809.0123"'
"""
full_diff = ""
fobj: typ.IO[str]
for file_path, pattern_strs in sorted(v1rewrite.iter_file_paths(file_patterns)):
with file_path.open(mode="rt", encoding="utf-8") as fobj:
content = fobj.read()
try:
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
except v1rewrite.NoPatternMatch:
# pylint:disable=raise-missing-from ; we support py2, so not an option
errmsg = f"No patterns matched for '{file_path}'"
raise v1rewrite.NoPatternMatch(errmsg)
rfd = rfd._replace(path=str(file_path))
lines = v1rewrite.diff_lines(rfd)
if len(lines) == 0:
errmsg = f"No patterns matched for '{file_path}'"
raise v1rewrite.NoPatternMatch(errmsg)
full_diff += "\n".join(lines) + "\n"
full_diff = full_diff.rstrip("\n")
return full_diff
def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo) -> None:
"""Rewrite project files, updating each with the new version."""
fobj: typ.IO[str]
for file_data in iter_rewritten(file_patterns, new_vinfo):
new_content = file_data.line_sep.join(file_data.new_lines)
with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj:
fobj.write(new_content)

590
src/pycalver2/version.py Normal file
View file

@ -0,0 +1,590 @@
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Functions related to version string manipulation."""
import typing as typ
import logging
import datetime as dt
import lexid
import pkg_resources
from . import patterns
logger = logging.getLogger("pycalver.version")
# The test suite may replace this.
TODAY = dt.datetime.utcnow().date()
PATTERN_PART_FIELDS = {
'YYYY' : 'year_y',
'YY' : 'year_y',
'0Y' : 'year_y',
'Q' : 'quarter',
'MM' : 'month',
'0M' : 'month',
'DD' : 'dom',
'0D' : 'dom',
'JJJ' : 'doy',
'00J' : 'doy',
'MAJOR': 'major',
'MINOR': 'minor',
'PATCH': 'patch',
'MICRO': 'patch',
'BUILD': 'bid',
'TAG' : 'tag',
'PYTAG': 'pytag',
'WW' : 'week_w',
'0W' : 'week_w',
'UU' : 'week_u',
'0U' : 'week_u',
'VV' : 'week_v',
'0V' : 'week_v',
'GGGG' : 'year_g',
'GG' : 'year_g',
'0G' : 'year_g',
}
ID_FIELDS_BY_PART = {
'MAJOR': 'major',
'MINOR': 'minor',
'PATCH': 'patch',
'MICRO': 'patch',
}
ZERO_VALUES = {
'major': "0",
'minor': "0",
'patch': "0",
'TAG' : "final",
'PYTAG': "",
}
class CalendarInfo(typ.NamedTuple):
"""Container for calendar components of version strings."""
year_y : int
year_g : int
quarter: int
month : int
dom : int
doy : int
week_w : int
week_u : int
week_v : 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:
"""TODO reenable doctest"""
pass
"""Generate calendar components for current date.
>>> from datetime import date
>>> c = cal_info(date(2019, 1, 5))
>>> (c.year_y, 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_y, 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_y, 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_y, 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
kwargs = {
'year_y' : date.year,
'year_g' : int(date.strftime("%G"), base=10),
'quarter': _quarter_from_month(date.month),
'month' : date.month,
'dom' : date.day,
'doy' : int(date.strftime("%j"), base=10),
'week_w' : int(date.strftime("%W"), base=10),
'week_u' : int(date.strftime("%U"), base=10),
'week_v' : int(date.strftime("%V"), base=10),
}
return CalendarInfo(**kwargs)
class VersionInfo(typ.NamedTuple):
"""Container for parsed version string."""
year_y : typ.Optional[int]
year_g : typ.Optional[int]
quarter: typ.Optional[int]
month : typ.Optional[int]
dom : typ.Optional[int]
doy : typ.Optional[int]
week_w : typ.Optional[int]
week_u : typ.Optional[int]
week_v : typ.Optional[int]
major : int
minor : int
patch : int
bid : str
tag : str
pytag : str
FieldKey = str
MatchGroupKey = str
MatchGroupStr = str
PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr]
FieldValues = typ.Dict[FieldKey , MatchGroupStr]
def _parse_field_values(field_values: FieldValues) -> VersionInfo:
fvals = field_values
tag = fvals.get('tag')
if tag is None:
tag = "final"
tag = TAG_ALIASES.get(tag, tag)
assert tag is not None
# TODO (mb 2020-09-06): parts of course
pytag = "TODO"
bid = fvals['bid'] if 'bid' in fvals else "1001"
year_y = int(fvals['year_y']) if 'year_y' in fvals else None
year_g = int(fvals['year_g']) if 'year_g' in fvals else None
doy = int(fvals['doy' ]) if 'doy' in fvals else None
date: typ.Optional[dt.date] = None
month: typ.Optional[int] = None
dom : typ.Optional[int] = None
week_w: typ.Optional[int] = None
week_u: typ.Optional[int] = None
week_v: typ.Optional[int] = None
if year_y and doy:
date = _date_from_doy(year_y, doy)
month = date.month
dom = date.day
else:
month = int(fvals['month']) if 'month' in fvals else None
dom = int(fvals['dom' ]) if 'dom' in fvals else None
quarter = int(fvals['quarter']) if 'quarter' in fvals else None
if quarter is None and month:
quarter = _quarter_from_month(month)
if year_y and month and dom:
date = dt.date(year_y, month, dom)
if date:
# derive all fields from other previous values
doy = int(date.strftime("%j"), base=10)
week_w = int(date.strftime("%W"), base=10)
week_u = int(date.strftime("%U"), base=10)
week_v = int(date.strftime("%V"), base=10)
year_g = int(date.strftime("%G"), base=10)
major = int(fvals['major']) if 'major' in fvals else 0
minor = int(fvals['minor']) if 'minor' in fvals else 0
patch = int(fvals['patch']) if 'patch' in fvals else 0
return VersionInfo(
year_y=year_y,
year_g=year_g,
quarter=quarter,
month=month,
dom=dom,
doy=doy,
week_w=week_w,
week_u=week_u,
week_v=week_v,
major=major,
minor=minor,
patch=patch,
bid=bid,
tag=tag,
pytag=pytag,
)
def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool:
"""TODO reenable doctest"""
pass
"""Check pattern for any calendar based parts.
>>> _is_calver(cal_info())
True
>>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"})
>>> _is_calver(vnfo)
True
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"})
>>> _is_calver(vnfo)
False
"""
for field in CalendarInfo._fields:
maybe_val: typ.Any = getattr(nfo, field, None)
if isinstance(maybe_val, int):
return True
return False
TAG_ALIASES: typ.Dict[str, str] = {
'a' : "alpha",
'b' : "beta",
'pre': "rc",
}
PEP440_TAGS: typ.Dict[str, str] = {
'alpha': "a",
'beta' : "b",
'final': "",
'rc' : "rc",
'dev' : "dev",
'post' : "post",
}
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
class PatternError(Exception):
pass
def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues:
for part_name in pattern_groups.keys():
is_valid_part_name = (
part_name in patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS
)
if not is_valid_part_name:
err_msg = f"Invalid part '{part_name}'"
raise PatternError(err_msg)
field_value_items = [
(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 field_value_items]
unique_fields = set(all_fields)
duplicate_fields = [f for f in unique_fields if all_fields.count(f) > 1]
if any(duplicate_fields):
err_msg = f"Multiple parts for same field {duplicate_fields}."
raise PatternError(err_msg)
return dict(field_value_items)
def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo:
"""TODO reenable doctest"""
pass
"""Parse normalized VersionInfo from groups of a matched pattern.
>>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"})
>>> (vnfo.year_y, 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_y, 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)
"""
field_values = _parse_pattern_groups(pattern_groups)
return _parse_field_values(field_values)
def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> VersionInfo:
"""TODO reenable doctest"""
pass
"""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 PatternError(err_msg)
return _parse_version_info(match.groupdict())
def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool:
"""TODO reenable doctest"""
pass
"""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 PatternError:
return False
TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]]
def _derive_template_kwargs(vinfo: VersionInfo) -> TemplateKwargs:
"""Generate kwargs for template from minimal VersionInfo.
The VersionInfo Tuple only has the minimal representation
of a parsed version, not the values suitable for formatting.
It may for example have month=9, but not the formatted
representation '09' for '0M'.
"""
kwargs: TemplateKwargs = vinfo._asdict()
tag = vinfo.tag
kwargs['TAG'] = tag
if tag == 'final':
kwargs['PYTAG'] = ""
else:
kwargs['PYTAG'] = PEP440_TAGS[tag] + "0"
year_y = vinfo.year_y
if year_y:
kwargs['0Y' ] = str(year_y)[-2:]
kwargs['YY' ] = int(str(year_y)[-2:])
kwargs['YYYY'] = year_y
year_g = vinfo.year_g
if year_g:
kwargs['0G' ] = str(year_g)[-2:]
kwargs['GG' ] = int(str(year_g)[-2:])
kwargs['GGGG'] = year_g
kwargs['BUILD'] = int(vinfo.bid, 10)
for part_name, field in ID_FIELDS_BY_PART.items():
val = kwargs[field]
if part_name.lower() == field.lower():
if isinstance(val, str):
kwargs[part_name] = int(val, base=10)
else:
kwargs[part_name] = val
else:
assert len(set(part_name)) == 1
padded_len = len(part_name)
kwargs[part_name] = str(val).zfill(padded_len)
return kwargs
def _compile_format_template(pattern: str, kwargs: TemplateKwargs) -> str:
# NOTE (mb 2020-09-04): Some parts are optional, we need the kwargs to
# determine if part is set to its zero value
format_tmpl = pattern
for part_name, full_part_format in patterns.FULL_PART_FORMATS.items():
format_tmpl = format_tmpl.replace("{" + part_name + "}", full_part_format)
return format_tmpl
def format_version(vinfo: VersionInfo, pattern: str) -> str:
"""TODO reenable doctest"""
pass
"""Generate version string.
>>> import datetime as dt
>>> vinfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}")
>>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict())
>>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict())
>>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}")
'v17.33-beta'
>>> format_version(vinfo_a, pattern="vYY.BUILD[-TAG]")
'v17.33-beta'
>>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG]")
'201701.33b0'
>>> format_version(vinfo_a, pattern="{pycalver}")
'v201701.0033-beta'
>>> format_version(vinfo_b, pattern="{pycalver}")
'v201712.0033-beta'
>>> format_version(vinfo_a, pattern="v{year}w{iso_week}.{BID}{release}")
'v2017w00.33-beta'
>>> format_version(vinfo_a, pattern="vYYYYwWW.BUILD[-TAG]")
'v2017w00.33-beta'
>>> format_version(vinfo_b, pattern="v{year}w{iso_week}.{BID}{release}")
'v2017w52.33-beta'
>>> format_version(vinfo_b, pattern="vYYYYwWW.BUILD[-TAG]")
'v2017w52.33-beta'
>>> format_version(vinfo_a, pattern="v{year}d{doy}.{bid}{release}")
'v2017d001.0033-beta'
>>> format_version(vinfo_b, pattern="v{year}d{doy}.{bid}{release}")
'v2017d365.0033-beta'
>>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]")
'v2017d001.0033-beta'
>>> format_version(vinfo_b, pattern="vYYYYdJJJ.BUILD[-TAG]")
'v2017d365.0033-beta'
>>> format_version(vinfo_a, pattern="vGGGGwVV.BUILD[-TAG]")
'v2016w52.0033-beta'
>>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final')
>>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}-{tag}")
'v2017w52.33-final'
>>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}{release}")
'v2017w52.33'
>>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG")
'v2017w52.33-final'
>>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]")
'v2017w52.33'
>>> format_version(vinfo_c, pattern="v{MAJOR}.{MINOR}.{PATCH}")
'v1.2.34'
>>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH")
'v1.2.34'
>>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final')
>>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG")
'v1.0.0-final'
>>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-TAG]")
'v1.0.0'
>>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]")
'v1.0'
>>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.MICRO[-TAG]]")
'v1.0'
>>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]")
'v1'
"""
kwargs = _derive_template_kwargs(vinfo)
format_tmpl = _compile_format_template(pattern, kwargs)
return format_tmpl.format(**kwargs)
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'
"""
try:
old_vinfo = parse_version_info(old_version, pattern)
except PatternError as ex:
logger.error(str(ex))
return None
cur_vinfo = old_vinfo
cur_cal_nfo = cal_info()
old_date = (old_vinfo.year_y or 0, old_vinfo.month or 0, old_vinfo.dom or 0)
cur_date = (cur_cal_nfo.year_y , cur_cal_nfo.month , cur_cal_nfo.dom)
if old_date <= cur_date:
cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict())
else:
logger.warning(f"Version appears to be from the future '{old_version}'")
cur_vinfo = cur_vinfo._replace(bid=lexid.incr(cur_vinfo.bid))
if major:
cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0)
if minor:
cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0)
if patch:
cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1)
if release:
cur_vinfo = cur_vinfo._replace(tag=release)
new_version = format_version(cur_vinfo, pattern)
if new_version == old_version:
logger.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.
>>> to_pep440("v201811.0007-beta")
'201811.7b0'
"""
return str(pkg_resources.parse_version(version))