diff --git a/src/pycalver2/patterns.py b/src/pycalver2/patterns.py index 8c3496c..d1a5097 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver2/patterns.py @@ -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" + result_pattern + ")" def compile_pattern_str(pattern: str) -> str: diff --git a/src/pycalver2/version.py b/src/pycalver2/version.py index b715f30..e705293 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver2/version.py @@ -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, diff --git a/test/test_patterns.py b/test/test_patterns.py index 774b66b..6e6d25f 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -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")