module reorg

This commit is contained in:
Manuel Barkhau 2020-09-19 22:35:48 +00:00
parent e1aaf7629b
commit 8af5047244
23 changed files with 1658 additions and 1532 deletions

View file

@ -1,8 +0,0 @@
# 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"

View file

@ -1,54 +0,0 @@
#!/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 pycalver.version as v1version
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 = v1version.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)

View file

@ -1,252 +0,0 @@
# 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.
>>> pattern = compile_pattern("vYYYY0M.BUILD[-TAG]")
>>> version_info = pattern.regexp.match("v201712.0123-alpha")
>>> assert version_info.groupdict() == {
... "version": "v201712.0123-alpha",
... "year_y" : "2017",
... "month" : "12",
... "bid" : "0123",
... "tag" : "alpha",
... }
>>>
>>> version_info = pattern.regexp.match("201712.1234")
>>> assert version_info is None
>>> version_info = pattern.regexp.match("v201713.1234")
>>> assert version_info is None
>>> version_info = pattern.regexp.match("v201712.1234")
>>> assert version_info.groupdict() == {
... "version": "v201712.1234",
... "year_y" : "2017",
... "month" : "12",
... "bid" : "1234",
... "tag" : None,
... }
"""
import re
import typing as typ
import pycalver.patterns as v1patterns
PATTERN_ESCAPES = [
("\u005c", "\u005c\u005c"),
("-" , "\u005c-"),
("." , "\u005c."),
("+" , "\u005c+"),
("*" , "\u005c*"),
("?" , "\u005c?"),
("{" , "\u005c{"),
("}" , "\u005c}"),
# ("[" , "\u005c["), # [braces] are used for optional parts
# ("]" , "\u005c]"),
("(", "\u005c("),
(")", "\u005c)"),
]
# NOTE (mb 2020-09-17): For patterns with different options, the longer
# patterns should be first/left (e.g. for 'MM', `1[0-2]` before `[1-9]`).
# This ensures that the longest match is done rather than the shortest.
# To have a consistent ordering, we always put the pattern that matches
# the larger number first (even if the patterns would otherwise be the
# same size).
PART_PATTERNS = {
# Based on calver.org
'YYYY': r"[1-9][0-9]{3}",
'YY' : r"[1-9][0-9]?",
'0Y' : r"[0-9]{2}",
'GGGG': r"[1-9][0-9]{3}",
'GG' : r"[1-9][0-9]?",
'0G' : r"[0-9]{2}",
'Q' : r"[1-4]",
'MM' : r"(?:1[0-2]|[1-9])",
'0M' : r"(?:1[0-2]|0[1-9])",
'DD' : r"(?:3[0-1]|[1-2][0-9]|[1-9])",
'0D' : r"(?:3[0-1]|[1-2][0-9]|0[1-9])",
'JJJ' : r"(?:36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|[1-9][0-9]|[1-9])",
'00J' : r"(?:36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|0[1-9][0-9]|00[1-9])",
# week numbering parts
'WW': r"(?:5[0-2]|[1-4][0-9]|[0-9])",
'0W': r"(?:5[0-2]|[0-4][0-9])",
'UU': r"(?:5[0-2]|[1-4][0-9]|[0-9])",
'0U': r"(?:5[0-2]|[0-4][0-9])",
'VV': r"(?:5[0-3]|[1-4][0-9]|[1-9])",
'0V': r"(?:5[0-3]|[1-4][0-9]|0[1-9])",
# non calver parts
'MAJOR': r"[0-9]+",
'MINOR': r"[0-9]+",
'PATCH': r"[0-9]+",
'BUILD': r"[0-9]+",
'BLD' : r"[1-9][0-9]*",
'TAG' : r"(?:alpha|beta|dev|pre|rc|post|final)",
'PYTAG': r"(?:a|b|dev|rc|post)",
'NUM' : r"[0-9]+",
}
PATTERN_PART_FIELDS = {
'YYYY' : 'year_y',
'YY' : 'year_y',
'0Y' : 'year_y',
'GGGG' : 'year_g',
'GG' : 'year_g',
'0G' : 'year_g',
'Q' : 'quarter',
'MM' : 'month',
'0M' : 'month',
'DD' : 'dom',
'0D' : 'dom',
'JJJ' : 'doy',
'00J' : 'doy',
'MAJOR': 'major',
'MINOR': 'minor',
'PATCH': 'patch',
'BUILD': 'bid',
'BLD' : 'bid',
'TAG' : 'tag',
'PYTAG': 'pytag',
'NUM' : 'num',
'WW' : 'week_w',
'0W' : 'week_w',
'UU' : 'week_u',
'0U' : 'week_u',
'VV' : 'week_v',
'0V' : 'week_v',
}
FieldValue = typ.Union[str, int]
def _fmt_num(val: FieldValue) -> str:
return str(val)
def _fmt_bld(val: FieldValue) -> str:
return str(int(val))
def _fmt_yy(year_y: FieldValue) -> str:
return str(int(str(year_y)[-2:]))
def _fmt_0y(year_y: FieldValue) -> str:
return "{0:02}".format(int(str(year_y)[-2:]))
def _fmt_gg(year_g: FieldValue) -> str:
return str(int(str(year_g)[-2:]))
def _fmt_0g(year_g: FieldValue) -> str:
return "{0:02}".format(int(str(year_g)[-2:]))
def _fmt_0m(month: FieldValue) -> str:
return "{0:02}".format(int(month))
def _fmt_0d(dom: FieldValue) -> str:
return "{0:02}".format(int(dom))
def _fmt_00j(doy: FieldValue) -> str:
return "{0:03}".format(int(doy))
def _fmt_0w(week_w: FieldValue) -> str:
return "{0:02}".format(int(week_w))
def _fmt_0u(week_u: FieldValue) -> str:
return "{0:02}".format(int(week_u))
def _fmt_0v(week_v: FieldValue) -> str:
return "{0:02}".format(int(week_v))
PART_FORMATS: typ.Dict[str, typ.Callable[[FieldValue], str]] = {
'YYYY' : _fmt_num,
'YY' : _fmt_yy,
'0Y' : _fmt_0y,
'GGGG' : _fmt_num,
'GG' : _fmt_gg,
'0G' : _fmt_0g,
'Q' : _fmt_num,
'MM' : _fmt_num,
'0M' : _fmt_0m,
'DD' : _fmt_num,
'0D' : _fmt_0d,
'JJJ' : _fmt_num,
'00J' : _fmt_00j,
'MAJOR': _fmt_num,
'MINOR': _fmt_num,
'PATCH': _fmt_num,
'BUILD': _fmt_num,
'BLD' : _fmt_bld,
'TAG' : _fmt_num,
'PYTAG': _fmt_num,
'NUM' : _fmt_num,
'WW' : _fmt_num,
'0W' : _fmt_0w,
'UU' : _fmt_num,
'0U' : _fmt_0u,
'VV' : _fmt_num,
'0V' : _fmt_0v,
}
def _replace_pattern_parts(pattern: str) -> str:
# The pattern is escaped, so that everything besides the format
# string variables is treated literally.
if "[" in pattern and "]" in pattern:
pattern = pattern.replace("[", "(?:")
pattern = pattern.replace("]", ")?")
part_patterns_by_index: typ.Dict[typ.Tuple[int, int], typ.Tuple[int, int, str]] = {}
for part_name, part_pattern in PART_PATTERNS.items():
start_idx = pattern.find(part_name)
if start_idx < 0:
continue
field = PATTERN_PART_FIELDS[part_name]
named_part_pattern = f"(?P<{field}>{part_pattern})"
end_idx = start_idx + len(part_name)
sort_key = (-end_idx, -len(part_name))
part_patterns_by_index[sort_key] = (start_idx, end_idx, named_part_pattern)
# NOTE (mb 2020-09-17): The sorting is done so that we process items:
# - right before left
# - longer before shorter
last_start_idx = len(pattern) + 1
result_pattern = pattern
for _, (start_idx, end_idx, named_part_pattern) in sorted(part_patterns_by_index.items()):
if end_idx <= last_start_idx:
result_pattern = (
result_pattern[:start_idx] + named_part_pattern + result_pattern[end_idx:]
)
last_start_idx = start_idx
return result_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) -> v1patterns.Pattern:
pattern_str = compile_pattern_str(pattern)
pattern_re = re.compile(pattern_str)
return v1patterns.Pattern(pattern, pattern_re)

View file

@ -1,178 +0,0 @@
# 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
import pycalver.rewrite as v1rewrite
import pycalver2.version as v2version
import pycalver2.patterns as v2patterns
from pycalver import parse
from pycalver import config
logger = logging.getLogger("pycalver2.rewrite")
def rewrite_lines(
pattern_strs: typ.List[str],
new_vinfo : v2version.VersionInfo,
old_lines : typ.List[str],
) -> typ.List[str]:
"""Replace occurances of pattern_strs in old_lines with new_vinfo.
>>> new_vinfo = v2version.parse_version_info("v201811.0123-beta")
>>> pattern_strs = ['__version__ = "vYYYY0M.BUILD[-TAG]"']
>>> old_lines = ['__version__ = "v201809.0002-alpha" ']
>>> rewrite_lines(pattern_strs, new_vinfo, old_lines)
['__version__ = "v201811.0123-beta" ']
>>> old_lines = ['__version__ = "v201809.0002-alpha" # comment']
>>> rewrite_lines(pattern_strs, new_vinfo, old_lines)
['__version__ = "v201811.0123-beta" # comment']
>>> pattern_strs = ['__version__ = "YYYY0M.BLD[PYTAGNUM]"']
>>> old_lines = ['__version__ = "201809.2a0"']
>>> rewrite_lines(pattern_strs, new_vinfo, old_lines)
['__version__ = "201811.123b0"']
"""
new_lines = old_lines[:]
found_patterns = set()
patterns = [v2patterns.compile_pattern(p) for p in pattern_strs]
matches = parse.iter_matches(old_lines, patterns)
for match in matches:
found_patterns.add(match.pattern.raw)
replacement = v2version.format_version(new_vinfo, match.pattern.raw)
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 = v2patterns.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 : v2version.VersionInfo,
content : str,
) -> v1rewrite.RewrittenFileData:
r"""Rewrite pattern occurrences with version string.
>>> new_vinfo = v2version.parse_version_info("v201809.0123")
>>> pattern_strs = ['__version__ = "vYYYY0M.BUILD[-TAG]"']
>>> content = '__version__ = "v201809.0001-alpha"'
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
>>> rfd.new_lines
['__version__ = "v201809.0123"']
>>>
>>> new_vinfo = v2version.parse_version_info("v1.2.3", "vMAJOR.MINOR.PATCH")
>>> pattern_strs = ['__version__ = "vMAJOR.MINOR.PATCH"']
>>> 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 : v2version.VersionInfo,
) -> typ.Iterable[v1rewrite.RewrittenFileData]:
r'''Iterate over files with version string replaced.
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']}
>>> new_vinfo = v2version.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://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__ = "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 : v2version.VersionInfo,
file_patterns: config.PatternsByGlob,
) -> str:
r"""Generate diffs of rewritten files.
>>> new_vinfo = v2version.parse_version_info("v201809.0123")
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']}
>>> 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: v2version.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)

View file

@ -1,638 +0,0 @@
# 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 pycalver2.patterns as v2patterns
# import pycalver.version as v1version
# import pycalver.patterns as v1patterns
logger = logging.getLogger("pycalver.version")
# The test suite may replace this.
TODAY = dt.datetime.utcnow().date()
ZERO_VALUES = {
'MAJOR': "0",
'MINOR': "0",
'PATCH': "0",
'TAG' : "final",
'PYTAG': "",
'NUM' : "0",
}
TAG_BY_PEP440_TAG = {
'a' : 'alpha',
'b' : 'beta',
"" : 'final',
'rc' : 'rc',
'dev' : 'dev',
'post': 'post',
}
PEP440_TAG_BY_TAG = {
'alpha': "a",
'beta' : "b",
'final': "",
'pre' : "rc",
'rc' : "rc",
'dev' : "dev",
'post' : "post",
}
assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values())
assert set(TAG_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_TAG.keys())
# PEP440_TAGS_REVERSE = {
# "a" : 'alpha',
# "b" : 'beta',
# "rc" : 'rc',
# "dev" : 'dev',
# "post": 'post',
# }
MaybeInt = typ.Optional[int]
class CalendarInfo(typ.NamedTuple):
"""Container for calendar components of version strings."""
year_y : MaybeInt
year_g : MaybeInt
quarter: MaybeInt
month : MaybeInt
dom : MaybeInt
doy : MaybeInt
week_w : MaybeInt
week_u : MaybeInt
week_v : MaybeInt
class VersionInfo(typ.NamedTuple):
"""Container for parsed version string."""
year_y : MaybeInt
year_g : MaybeInt
quarter: MaybeInt
month : MaybeInt
dom : MaybeInt
doy : MaybeInt
week_w : MaybeInt
week_u : MaybeInt
week_v : MaybeInt
major : int
minor : int
patch : int
num : int
bid : str
tag : str
pytag : str
def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo:
return CalendarInfo(
vinfo.year_y,
vinfo.year_g,
vinfo.quarter,
vinfo.month,
vinfo.dom,
vinfo.doy,
vinfo.week_w,
vinfo.week_u,
vinfo.week_v,
)
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.
>>> import datetime as dt
>>> c = cal_info(dt.date(2019, 1, 5))
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v)
(2019, 1, 1, 5, 5, 0, 0, 1)
>>> c = cal_info(dt.date(2019, 1, 6))
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v)
(2019, 1, 1, 6, 6, 0, 1, 1)
>>> c = cal_info(dt.date(2019, 1, 7))
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v)
(2019, 1, 1, 7, 7, 1, 1, 2)
>>> c = cal_info(dt.date(2019, 4, 7))
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.week_w, c.week_u, c.week_v)
(2019, 2, 4, 7, 97, 13, 14, 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)
VALID_FIELD_KEYS = set(VersionInfo._fields) | {'version'}
FieldKey = str
MatchGroupKey = str
MatchGroupStr = str
PatternGroups = typ.Dict[FieldKey, MatchGroupStr]
FieldValues = typ.Dict[FieldKey, MatchGroupStr]
def _parse_version_info(field_values: FieldValues) -> VersionInfo:
"""Parse normalized VersionInfo from groups of a matched pattern.
>>> vnfo = _parse_version_info({'year_y': "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_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"})
>>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.doy, vnfo.bid, vnfo.tag)
(2018, 1, 11, 11, '099', 'beta')
>>> vnfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"})
>>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.doy)
(2018, 6, 15, 166)
>>> 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", 'minor': "023", 'patch': "0045"})
>>> (vnfo.major, vnfo.minor, vnfo.patch)
(1, 23, 45)
>>> vnfo = _parse_version_info({'year_y': "2021", 'week_w': "02"})
>>> (vnfo.year_y, vnfo.week_w)
(2021, 2)
>>> vnfo = _parse_version_info({'year_y': "2021", 'week_u': "02"})
>>> (vnfo.year_y, vnfo.week_u)
(2021, 2)
>>> vnfo = _parse_version_info({'year_g': "2021", 'week_v': "02"})
>>> (vnfo.year_g, vnfo.week_v)
(2021, 2)
>>> vnfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"})
>>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.tag)
(2021, 1, 3, 'final')
>>> (vnfo.year_y, vnfo.week_w, vnfo.year_y, vnfo.week_u,vnfo.year_g, vnfo.week_v)
(2021, 0, 2021, 1, 2020, 53)
"""
for key in field_values:
assert key in VALID_FIELD_KEYS, key
fvals = field_values
tag = fvals.get('tag' ) or "final"
pytag = fvals.get('pytag') or ""
if tag and not pytag:
pytag = PEP440_TAG_BY_TAG[tag]
elif pytag and not tag:
tag = TAG_BY_PEP440_TAG[pytag]
date: typ.Optional[dt.date] = None
year_y: MaybeInt = int(fvals['year_y']) if 'year_y' in fvals else None
year_g: MaybeInt = int(fvals['year_g']) if 'year_g' in fvals else None
month: MaybeInt = int(fvals['month']) if 'month' in fvals else None
doy : MaybeInt = int(fvals['doy' ]) if 'doy' in fvals else None
dom : MaybeInt = int(fvals['dom' ]) if 'dom' in fvals else None
week_w: MaybeInt = int(fvals['week_w']) if 'week_w' in fvals else None
week_u: MaybeInt = int(fvals['week_u']) if 'week_u' in fvals else None
week_v: MaybeInt = int(fvals['week_v']) if 'week_v' in fvals else 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
if year_y and month and dom:
date = dt.date(year_y, month, dom)
if date:
# derive all fields from other previous values
year_y = int(date.strftime("%Y"), base=10)
year_g = int(date.strftime("%G"), base=10)
month = int(date.strftime("%m"), base=10)
dom = int(date.strftime("%d"), base=10)
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)
quarter = int(fvals['quarter']) if 'quarter' in fvals else None
if quarter is None and month:
quarter = _quarter_from_month(month)
# NOTE (mb 2020-09-18): If a part is optional, fvals[<field>] may be None
major = int(fvals.get('major') or 0)
minor = int(fvals.get('minor') or 0)
patch = int(fvals.get('patch') or 0)
num = int(fvals.get('num' ) or 0)
bid = fvals['bid'] if 'bid' in fvals else "1000"
vnfo = 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,
num=num,
bid=bid,
tag=tag,
pytag=pytag,
)
return vnfo
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
class PatternError(Exception):
pass
def parse_version_info(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG[NUM]]") -> VersionInfo:
"""Parse normalized VersionInfo.
>>> vnfo = parse_version_info("v201712.0033-beta0", pattern="vYYYY0M.BUILD[-TAG[NUM]]")
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta", 'num': 0}
>>> assert vnfo == _parse_version_info(fvals)
>>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG[NUM]]")
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}
>>> assert vnfo == _parse_version_info(fvals)
>>> vnfo = parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]")
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"}
>>> assert vnfo == _parse_version_info(fvals)
>>> vnfo = parse_version_info("1.23.456", pattern="MAJOR.MINOR.PATCH")
>>> fvals = {'major': "1", 'minor': "23", 'patch': "456"}
>>> assert vnfo == _parse_version_info(fvals)
"""
pattern_tup = v2patterns.compile_pattern(pattern)
match = pattern_tup.regexp.match(version_str)
if match is None:
err_msg = (
f"Invalid version string '{version_str}' "
f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'"
)
raise PatternError(err_msg)
else:
field_values = match.groupdict()
return _parse_version_info(field_values)
def is_valid(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool:
"""Check if a version matches a pattern.
>>> is_valid("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]")
True
>>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH")
False
>>> is_valid("1.2.3", pattern="MAJOR.MINOR.PATCH")
True
>>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH")
False
"""
try:
parse_version_info(version_str, pattern)
return True
except PatternError:
return False
TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]]
def _format_part_values(vinfo: VersionInfo) -> typ.Dict[str, str]:
"""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'.
>>> vinfo = parse_version_info("v200709.1033-beta", pattern="vYYYY0M.BUILD[-TAG]")
>>> kwargs = _format_part_values(vinfo)
>>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['TAG'])
('2007', '09', '1033', 'beta')
>>> (kwargs['YY'], kwargs['0Y'], kwargs['MM'], kwargs['PYTAG'])
('7', '07', '9', 'b')
>>> vinfo = parse_version_info("200709.1033b1", pattern="YYYY0M.BLD[PYTAGNUM]")
>>> kwargs = _format_part_values(vinfo)
>>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['PYTAG'], kwargs['NUM'])
('2007', '09', '1033', 'b', '1')
"""
vnfo_kwargs: TemplateKwargs = vinfo._asdict()
kwargs : typ.Dict[str, str] = {}
for part, field in v2patterns.PATTERN_PART_FIELDS.items():
field_val = vnfo_kwargs[field]
if field_val is not None:
format_fn = v2patterns.PART_FORMATS[part]
kwargs[part] = format_fn(field_val)
return kwargs
def _make_segments(pattern: str) -> typ.List[str]:
pattern_segs_l: typ.List[str] = []
pattern_segs_r: typ.List[str] = []
pattern_rest = pattern
while "[" in pattern_rest and "]" in pattern_rest:
try:
seg_l , pattern_rest = pattern_rest.split("[", 1)
pattern_rest, seg_r = pattern_rest.rsplit("]", 1)
except ValueError as val_err:
if "values to unpack" in str(val_err):
pat_err = PatternError(f"Unbalanced braces [] in '{pattern}'")
pat_err.__cause__ = val_err
raise pat_err
else:
raise
pattern_segs_l.append(seg_l)
pattern_segs_r.append(seg_r)
pattern_segs_l.append(pattern_rest)
return pattern_segs_l + list(reversed(pattern_segs_r))
def _clear_zero_segments(
formatted_segs: typ.List[str], is_zero_segment: typ.List[bool]
) -> typ.List[str]:
non_zero_segs = list(formatted_segs)
has_val_to_right = False
for idx, is_zero in reversed(list(enumerate(is_zero_segment))):
is_optional = 0 < idx < len(formatted_segs) - 1
if is_optional:
if is_zero and not has_val_to_right:
non_zero_segs[idx] = ""
else:
has_val_to_right = True
return non_zero_segs
def _format_segments(
vinfo : VersionInfo,
pattern_segs: typ.List[str],
) -> typ.List[str]:
kwargs = _format_part_values(vinfo)
part_values = sorted(kwargs.items(), key=lambda item: -len(item[0]))
is_zero_segment = [True] * len(pattern_segs)
formatted_segs_l: typ.List[str] = []
formatted_segs_r: typ.List[str] = []
idx_l = 0
idx_r = len(pattern_segs) - 1
while idx_l <= idx_r:
# NOTE (mb 2020-09-18): All segments are optional,
# except the most left and the most right,
# i.e the ones NOT surrounded by braces.
# Empty string is a valid segment.
is_optional = idx_l > 0
seg_l = pattern_segs[idx_l]
seg_r = pattern_segs[idx_r]
for part, part_value in part_values:
if part in seg_l:
seg_l = seg_l.replace(part, part_value)
if not (is_optional and str(part_value) == ZERO_VALUES.get(part)):
is_zero_segment[idx_l] = False
if part in seg_r:
seg_r = seg_r.replace(part, part_value)
if not (is_optional and str(part_value) == ZERO_VALUES[part]):
is_zero_segment[idx_r] = False
formatted_segs_l.append(seg_l)
if idx_l < idx_r:
formatted_segs_r.append(seg_r)
idx_l += 1
idx_r -= 1
formatted_segs = formatted_segs_l + list(reversed(formatted_segs_r))
return _clear_zero_segments(formatted_segs, is_zero_segment)
def format_version(vinfo: VersionInfo, pattern: str) -> str:
"""Generate version string.
>>> import datetime as dt
>>> vinfo = parse_version_info("v200712.0033-beta", pattern="vYYYY0M.BUILD[-TAG[NUM]]")
>>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2007, 1, 1))._asdict())
>>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2007, 12, 31))._asdict())
>>> format_version(vinfo_a, pattern="vYY.BLD[-PYTAGNUM]")
'v7.33-b0'
>>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG[NUM]]")
'200701.0033b'
>>> format_version(vinfo_a, pattern="vYY.BLD[-PYTAGNUM]")
'v7.33-b0'
>>> format_version(vinfo_a, pattern="v0Y.BLD[-TAG]")
'v07.33-beta'
>>> format_version(vinfo_a, pattern="vYYYY0M.BUILD[-TAG]")
'v200701.0033-beta'
>>> format_version(vinfo_b, pattern="vYYYY0M.BUILD[-TAG]")
'v200712.0033-beta'
>>> format_version(vinfo_a, pattern="vYYYYw0W.BUILD[-TAG]")
'v2007w01.0033-beta'
>>> format_version(vinfo_a, pattern="vYYYYwWW.BLD[-TAG]")
'v2007w1.33-beta'
>>> format_version(vinfo_b, pattern="vYYYYw0W.BUILD[-TAG]")
'v2007w53.0033-beta'
>>> format_version(vinfo_a, pattern="vYYYYd00J.BUILD[-TAG]")
'v2007d001.0033-beta'
>>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]")
'v2007d1.0033-beta'
>>> format_version(vinfo_b, pattern="vYYYYd00J.BUILD[-TAG]")
'v2007d365.0033-beta'
>>> format_version(vinfo_a, pattern="vGGGGwVV.BLD[PYTAGNUM]")
'v2007w1.33b0'
>>> format_version(vinfo_a, pattern="vGGGGw0V.BUILD[-TAG]")
'v2007w01.0033-beta'
>>> format_version(vinfo_b, pattern="vGGGGw0V.BUILD[-TAG]")
'v2008w01.0033-beta'
>>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final')
>>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG")
'v2007w53.0033-final'
>>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]")
'v2007w53.0033'
>>> 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-TAGNUM")
'v1.0.0-final0'
>>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG[NUM]")
'v1.0.0-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[.PATCH[-TAG]]]")
'v1'
>>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=1, tag='rc', num=0)
>>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH]]")
'v1.0.1'
>>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]")
'v1.0.1-rc'
>>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAGNUM]]]")
'v1.0.1-rc0'
>>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH]]")
'v1.0.1'
>>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2)
>>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]")
'v1.0.0-rc2'
>>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2)
>>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]"')
'__version__ = "v1.0.0-rc2"'
"""
pattern_segs = _make_segments(pattern)
formatted_segs = _format_segments(vinfo, pattern_segs)
return "".join(formatted_segs)
def incr(
old_version: str,
pattern : str = "vYYYY0M.BUILD[-TAG]",
*,
release : str = None,
major : bool = False,
minor : bool = False,
patch : bool = False,
pin_date: 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 = _ver_to_cal_info(old_vinfo) if pin_date else 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 or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0)
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}'")
_bid = cur_vinfo.bid
if int(_bid) < 1000:
# prevent truncation of leading zeros
_bid = str(int(_bid) + 1000)
cur_vinfo = cur_vinfo._replace(bid=lexid.incr(_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