From 5bf12a49ce1e5044a7a3311ee8c9b25f9b15a510 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 25 Jul 2019 10:48:23 +0200 Subject: [PATCH 1/4] Add {month_short} part fixes #6 --- src/pycalver/patterns.py | 76 +++++++++++++++++++++------------------- src/pycalver/version.py | 63 +++++++++++++++++---------------- test/test_patterns.py | 19 ++++++++++ 3 files changed, 90 insertions(+), 68 deletions(-) diff --git a/src/pycalver/patterns.py b/src/pycalver/patterns.py index f3c6f49..5aca46d 100644 --- a/src/pycalver/patterns.py +++ b/src/pycalver/patterns.py @@ -85,37 +85,38 @@ COMPOSITE_PART_PATTERNS = { PART_PATTERNS = { - 'year' : r"\d{4}", - 'month' : r"(?:0[0-9]|1[0-2])", - 'build_no' : r"\d{4,}", - 'pep440_tag': r"(?:a|b|dev|rc|post)?\d*", - 'tag' : r"(?:alpha|beta|dev|rc|post|final)", - 'yy' : r"\d{2}", - 'yyyy' : r"\d{4}", - 'quarter' : r"[1-4]", - 'iso_week' : r"(?:[0-4]\d|5[0-3])", - 'us_week' : r"(?:[0-4]\d|5[0-3])", - 'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])", - 'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", - 'MAJOR' : r"\d+", - 'MINOR' : r"\d+", - 'MM' : r"\d{2,}", - 'MMM' : r"\d{3,}", - 'MMMM' : r"\d{4,}", - 'MMMMM' : r"\d{5,}", - 'PATCH' : r"\d+", - 'PP' : r"\d{2,}", - 'PPP' : r"\d{3,}", - 'PPPP' : r"\d{4,}", - 'PPPPP' : r"\d{5,}", - 'bid' : r"\d{4,}", - 'BID' : r"[1-9]\d*", - 'BB' : r"[1-9]\d{1,}", - 'BBB' : r"[1-9]\d{2,}", - 'BBBB' : r"[1-9]\d{3,}", - 'BBBBB' : r"[1-9]\d{4,}", - 'BBBBBB' : r"[1-9]\d{5,}", - 'BBBBBBB' : r"[1-9]\d{6,}", + 'year' : r"\d{4}", + 'month' : r"(?:0[0-9]|1[0-2])", + 'month_short': r"(?:1[0-2]|[1-9])", + 'build_no' : r"\d{4,}", + 'pep440_tag' : r"(?:a|b|dev|rc|post)?\d*", + 'tag' : r"(?:alpha|beta|dev|rc|post|final)", + 'yy' : r"\d{2}", + 'yyyy' : r"\d{4}", + 'quarter' : r"[1-4]", + 'iso_week' : r"(?:[0-4]\d|5[0-3])", + 'us_week' : r"(?:[0-4]\d|5[0-3])", + 'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])", + 'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", + 'MAJOR' : r"\d+", + 'MINOR' : r"\d+", + 'MM' : r"\d{2,}", + 'MMM' : r"\d{3,}", + 'MMMM' : r"\d{4,}", + 'MMMMM' : r"\d{5,}", + 'PATCH' : r"\d+", + 'PP' : r"\d{2,}", + 'PPP' : r"\d{3,}", + 'PPPP' : r"\d{4,}", + 'PPPPP' : r"\d{5,}", + 'bid' : r"\d{4,}", + 'BID' : r"[1-9]\d*", + 'BB' : r"[1-9]\d{1,}", + 'BBB' : r"[1-9]\d{2,}", + 'BBBB' : r"[1-9]\d{3,}", + 'BBBBB' : r"[1-9]\d{4,}", + 'BBBBBB' : r"[1-9]\d{5,}", + 'BBBBBBB' : r"[1-9]\d{6,}", } @@ -129,12 +130,13 @@ FULL_PART_FORMATS = { # NOTE (mb 2019-01-04): since release is optional, it # is treates specially in version.format # 'release' : "-{tag}", - 'month' : "{month:02}", - 'build_no': "{bid}", - 'iso_week': "{iso_week:02}", - 'us_week' : "{us_week:02}", - 'dom' : "{dom:02}", - 'doy' : "{doy:03}", + 'month' : "{month:02}", + 'month_short': "{month}", + 'build_no' : "{bid}", + 'iso_week' : "{iso_week:02}", + 'us_week' : "{us_week:02}", + 'dom' : "{dom:02}", + 'doy' : "{doy:03}", # depricated 'pep440_version': "{year}{month:02}.{BID}{pep440_tag}", 'version' : "v{year}{month:02}.{bid}{release}", diff --git a/src/pycalver/version.py b/src/pycalver/version.py index 76c9a3c..5bdad87 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -21,37 +21,38 @@ TODAY = dt.datetime.utcnow().date() PATTERN_PART_FIELDS = { - 'year' : 'year', - 'month' : 'month', - 'pep440_tag': 'tag', - 'tag' : 'tag', - 'yy' : 'year', - 'yyyy' : 'year', - 'quarter' : 'quarter', - 'iso_week' : 'iso_week', - 'us_week' : 'us_week', - 'dom' : 'dom', - 'doy' : 'doy', - 'MAJOR' : 'major', - 'MINOR' : 'minor', - 'MM' : 'minor', - 'MMM' : 'minor', - 'MMMM' : 'minor', - 'MMMMM' : 'minor', - 'PP' : 'patch', - 'PPP' : 'patch', - 'PPPP' : 'patch', - 'PPPPP' : 'patch', - 'PATCH' : 'patch', - 'build_no' : 'bid', - 'bid' : 'bid', - 'BID' : 'bid', - 'BB' : 'bid', - 'BBB' : 'bid', - 'BBBB' : 'bid', - 'BBBBB' : 'bid', - 'BBBBBB' : 'bid', - 'BBBBBBB' : 'bid', + 'year' : 'year', + 'month' : 'month', + 'month_short': 'month', + 'pep440_tag' : 'tag', + 'tag' : 'tag', + 'yy' : 'year', + 'yyyy' : 'year', + 'quarter' : 'quarter', + 'iso_week' : 'iso_week', + 'us_week' : 'us_week', + 'dom' : 'dom', + 'doy' : 'doy', + 'MAJOR' : 'major', + 'MINOR' : 'minor', + 'MM' : 'minor', + 'MMM' : 'minor', + 'MMMM' : 'minor', + 'MMMMM' : 'minor', + 'PP' : 'patch', + 'PPP' : 'patch', + 'PPPP' : 'patch', + 'PPPPP' : 'patch', + 'PATCH' : 'patch', + 'build_no' : 'bid', + 'bid' : 'bid', + 'BID' : 'bid', + 'BB' : 'bid', + 'BBB' : 'bid', + 'BBBB' : 'bid', + 'BBBBB' : 'bid', + 'BBBBBB' : 'bid', + 'BBBBBBB' : 'bid', } diff --git a/test/test_patterns.py b/test/test_patterns.py index dfb31d4..df51aa3 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -53,6 +53,25 @@ def test_re_pattern_parts(part_name, line, expected): assert result_val == expected, (part_name, line) +PATTERN_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"), +] + + +@pytest.mark.parametrize("pattern_str, line, expected", PATTERN_CASES) +def test_patterns(pattern_str, line, expected): + pattern_re = patterns.compile_pattern(pattern_str) + result = pattern_re.search(line) + if result is None: + assert expected is None, (pattern_str, line) + else: + result_val = result.group(0) + assert result_val == expected, (pattern_str, line) + + CLI_MAIN_FIXTURE = """ @click.group() @click.version_option(version="v201812.0123-beta") From 43fe46cd1b2c59d6dff260b9b85b347d292b9524 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 25 Jul 2019 10:48:32 +0200 Subject: [PATCH 2/4] update changelog --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15522ec..9938026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,10 @@ ## v201907.003x - - Fix #5: Better warning when using bump with semver (one of --major/--minor/--patch is required) - - Fix #4: Make {release} part optional, so that versions generated by --release=final are parsed. + + - Fix gitlab#6: Add {month_short} as a minimal fix. + - Fix gitlab#5: Better warning when using bump with semver (one of --major/--minor/--patch is required) + - Fix gitlab#4: Make {release} part optional, so that versions generated by --release=final are parsed. ## v201903.0030 @@ -44,7 +46,7 @@ ## v201812.0017 - - Fixed #2 on github. `pycalver init` was broken. + - Fixed github#2. `pycalver init` was broken. - Fixed pattern escaping issues. - Added lots more tests for cli. - Cleaned up documentation. From 8833bb8b2b14ff45087703cf8acde47b5e2b8847 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 25 Jul 2019 10:55:31 +0200 Subject: [PATCH 3/4] more consistent paramter order --- src/pycalver/cli.py | 2 +- src/pycalver/rewrite.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pycalver/cli.py b/src/pycalver/cli.py index e09b905..1bec882 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver/cli.py @@ -228,7 +228,7 @@ def _bump(cfg: config.Config, new_version: str, allow_dirty: bool = False) -> No try: new_vinfo = version.parse_version_info(new_version, cfg.version_pattern) - rewrite.rewrite(new_vinfo, cfg.file_patterns) + rewrite.rewrite(cfg.file_patterns, new_vinfo) except Exception as ex: log.error(str(ex)) sys.exit(1) diff --git a/src/pycalver/rewrite.py b/src/pycalver/rewrite.py index f7f28dd..fc60b82 100644 --- a/src/pycalver/rewrite.py +++ b/src/pycalver/rewrite.py @@ -226,7 +226,7 @@ def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) - return full_diff -def rewrite(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -> None: +def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo) -> None: """Rewrite project files, updating each with the new version.""" fh: typ.IO[str] From 052d01445c96459c3df1731e29292b88a6490ebb Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 25 Jul 2019 12:17:52 +0200 Subject: [PATCH 4/4] Add dom_short, doy_short and regression tests --- CHANGELOG.md | 2 +- src/pycalver/patterns.py | 4 ++ src/pycalver/version.py | 141 ++++++++++++++++++++++----------------- test/test_version.py | 77 +++++++++++++++------ 4 files changed, 141 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9938026..3c0af81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## v201907.003x - - Fix gitlab#6: Add {month_short} as a minimal fix. + - Fix gitlab#6: Add parts `{month_short}`, `{dom_short}`, `{doy_short}`. - Fix gitlab#5: Better warning when using bump with semver (one of --major/--minor/--patch is required) - Fix gitlab#4: Make {release} part optional, so that versions generated by --release=final are parsed. diff --git a/src/pycalver/patterns.py b/src/pycalver/patterns.py index 5aca46d..7274e07 100644 --- a/src/pycalver/patterns.py +++ b/src/pycalver/patterns.py @@ -97,7 +97,9 @@ PART_PATTERNS = { 'iso_week' : r"(?:[0-4]\d|5[0-3])", 'us_week' : r"(?:[0-4]\d|5[0-3])", 'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])", + 'dom_short' : r"([1-9]|[1-2][0-9]|3[0-1])", 'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", + 'doy_short' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])", 'MAJOR' : r"\d+", 'MINOR' : r"\d+", 'MM' : r"\d{2,}", @@ -137,6 +139,8 @@ FULL_PART_FORMATS = { 'us_week' : "{us_week:02}", 'dom' : "{dom:02}", 'doy' : "{doy:03}", + 'dom_short' : "{dom}", + 'doy_short' : "{doy}", # depricated 'pep440_version': "{year}{month:02}.{BID}{pep440_tag}", 'version' : "v{year}{month:02}.{bid}{release}", diff --git a/src/pycalver/version.py b/src/pycalver/version.py index 5bdad87..84fe173 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -33,6 +33,8 @@ PATTERN_PART_FIELDS = { 'us_week' : 'us_week', 'dom' : 'dom', 'doy' : 'doy', + 'dom_short' : 'dom', + 'doy_short' : 'doy', 'MAJOR' : 'major', 'MINOR' : 'minor', 'MM' : 'minor', @@ -147,6 +149,74 @@ class VersionInfo(typ.NamedTuple): tag : str +FieldKey = str +MatchGroupKey = str +MatchGroupStr = str + +PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr] +FieldValues = typ.Dict[FieldKey , MatchGroupStr] + + +def _parse_field_values(field_values: FieldValues) -> VersionInfo: + fv = field_values + tag = fv.get('tag') + if tag is None: + tag = "final" + tag = TAG_ALIASES.get(tag, tag) + assert tag is not None + + bid = fv['bid'] if 'bid' in fv else "0001" + + year = int(fv['year']) if 'year' in fv else None + doy = int(fv['doy' ]) if 'doy' in fv else None + + month: typ.Optional[int] + dom : typ.Optional[int] + + if year and doy: + date = _date_from_doy(year, doy) + month = date.month + dom = date.day + else: + month = int(fv['month']) if 'month' in fv else None + dom = int(fv['dom' ]) if 'dom' in fv else None + + iso_week: typ.Optional[int] + us_week : typ.Optional[int] + + if year and month and dom: + date = dt.date(year, month, dom) + doy = int(date.strftime("%j"), base=10) + iso_week = int(date.strftime("%W"), base=10) + us_week = int(date.strftime("%U"), base=10) + else: + iso_week = None + us_week = None + + quarter = int(fv['quarter']) if 'quarter' in fv else None + if quarter is None and month: + quarter = _quarter_from_month(month) + + major = int(fv['major']) if 'major' in fv else 0 + minor = int(fv['minor']) if 'minor' in fv else 0 + patch = int(fv['patch']) if 'patch' in fv else 0 + + return VersionInfo( + year=year, + quarter=quarter, + month=month, + dom=dom, + doy=doy, + iso_week=iso_week, + us_week=us_week, + major=major, + minor=minor, + patch=patch, + bid=bid, + tag=tag, + ) + + def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool: """Check pattern for any calendar based parts. @@ -189,7 +259,7 @@ class PatternError(Exception): pass -def _parse_pattern_groups(pattern_groups: typ.Dict[str, str]) -> typ.Dict[str, str]: +def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: for part_name in pattern_groups.keys(): is_valid_part_name = ( part_name in patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS @@ -198,12 +268,13 @@ def _parse_pattern_groups(pattern_groups: typ.Dict[str, str]) -> typ.Dict[str, s err_msg = f"Invalid part '{part_name}'" raise PatternError(err_msg) - items = [ + 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 items] + + 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] @@ -211,10 +282,10 @@ def _parse_pattern_groups(pattern_groups: typ.Dict[str, str]) -> typ.Dict[str, s err_msg = f"Multiple parts for same field {duplicate_fields}." raise PatternError(err_msg) - return dict(items) + return dict(field_value_items) -def _parse_version_info(pattern_groups: typ.Dict[str, str]) -> VersionInfo: +def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo: """Parse normalized VersionInfo from groups of a matched pattern. >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"}) @@ -233,64 +304,8 @@ def _parse_version_info(pattern_groups: typ.Dict[str, str]) -> VersionInfo: >>> (vnfo.major, vnfo.minor, vnfo.patch) (1, 23, 45) """ - kw = _parse_pattern_groups(pattern_groups) - - tag = kw.get('tag') - if tag is None: - tag = "final" - tag = TAG_ALIASES.get(tag, tag) - assert tag is not None - - bid = kw['bid'] if 'bid' in kw else "0001" - - year = int(kw['year']) if 'year' in kw else None - doy = int(kw['doy' ]) if 'doy' in kw else None - - month: typ.Optional[int] - dom : typ.Optional[int] - - if year and doy: - date = _date_from_doy(year, doy) - month = date.month - dom = date.day - else: - month = int(kw['month']) if 'month' in kw else None - dom = int(kw['dom' ]) if 'dom' in kw else None - - iso_week: typ.Optional[int] - us_week : typ.Optional[int] - - if year and month and dom: - date = dt.date(year, month, dom) - doy = int(date.strftime("%j"), base=10) - iso_week = int(date.strftime("%W"), base=10) - us_week = int(date.strftime("%U"), base=10) - else: - iso_week = None - us_week = None - - quarter = int(kw['quarter']) if 'quarter' in kw else None - if quarter is None and month: - quarter = _quarter_from_month(month) - - major = int(kw['major']) if 'major' in kw else 0 - minor = int(kw['minor']) if 'minor' in kw else 0 - patch = int(kw['patch']) if 'patch' in kw else 0 - - return VersionInfo( - year=year, - quarter=quarter, - month=month, - dom=dom, - doy=doy, - iso_week=iso_week, - us_week=us_week, - major=major, - minor=minor, - patch=patch, - bid=bid, - tag=tag, - ) + field_values = _parse_pattern_groups(pattern_groups) + return _parse_field_values(field_values) def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> VersionInfo: diff --git a/test/test_version.py b/test/test_version.py index 3da513d..b851f0e 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -1,6 +1,8 @@ import random import datetime as dt +import pytest + from pycalver import version from pycalver import patterns @@ -50,25 +52,25 @@ def test_bump_random(monkeypatch): def test_parse_version_info(): - version_str = "v201712.0001-alpha" - version_nfo = version.parse_version_info(version_str) + version_str = "v201712.0001-alpha" + version_info = version.parse_version_info(version_str) - # assert version_nfo.pep440_version == "201712.1a0" - # assert version_nfo.version == "v201712.0001-alpha" - assert version_nfo.year == 2017 - assert version_nfo.month == 12 - assert version_nfo.bid == "0001" - assert version_nfo.tag == "alpha" + # assert version_info.pep440_version == "201712.1a0" + # assert version_info.version == "v201712.0001-alpha" + assert version_info.year == 2017 + assert version_info.month == 12 + assert version_info.bid == "0001" + assert version_info.tag == "alpha" - version_str = "v201712.0001" - version_nfo = version.parse_version_info(version_str) + version_str = "v201712.0001" + version_info = version.parse_version_info(version_str) - # assert version_nfo.pep440_version == "201712.1" - # assert version_nfo.version == "v201712.0001" - assert version_nfo.year == 2017 - assert version_nfo.month == 12 - assert version_nfo.bid == "0001" - assert version_nfo.tag == "final" + # assert version_info.pep440_version == "201712.1" + # assert version_info.version == "v201712.0001" + assert version_info.year == 2017 + assert version_info.month == 12 + assert version_info.bid == "0001" + assert version_info.tag == "final" def test_readme_pycalver1(): @@ -108,7 +110,7 @@ def test_parse_error_empty(): version.parse_version_info("") assert False except version.PatternError as err: - pass + assert "Invalid version string" in str(err) def test_parse_error_noprefix(): @@ -116,7 +118,7 @@ def test_parse_error_noprefix(): version.parse_version_info("201809.0002") assert False except version.PatternError as err: - pass + assert "Invalid version string" in str(err) def test_parse_error_nopadding(): @@ -124,7 +126,7 @@ def test_parse_error_nopadding(): version.parse_version_info("v201809.2b0") assert False except version.PatternError as err: - pass + assert "Invalid version string" in str(err) def test_part_field_mapping(): @@ -141,3 +143,40 @@ def test_part_field_mapping(): b_fields = set(version.VersionInfo._fields) assert a_fields == b_fields + + +def vnfo(**field_values): + return version._parse_field_values(field_values) + + +PARSE_VERSION_TEST_CASES = [ + ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], + ["{year}.{month}.{dom_short}" , "2017.06.7" , vnfo(year="2017", month="06", dom="7" )], + ["{year}.{month}.{dom_short}" , "2017.06.7" , vnfo(year="2017", month="06", dom="7" )], + ["{year}.{month_short}.{dom_short}", "2017.6.7" , vnfo(year="2017", month="6" , dom="7" )], + ["{year}.{month}.{dom}" , "2017.6.07" , None], + ["{year}.{month}.{dom}" , "2017.06.7" , None], + ["{year}.{month_short}.{dom}" , "2017.06.7" , None], + ["{year}.{month}.{dom_short}" , "2017.6.07" , None], + ["{year}.{month_short}.{MINOR}" , "2017.6.7" , vnfo(year="2017", month="6" , minor="7" )], + ["{year}.{month}.{MINOR}" , "2017.06.7" , vnfo(year="2017", month="06", minor="7" )], + ["{year}.{month}.{MINOR}" , "2017.06.07", vnfo(year="2017", month="06", minor="07")], + ["{year}.{month}.{MINOR}" , "2017.6.7" , None], +] + + +@pytest.mark.parametrize("pattern_str, line, expected_vinfo", PARSE_VERSION_TEST_CASES) +def test_parse_versions(pattern_str, line, expected_vinfo): + pattern_re = patterns.compile_pattern(pattern_str) + version_match = pattern_re.search(line) + + if expected_vinfo is None: + assert version_match is None + return + + assert version_match is not None + + version_str = version_match.group(0) + version_info = version.parse_version_info(version_str, pattern_str) + + assert version_info == expected_vinfo