add v2 parsing

This commit is contained in:
Manuel Barkhau 2020-09-17 23:45:25 +00:00
parent d4bd8a5931
commit 5940fdbc40
3 changed files with 435 additions and 322 deletions

View file

@ -3,30 +3,33 @@
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
# """Compose Regular Expressions from Patterns.
"""Compose Regular Expressions from Patterns.
# >>> pattern = compile_pattern("vYYYY0M.BUILD[-TAG]")
# >>> version_info = pattern.regexp.match("v201712.0123-alpha")
# >>> assert version_info == {
# ... "version": "v201712.0123-alpha",
# ... "YYYY" : "2017",
# ... "0M" : "12",
# ... "BUILD" : "0123",
# ... "TAG" : "alpha",
# ... }
# >>>
# >>> version_info = pattern.regexp.match("201712.1234")
# >>> assert version_info is None
>>> 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("v201712.1234")
# >>> assert version_info == {
# ... "version": "v201712.0123-alpha",
# ... "YYYY" : "2017",
# ... "0M" : "12",
# ... "BUILD" : "0123",
# ... "TAG" : 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
@ -42,43 +45,52 @@ PATTERN_ESCAPES = [
("?" , "\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}",
'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]|[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-6])",
'00J' : r"(?:00[1-9]|0[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-6])",
# week numbering parts
'WW' : r"(?:[0-9]|[1-4][0-9]|5[0-2])",
'0W' : r"(?:[0-4][0-9]|5[0-2])",
'UU' : r"(?:[0-9]|[1-4][0-9]|5[0-2])",
'0U' : r"(?:[0-4][0-9]|5[0-2])",
'VV' : r"(?:[1-9]|[1-4][0-9]|5[0-3])",
'0V' : r"(?:0[1-9]|[1-4][0-9]|5[0-3])",
'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]+",
'MICRO': r"[0-9]+",
'BUILD': r"[0-9]+",
'TAG' : r"(?:alpha|beta|dev|rc|post|final)",
'PYTAG': r"(?:a|b|dev|rc|post)?[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',
@ -109,17 +121,118 @@ PATTERN_PART_FIELDS = {
'VV' : 'week_v',
'0V' : 'week_v',
}
FieldValue = typ.Union[str, int]
def _fmt_num(val: FieldValue) -> str:
return str(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,
'MICRO': _fmt_num,
'BUILD': _fmt_num,
'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():
named_part_pattern = f"(?P<{part_name}>{part_pattern})"
placeholder = "\u005c{" + part_name + "\u005c}"
pattern = pattern.replace(placeholder, named_part_pattern)
return pattern
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 "(?P<version>" + result_pattern + ")"
def compile_pattern_str(pattern: str) -> str:

View file

@ -12,9 +12,11 @@ import datetime as dt
import lexid
import pkg_resources
# import pycalver.patterns as v1patterns
import pycalver2.patterns as v2patterns
# import pycalver.version as v1version
# import pycalver.patterns as v1patterns
logger = logging.getLogger("pycalver.version")
@ -34,11 +36,44 @@ ZERO_VALUES = {
'major': "0",
'minor': "0",
'patch': "0",
'TAG' : "final",
'PYTAG': "",
'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',
# }
class CalendarInfo(typ.NamedTuple):
"""Container for calendar components of version strings."""
@ -79,27 +114,26 @@ def _quarter_from_month(month: int) -> int:
def cal_info(date: dt.date = None) -> CalendarInfo:
# TODO reenable doctest
# """Generate calendar components for current date.
"""Generate calendar components for current date.
# >>> from datetime import date
>>> import datetime as dt
# >>> 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(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(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(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(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(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(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)
# """
>>> 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
@ -118,59 +152,105 @@ def cal_info(date: dt.date = None) -> CalendarInfo:
return CalendarInfo(**kwargs)
MaybeInt = typ.Optional[int]
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]
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
bid : str
tag : str
pytag : str
num : MaybeInt
VALID_FIELD_KEYS = set(VersionInfo._fields) | {'version'}
FieldKey = str
MatchGroupKey = str
MatchGroupStr = str
PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr]
FieldValues = typ.Dict[FieldKey , MatchGroupStr]
PatternGroups = typ.Dict[FieldKey, MatchGroupStr]
FieldValues = typ.Dict[FieldKey, MatchGroupStr]
def _parse_field_values(field_values: FieldValues) -> VersionInfo:
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')
if tag is None:
tag = "final"
tag = fvals.get('tag' , "final")
pytag = fvals.get('pytag', "")
tag = TAG_ALIASES.get(tag, tag)
assert tag is not None
# TODO (mb 2020-09-06): parts of course
pytag = "TODO"
if tag and not pytag:
pytag = PEP440_TAG_BY_TAG[tag]
elif pytag and not tag:
tag = TAG_BY_PEP440_TAG[pytag]
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
num: MaybeInt = int(fvals['num']) if 'num' in fvals else None
date: typ.Optional[dt.date] = None
month: typ.Optional[int] = None
dom : typ.Optional[int] = 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
week_w: typ.Optional[int] = None
week_u: typ.Optional[int] = None
week_v: typ.Optional[int] = 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)
@ -180,26 +260,31 @@ def _parse_field_values(field_values: FieldValues) -> VersionInfo:
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
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)
year_g = int(date.strftime("%G"), base=10)
quarter = int(fvals['quarter']) if 'quarter' in fvals else None
if quarter is None and month:
quarter = _quarter_from_month(month)
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(
bid = fvals['bid'] if 'bid' in fvals else "1000"
vnfo = VersionInfo(
year_y=year_y,
year_g=year_g,
quarter=quarter,
@ -215,47 +300,9 @@ def _parse_field_values(field_values: FieldValues) -> VersionInfo:
bid=bid,
tag=tag,
pytag=pytag,
num=num,
)
def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool:
# TODO reenable doctest
# """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",
}
return vnfo
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
@ -265,65 +312,17 @@ 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 v2patterns.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
# """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 = "vYYYY0M.BUILD[-TAG]") -> VersionInfo:
# """Parse normalized VersionInfo.
"""Parse normalized VersionInfo.
# >>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]")
# >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"})
>>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]")
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}
>>> assert vnfo == _parse_version_info(fvals)
# >>> vnfo = parse_version_info("1.23.456", pattern="MAJOR.MINOR.PATCH")
# >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"})
# """
>>> 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:
@ -332,23 +331,23 @@ def parse_version_info(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") -
f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'"
)
raise PatternError(err_msg)
return _parse_version_info(match.groupdict())
else:
field_values = match.groupdict()
return _parse_version_info(field_values)
def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool:
# TODO reenable doctest
# """Check if a version matches a pattern.
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="{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
# """
>>> 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
@ -359,140 +358,106 @@ def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool:
TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]]
def _derive_template_kwargs(vinfo: VersionInfo) -> TemplateKwargs:
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')
"""
kwargs: TemplateKwargs = vinfo._asdict()
vnfo_kwargs: TemplateKwargs = vinfo._asdict()
kwargs : typ.Dict[str, str] = {}
tag = vinfo.tag
kwargs['TAG'] = tag
if tag == 'final':
kwargs['PYTAG'] = ""
else:
kwargs['PYTAG'] = PEP440_TAGS[tag] + "0"
for part, field in v2patterns.PATTERN_PART_FIELDS.items():
field_val = vnfo_kwargs[field]
if field_val is None:
continue
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)
format_fn = v2patterns.PART_FORMATS[part]
kwargs[part] = format_fn(field_val)
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 v2patterns.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
# """Generate version string.
"""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())
>>> import datetime as dt
>>> vinfo = parse_version_info("v200712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]")
>>> 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="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="vYY.BUILD[-TAG]")
'v7.33-beta'
>>> format_version(vinfo_a, pattern="v0Y.BUILD[-TAG]")
'v07.33-beta'
>>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG][NUM]")
'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="vYYYY0M.BUILD[-TAG]")
'v201701.0033-beta'
>>> format_version(vinfo_b, pattern="vYYYY0M.BUILD[-TAG]")
'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="vYYYYwWW.BUILD[-TAG]")
'v2017w00.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="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'
>>> format_version(vinfo_a, pattern="vGGGGwVV.BUILD[-TAG]")
'v2016w52.0033-beta'
# >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final')
>>> 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="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'
>>> 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)
>>> 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[.MICRO[-TAG]]")
'v1.0'
>>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]")
'v1'
"""
kwargs = _format_part_values(vinfo)
part_values = sorted(kwargs.items(), key=lambda item: -len(item[0]))
version = pattern
for part, value in part_values:
version = version.replace(part, value)
return format_tmpl.format(**kwargs)
return version
def incr(
old_version: str,
pattern : str = "{pycalver}",
pattern : str = "vYYYY0M.BUILD[-TAG]",
*,
release: str = None,
major : bool = False,

View file

@ -129,8 +129,25 @@ V2_PART_PATTERN_CASES = [
(['0V'], "53", "53"),
(['0V'], "54", None),
(['MAJOR', 'MINOR', 'PATCH', 'MICRO'], "0", "0"),
# ('TAG', ""),
# ('PYTAG', ""),
(['TAG' ], "alpha" , "alpha"),
(['TAG' ], "alfa" , None),
(['TAG' ], "beta" , "beta"),
(['TAG' ], "dev" , "dev"),
(['TAG' ], "rc" , "rc"),
(['TAG' ], "post" , "post"),
(['TAG' ], "final" , "final"),
(['TAG' ], "latest", None),
(['PYTAG'], "a" , "a"),
(['PYTAG'], "b" , "b"),
(['PYTAG'], "dev" , "dev"),
(['PYTAG'], "rc" , "rc"),
(['PYTAG'], "post" , "post"),
(['PYTAG'], "post" , "post"),
(['PYTAG'], "x" , None),
(['NUM' ], "a" , None),
(['NUM' ], "0" , "0"),
(['NUM' ], "1" , "1"),
(['NUM' ], "10" , "10"),
]
@ -191,16 +208,16 @@ def test_re_pattern_parts(part_name, line, expected):
assert result_val == expected, (part_name, line)
PATTERN_CASES = [
PATTERN_V1_CASES = [
(r"v{year}.{month}.{MINOR}" , "v2017.11.1" , "v2017.11.1"),
(r"v{year}.{month}.{MINOR}" , "v2017.07.12", "v2017.07.12"),
(r"v{year}.{month_short}.{MINOR}", "v2017.11.1" , "v2017.11.1"),
(r"v{year}.{month_short}.{MINOR}", "v2017.7.12" , "v2017.7.12"),
(r"v{year}.{month_short}.{PATCH}", "v2017.11.1" , "v2017.11.1"),
(r"v{year}.{month_short}.{PATCH}", "v2017.7.12" , "v2017.7.12"),
]
@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_CASES)
def test_patterns(pattern_str, line, expected):
@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_V1_CASES)
def test_patterns_v1(pattern_str, line, expected):
pattern = v1patterns.compile_pattern(pattern_str)
result = pattern.regexp.search(line)
if result is None:
@ -210,6 +227,24 @@ def test_patterns(pattern_str, line, expected):
assert result_val == expected, (pattern_str, line)
PATTERN_V2_CASES = [
("vYYYY.0M.MINOR" , "v2017.11.1" , "v2017.11.1"),
("vYYYY.0M.MINOR" , "v2017.07.12", "v2017.07.12"),
("YYYY.MM[.PATCH]", "2017.11.1" , "2017.11.1"),
("YYYY.MM[.PATCH]", "2017.7.12" , "2017.7.12"),
("YYYY.MM[.PATCH]", "2017.7" , "2017.7"),
("YYYY0M.BUILD" , "201707.1000", "201707.1000"),
]
@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_V2_CASES)
def test_patterns_v2(pattern_str, line, expected):
pattern = v2patterns.compile_pattern(pattern_str)
result = pattern.regexp.search(line)
result_val = None if result is None else result.group(0)
assert result_val == expected, (pattern_str, line, pattern.regexp.pattern)
CLI_MAIN_FIXTURE = """
@click.group()
@click.version_option(version="v201812.0123-beta")