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 # Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
# """Compose Regular Expressions from Patterns. """Compose Regular Expressions from Patterns.
# >>> pattern = compile_pattern("vYYYY0M.BUILD[-TAG]") >>> pattern = compile_pattern("vYYYY0M.BUILD[-TAG]")
# >>> version_info = pattern.regexp.match("v201712.0123-alpha") >>> version_info = pattern.regexp.match("v201712.0123-alpha")
# >>> assert version_info == { >>> assert version_info.groupdict() == {
# ... "version": "v201712.0123-alpha", ... "version": "v201712.0123-alpha",
# ... "YYYY" : "2017", ... "year_y" : "2017",
# ... "0M" : "12", ... "month" : "12",
# ... "BUILD" : "0123", ... "bid" : "0123",
# ... "TAG" : "alpha", ... "tag" : "alpha",
# ... } ... }
# >>> >>>
# >>> version_info = pattern.regexp.match("201712.1234") >>> version_info = pattern.regexp.match("201712.1234")
# >>> assert version_info is None >>> assert version_info is None
# >>> version_info = pattern.regexp.match("v201712.1234") >>> version_info = pattern.regexp.match("v201713.1234")
# >>> assert version_info == { >>> assert version_info is None
# ... "version": "v201712.0123-alpha",
# ... "YYYY" : "2017", >>> version_info = pattern.regexp.match("v201712.1234")
# ... "0M" : "12", >>> assert version_info.groupdict() == {
# ... "BUILD" : "0123", ... "version": "v201712.1234",
# ... "TAG" : None, ... "year_y" : "2017",
# ... } ... "month" : "12",
# """ ... "bid" : "1234",
... "tag" : None,
... }
"""
import re import re
import typing as typ 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("), ("(", "\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 = { PART_PATTERNS = {
# Based on calver.org # Based on calver.org
'YYYY': r"[1-9][0-9]{3}", 'YYYY': r"[1-9][0-9]{3}",
'YY' : r"[1-9][0-9]?", 'YY' : r"[1-9][0-9]?",
'0Y' : r"[0-9]{2}", '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}", 'GGGG': r"[1-9][0-9]{3}",
'GG' : r"[1-9][0-9]?", 'GG' : r"[1-9][0-9]?",
'0G' : r"[0-9]{2}", '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 # non calver parts
'MAJOR': r"[0-9]+", 'MAJOR': r"[0-9]+",
'MINOR': r"[0-9]+", 'MINOR': r"[0-9]+",
'PATCH': r"[0-9]+", 'PATCH': r"[0-9]+",
'MICRO': r"[0-9]+", 'MICRO': r"[0-9]+",
'BUILD': r"[0-9]+", 'BUILD': r"[0-9]+",
'TAG' : r"(?:alpha|beta|dev|rc|post|final)", 'TAG' : r"(?:alpha|beta|dev|pre|rc|post|final)",
'PYTAG': r"(?:a|b|dev|rc|post)?[0-9]*", 'PYTAG': r"(?:a|b|dev|rc|post)",
'NUM' : r"[0-9]+",
}
PATTERN_PART_FIELDS = { PATTERN_PART_FIELDS = {
'YYYY' : 'year_y', 'YYYY' : 'year_y',
@ -109,17 +121,118 @@ PATTERN_PART_FIELDS = {
'VV' : 'week_v', 'VV' : 'week_v',
'0V' : '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: def _replace_pattern_parts(pattern: str) -> str:
# 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.
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(): for part_name, part_pattern in PART_PATTERNS.items():
named_part_pattern = f"(?P<{part_name}>{part_pattern})" start_idx = pattern.find(part_name)
placeholder = "\u005c{" + part_name + "\u005c}" if start_idx < 0:
pattern = pattern.replace(placeholder, named_part_pattern) continue
return pattern
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: def compile_pattern_str(pattern: str) -> str:

View file

@ -12,9 +12,11 @@ import datetime as dt
import lexid import lexid
import pkg_resources import pkg_resources
# import pycalver.patterns as v1patterns
import pycalver2.patterns as v2patterns import pycalver2.patterns as v2patterns
# import pycalver.version as v1version
# import pycalver.patterns as v1patterns
logger = logging.getLogger("pycalver.version") logger = logging.getLogger("pycalver.version")
@ -34,11 +36,44 @@ ZERO_VALUES = {
'major': "0", 'major': "0",
'minor': "0", 'minor': "0",
'patch': "0", 'patch': "0",
'TAG' : "final", 'tag' : "final",
'PYTAG': "", '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): class CalendarInfo(typ.NamedTuple):
"""Container for calendar components of version strings.""" """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: 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 = cal_info(dt.date(2019, 1, 5))
# >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) >>> (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) (2019, 1, 1, 5, 5, 0, 0, 1)
# >>> c = cal_info(date(2019, 1, 6)) >>> c = cal_info(dt.date(2019, 1, 6))
# >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) >>> (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) (2019, 1, 1, 6, 6, 0, 1, 1)
# >>> c = cal_info(date(2019, 1, 7)) >>> c = cal_info(dt.date(2019, 1, 7))
# >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) >>> (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) (2019, 1, 1, 7, 7, 1, 1, 2)
# >>> c = cal_info(date(2019, 4, 7)) >>> c = cal_info(dt.date(2019, 4, 7))
# >>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) >>> (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) (2019, 2, 4, 7, 97, 13, 14, 14)
# """ """
if date is None: if date is None:
date = TODAY date = TODAY
@ -118,59 +152,105 @@ def cal_info(date: dt.date = None) -> CalendarInfo:
return CalendarInfo(**kwargs) return CalendarInfo(**kwargs)
MaybeInt = typ.Optional[int]
class VersionInfo(typ.NamedTuple): class VersionInfo(typ.NamedTuple):
"""Container for parsed version string.""" """Container for parsed version string."""
year_y : typ.Optional[int] year_y : MaybeInt
year_g : typ.Optional[int] year_g : MaybeInt
quarter: typ.Optional[int] quarter: MaybeInt
month : typ.Optional[int] month : MaybeInt
dom : typ.Optional[int] dom : MaybeInt
doy : typ.Optional[int] doy : MaybeInt
week_w : typ.Optional[int] week_w : MaybeInt
week_u : typ.Optional[int] week_u : MaybeInt
week_v : typ.Optional[int] week_v : MaybeInt
major : int major : int
minor : int minor : int
patch : int patch : int
bid : str bid : str
tag : str tag : str
pytag : str pytag : str
num : MaybeInt
VALID_FIELD_KEYS = set(VersionInfo._fields) | {'version'}
FieldKey = str FieldKey = str
MatchGroupKey = str MatchGroupKey = str
MatchGroupStr = str MatchGroupStr = str
PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr] PatternGroups = typ.Dict[FieldKey, MatchGroupStr]
FieldValues = 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 fvals = field_values
tag = fvals.get('tag') tag = fvals.get('tag' , "final")
if tag is None: pytag = fvals.get('pytag', "")
tag = "final"
tag = TAG_ALIASES.get(tag, tag) if tag and not pytag:
assert tag is not None pytag = PEP440_TAG_BY_TAG[tag]
# TODO (mb 2020-09-06): parts of course elif pytag and not tag:
pytag = "TODO" tag = TAG_BY_PEP440_TAG[pytag]
bid = fvals['bid'] if 'bid' in fvals else "1001" num: MaybeInt = int(fvals['num']) if 'num' in fvals else None
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 date: typ.Optional[dt.date] = None
month: typ.Optional[int] = None year_y: MaybeInt = int(fvals['year_y']) if 'year_y' in fvals else None
dom : typ.Optional[int] = None year_g: MaybeInt = int(fvals['year_g']) if 'year_g' in fvals else None
week_w: typ.Optional[int] = None month: MaybeInt = int(fvals['month']) if 'month' in fvals else None
week_u: typ.Optional[int] = None doy : MaybeInt = int(fvals['doy' ]) if 'doy' in fvals else None
week_v: typ.Optional[int] = 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: if year_y and doy:
date = _date_from_doy(year_y, 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 month = int(fvals['month']) if 'month' in fvals else None
dom = int(fvals['dom' ]) if 'dom' 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: if year_y and month and dom:
date = dt.date(year_y, month, dom) date = dt.date(year_y, month, dom)
if date: if date:
# derive all fields from other previous values # 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) doy = int(date.strftime("%j"), base=10)
week_w = int(date.strftime("%W"), base=10) week_w = int(date.strftime("%W"), base=10)
week_u = int(date.strftime("%U"), base=10) week_u = int(date.strftime("%U"), base=10)
week_v = int(date.strftime("%V"), 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 major = int(fvals['major']) if 'major' in fvals else 0
minor = int(fvals['minor']) if 'minor' in fvals else 0 minor = int(fvals['minor']) if 'minor' in fvals else 0
patch = int(fvals['patch']) if 'patch' 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_y=year_y,
year_g=year_g, year_g=year_g,
quarter=quarter, quarter=quarter,
@ -215,47 +300,9 @@ def _parse_field_values(field_values: FieldValues) -> VersionInfo:
bid=bid, bid=bid,
tag=tag, tag=tag,
pytag=pytag, pytag=pytag,
num=num,
) )
return vnfo
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",
}
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
@ -265,65 +312,17 @@ class PatternError(Exception):
pass 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: 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]") >>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]")
# >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}) >>> 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") >>> vnfo = parse_version_info("1.23.456", pattern="MAJOR.MINOR.PATCH")
# >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) >>> fvals = {'major': "1", 'minor': "23", 'patch': "456"}
# """ >>> assert vnfo == _parse_version_info(fvals)
"""
pattern_tup = v2patterns.compile_pattern(pattern) pattern_tup = v2patterns.compile_pattern(pattern)
match = pattern_tup.regexp.match(version_str) match = pattern_tup.regexp.match(version_str)
if match is None: 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}'" f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'"
) )
raise PatternError(err_msg) raise PatternError(err_msg)
else:
return _parse_version_info(match.groupdict()) field_values = match.groupdict()
return _parse_version_info(field_values)
def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool: def is_valid(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool:
# TODO reenable doctest """Check if a version matches a pattern.
# """Check if a version matches a pattern.
# >>> is_valid("v201712.0033-beta", pattern="{pycalver}") >>> is_valid("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]")
# True True
# >>> is_valid("v201712.0033-beta", pattern="{semver}") >>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH")
# False False
# >>> is_valid("1.2.3", pattern="{semver}") >>> is_valid("1.2.3", pattern="MAJOR.MINOR.PATCH")
# True True
# >>> is_valid("v201712.0033-beta", pattern="{semver}") >>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH")
# False False
# """ """
try: try:
parse_version_info(version_str, pattern) parse_version_info(version_str, pattern)
return True 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]] 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. """Generate kwargs for template from minimal VersionInfo.
The VersionInfo Tuple only has the minimal representation The VersionInfo Tuple only has the minimal representation
of a parsed version, not the values suitable for formatting. of a parsed version, not the values suitable for formatting.
It may for example have month=9, but not the formatted It may for example have month=9, but not the formatted
representation '09' for '0M'. 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 for part, field in v2patterns.PATTERN_PART_FIELDS.items():
kwargs['TAG'] = tag field_val = vnfo_kwargs[field]
if tag == 'final': if field_val is None:
kwargs['PYTAG'] = "" continue
else:
kwargs['PYTAG'] = PEP440_TAGS[tag] + "0"
year_y = vinfo.year_y format_fn = v2patterns.PART_FORMATS[part]
if year_y: kwargs[part] = format_fn(field_val)
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 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: def format_version(vinfo: VersionInfo, pattern: str) -> str:
# TODO reenable doctest """Generate version string.
# """Generate version string.
# >>> import datetime as dt >>> import datetime as dt
# >>> vinfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") >>> vinfo = parse_version_info("v200712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]")
# >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict()) >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2007, 1, 1))._asdict())
# >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict()) >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2007, 12, 31))._asdict())
# >>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}") >>> format_version(vinfo_a, pattern="vYY.BUILD[-TAG]")
# 'v17.33-beta' 'v7.33-beta'
# >>> format_version(vinfo_a, pattern="vYY.BUILD[-TAG]") >>> format_version(vinfo_a, pattern="v0Y.BUILD[-TAG]")
# 'v17.33-beta' 'v07.33-beta'
# >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG]") >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG][NUM]")
# '201701.33b0' '201701.33b0'
# >>> format_version(vinfo_a, pattern="{pycalver}") >>> format_version(vinfo_a, pattern="vYYYY0M.BUILD[-TAG]")
# 'v201701.0033-beta' 'v201701.0033-beta'
# >>> format_version(vinfo_b, pattern="{pycalver}") >>> format_version(vinfo_b, pattern="vYYYY0M.BUILD[-TAG]")
# 'v201712.0033-beta' 'v201712.0033-beta'
# >>> format_version(vinfo_a, pattern="v{year}w{iso_week}.{BID}{release}") >>> format_version(vinfo_a, pattern="vYYYYwWW.BUILD[-TAG]")
# 'v2017w00.33-beta' 'v2017w00.33-beta'
# >>> format_version(vinfo_a, pattern="vYYYYwWW.BUILD[-TAG]") >>> format_version(vinfo_b, pattern="vYYYYwWW.BUILD[-TAG]")
# 'v2017w00.33-beta' 'v2017w52.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}") >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]")
# 'v2017d001.0033-beta' 'v2017d001.0033-beta'
# >>> format_version(vinfo_b, pattern="v{year}d{doy}.{bid}{release}") >>> format_version(vinfo_b, pattern="vYYYYdJJJ.BUILD[-TAG]")
# 'v2017d365.0033-beta' '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]") >>> format_version(vinfo_a, pattern="vGGGGwVV.BUILD[-TAG]")
# 'v2016w52.0033-beta' '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}") >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG")
# 'v2017w52.33-final' 'v2017w52.33-final'
# >>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}{release}") >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]")
# 'v2017w52.33' '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}") >>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH")
# 'v1.2.34' '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') >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final')
# >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG") >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAGNUM")
# 'v1.0.0-final' 'v1.0.0-final0'
# >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-TAG]") >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG[NUM]")
# 'v1.0.0' 'v1.0.0-final'
# >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]") >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG")
# 'v1.0' 'v1.0.0-final'
# >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.MICRO[-TAG]]") >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-TAG]")
# 'v1.0' 'v1.0.0'
# >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]") >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]")
# 'v1' 'v1.0'
# """ >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.MICRO[-TAG]]")
kwargs = _derive_template_kwargs(vinfo) 'v1.0'
format_tmpl = _compile_format_template(pattern, kwargs) >>> 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( def incr(
old_version: str, old_version: str,
pattern : str = "{pycalver}", pattern : str = "vYYYY0M.BUILD[-TAG]",
*, *,
release: str = None, release: str = None,
major : bool = False, major : bool = False,

View file

@ -129,8 +129,25 @@ V2_PART_PATTERN_CASES = [
(['0V'], "53", "53"), (['0V'], "53", "53"),
(['0V'], "54", None), (['0V'], "54", None),
(['MAJOR', 'MINOR', 'PATCH', 'MICRO'], "0", "0"), (['MAJOR', 'MINOR', 'PATCH', 'MICRO'], "0", "0"),
# ('TAG', ""), (['TAG' ], "alpha" , "alpha"),
# ('PYTAG', ""), (['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) 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.11.1" , "v2017.11.1"),
(r"v{year}.{month}.{MINOR}" , "v2017.07.12", "v2017.07.12"), (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}.{PATCH}", "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.7.12" , "v2017.7.12"),
] ]
@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_CASES) @pytest.mark.parametrize("pattern_str, line, expected", PATTERN_V1_CASES)
def test_patterns(pattern_str, line, expected): def test_patterns_v1(pattern_str, line, expected):
pattern = v1patterns.compile_pattern(pattern_str) pattern = v1patterns.compile_pattern(pattern_str)
result = pattern.regexp.search(line) result = pattern.regexp.search(line)
if result is None: if result is None:
@ -210,6 +227,24 @@ def test_patterns(pattern_str, line, expected):
assert result_val == expected, (pattern_str, line) 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 = """ CLI_MAIN_FIXTURE = """
@click.group() @click.group()
@click.version_option(version="v201812.0123-beta") @click.version_option(version="v201812.0123-beta")