diff --git a/setup.cfg b/setup.cfg index 4e9f261..b7ba17d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -131,7 +131,7 @@ jobs = 4 output-format = colorized # Maximum number of locals for function / method body -max-locals = 25 +max-locals = 20 # Maximum number of arguments for function / method max-args = 12 diff --git a/src/pycalver/cli.py b/src/pycalver/cli.py index f4c8793..f12186c 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver/cli.py @@ -100,6 +100,52 @@ def _validate_release_tag(tag: typ.Optional[str]) -> None: sys.exit(1) +def _validate_flags( + raw_pattern: str, + major : bool, + minor : bool, + patch : bool, +) -> None: + if "{" in raw_pattern and "}" in raw_pattern: + # only validate for new style patterns + return + + valid = True + if major and "MAJOR" not in raw_pattern: + logger.error(f"Flag --major is not applicable to pattern '{raw_pattern}'") + valid = False + if minor and "MINOR" not in raw_pattern: + logger.error(f"Flag --minor is not applicable to pattern '{raw_pattern}'") + valid = False + if patch and "PATCH" not in raw_pattern: + logger.error(f"Flag --patch is not applicable to pattern '{raw_pattern}'") + valid = False + + if not valid: + sys.exit(1) + + +def _log_no_change(subcmd: str, version_pattern: str, old_version: str) -> None: + msg = ( + f"Version did not change: '{old_version}'. " + f"Invalid version and/or pattern '{version_pattern}'." + ) + logger.error(msg) + + is_semver = "{semver}" in version_pattern or ( + "MAJOR" in version_pattern and "MAJOR" in version_pattern and "PATCH" in version_pattern + ) + if is_semver: + logger.warning(f"pycalver {subcmd} [--major/--minor/--patch] required for use with SemVer.") + else: + available_flags = [ + "--" + part.lower() for part in ['MAJOR', 'MINOR', 'PATCH'] if part in version_pattern + ] + if available_flags: + available_flags_str = "/".join(available_flags) + logger.info(f"Perhaps try: pycalver {subcmd} {available_flags_str} ") + + @click.group() @click.version_option(version="v202010.1041-beta") @click.help_option() @@ -151,6 +197,7 @@ def test( tag = release # use internal naming convention _validate_release_tag(tag) + _validate_flags(raw_pattern, major, minor, patch) _date = _validate_date(date, pin_date) new_version = incr_dispatch( @@ -165,7 +212,7 @@ def test( date=_date, ) if new_version is None: - logger.error(f"Invalid version '{old_version}' and/or pattern '{raw_pattern}'.") + _log_no_change('test', raw_pattern, old_version) sys.exit(1) pep440_version = version.to_pep440(new_version) @@ -395,7 +442,7 @@ def incr_dispatch( logger.info("regex = " + regexfmt.pyexpr_regex(pattern.regexp.pattern)) if has_v1_part: - return v1version.incr( + new_version = v1version.incr( old_version, raw_pattern=raw_pattern, tag=tag, @@ -407,7 +454,7 @@ def incr_dispatch( date=date, ) else: - return v2version.incr( + new_version = v2version.incr( old_version, raw_pattern=raw_pattern, tag=tag, @@ -419,6 +466,15 @@ def incr_dispatch( date=date, ) + if new_version is None: + return None + elif pkg_resources.parse_version(new_version) <= pkg_resources.parse_version(old_version): + logger.error("Invariant violated: New version must be greater than old version ") + logger.error(f" Result: '{new_version}' > '{old_version}' -> False") + return None + else: + return new_version + def _bump( cfg : config.Config, @@ -625,16 +681,7 @@ def bump( ) if new_version is None: - is_semver = "{semver}" in cfg.version_pattern or ( - "MAJOR" in cfg.version_pattern - and "MAJOR" in cfg.version_pattern - and "PATCH" in cfg.version_pattern - ) - has_semver_inc = major or minor or patch - if is_semver and not has_semver_inc: - logger.warning("bump --major/--minor/--patch required when using semver.") - else: - logger.error(f"Invalid version '{old_version}' and/or pattern '{cfg.version_pattern}'.") + _log_no_change('bump', cfg.version_pattern, old_version) sys.exit(1) logger.info(f"Old Version: {old_version}") diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index f3a85cd..bf41395 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -102,65 +102,26 @@ FieldValues = typ.Dict[FieldKey, MatchGroupStr] VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] -def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: - """Parse normalized V2VersionInfo from groups of a matched pattern. +def _parse_calendar_info(field_values: FieldValues) -> version.V2CalendarInfo: + """Parse normalized V2CalendarInfo from groups of a matched pattern. - >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "11", 'bid': "0099"}) - >>> (vinfo.year_y, vinfo.month, vinfo.quarter, vinfo.bid, vinfo.tag) - (2018, 11, 4, '0099', 'final') - - >>> vinfo = _parse_version_info({'year_y': "18", 'month': "11"}) - >>> (vinfo.year_y, vinfo.month, vinfo.quarter) - (2018, 11, 4) - - >>> vinfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"}) - >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy, vinfo.bid, vinfo.tag) - (2018, 1, 11, 11, '099', 'beta') - - >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"}) - >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy) - (2018, 6, 15, 166) - - >>> vinfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"}) - >>> (vinfo.major, vinfo.minor, vinfo.patch) - (1, 23, 45) - - >>> vinfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"}) - >>> (vinfo.major, vinfo.minor, vinfo.patch) - (1, 23, 45) - - >>> vinfo = _parse_version_info({'year_y': "2021", 'week_w': "02"}) - >>> (vinfo.year_y, vinfo.week_w) + >>> cinfo = _parse_version_info({'year_y': "2021", 'week_w': "02"}) + >>> (cinfo.year_y, cinfo.week_w) (2021, 2) - >>> vinfo = _parse_version_info({'year_y': "2021", 'week_u': "02"}) - >>> (vinfo.year_y, vinfo.week_u) + >>> cinfo = _parse_version_info({'year_y': "2021", 'week_u': "02"}) + >>> (cinfo.year_y, cinfo.week_u) (2021, 2) - >>> vinfo = _parse_version_info({'year_g': "2021", 'week_v': "02"}) - >>> (vinfo.year_g, vinfo.week_v) + >>> cinfo = _parse_version_info({'year_g': "2021", 'week_v': "02"}) + >>> (cinfo.year_g, cinfo.week_v) (2021, 2) - >>> vinfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) - >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.tag) - (2021, 1, 3, 'final') - >>> (vinfo.year_y, vinfo.week_w, vinfo.year_y, vinfo.week_u,vinfo.year_g, vinfo.week_v) + >>> cinfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) + >>> (cinfo.year_y, cinfo.month, cinfo.dom) + (2021, 1, 3) + >>> (cinfo.year_y, cinfo.week_w, cinfo.year_y, cinfo.week_u,cinfo.year_g, cinfo.week_v) (2021, 0, 2021, 1, 2020, 53) """ - # pylint:disable=dangerous-default-value; We don't mutate args, mypy would fail if we did. - for key in field_values: - assert key in VALID_FIELD_KEYS, key - fvals = field_values - tag = fvals.get('tag' ) or "" - pytag = fvals.get('pytag') or "" - - if tag and not pytag: - pytag = version.PEP440_TAG_BY_RELEASE[tag] - elif pytag and not tag: - tag = version.RELEASE_BY_PEP440_TAG[pytag] - - if not tag: - tag = "final" - date: typ.Optional[dt.date] = None year_y: MaybeInt = int(fvals['year_y']) if 'year_y' in fvals else None @@ -205,16 +166,7 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: if quarter is None and month: quarter = version.quarter_from_month(month) - # NOTE (mb 2020-09-18): If a part is optional, fvals[] may be None - major = int(fvals.get('major') or 0) - minor = int(fvals.get('minor') or 0) - patch = int(fvals.get('patch') or 0) - num = int(fvals.get('num' ) or 0) - bid = fvals['bid'] if 'bid' in fvals else "1000" - inc0 = int(fvals.get('inc0') or 0) - inc1 = int(fvals.get('inc1') or 1) - - vinfo = version.V2VersionInfo( + return version.V2CalendarInfo( year_y=year_y, year_g=year_g, quarter=quarter, @@ -224,6 +176,74 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: week_w=week_w, week_u=week_u, week_v=week_v, + ) + + +def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: + """Parse normalized V2VersionInfo from groups of a matched pattern. + + >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "11", 'bid': "0099"}) + >>> (vinfo.year_y, vinfo.month, vinfo.quarter, vinfo.bid, vinfo.tag) + (2018, 11, 4, '0099', 'final') + + >>> vinfo = _parse_version_info({'year_y': "18", 'month': "11"}) + >>> (vinfo.year_y, vinfo.month, vinfo.quarter) + (2018, 11, 4) + + >>> vinfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"}) + >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy, vinfo.bid, vinfo.tag) + (2018, 1, 11, 11, '099', 'beta') + + >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"}) + >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy) + (2018, 6, 15, 166) + + >>> vinfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"}) + >>> (vinfo.major, vinfo.minor, vinfo.patch) + (1, 23, 45) + + >>> vinfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"}) + >>> (vinfo.major, vinfo.minor, vinfo.patch, vinfo.tag) + (1, 23, 45, 'final') + """ + # pylint:disable=dangerous-default-value; We don't mutate args, mypy would fail if we did. + for key in field_values: + assert key in VALID_FIELD_KEYS, key + + cinfo = _parse_calendar_info(field_values) + + fvals = field_values + + tag = fvals.get('tag' ) or "" + pytag = fvals.get('pytag') or "" + + if tag and not pytag: + pytag = version.PEP440_TAG_BY_RELEASE[tag] + elif pytag and not tag: + tag = version.RELEASE_BY_PEP440_TAG[pytag] + + if not tag: + tag = "final" + + # NOTE (mb 2020-09-18): If a part is optional, fvals[] may be None + major = int(fvals.get('major') or 0) + minor = int(fvals.get('minor') or 0) + patch = int(fvals.get('patch') or 0) + num = int(fvals.get('num' ) or 0) + bid = fvals['bid'] if 'bid' in fvals else "1000" + inc0 = int(fvals.get('inc0') or 0) + inc1 = int(fvals.get('inc1') or 1) + + return version.V2VersionInfo( + year_y=cinfo.year_y, + year_g=cinfo.year_g, + quarter=cinfo.quarter, + month=cinfo.month, + dom=cinfo.dom, + doy=cinfo.doy, + week_w=cinfo.week_w, + week_u=cinfo.week_u, + week_v=cinfo.week_v, major=major, minor=minor, patch=patch, @@ -234,7 +254,6 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: inc0=inc0, inc1=inc1, ) - return vinfo def parse_version_info( diff --git a/src/pycalver/version.py b/src/pycalver/version.py index afa4a20..b83e443 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -126,8 +126,6 @@ V2_FIELD_INITIAL_VALUES = { 'major': "0", 'minor': "0", 'patch': "0", - 'tag' : "final", - 'pytag': "", 'num' : "0", 'inc0' : "0", 'inc1' : "1", diff --git a/test/test_cli.py b/test/test_cli.py index 698fbe9..30b8527 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -566,7 +566,7 @@ def test_v1_bump_semver_warning(runner, caplog, version_pattern): assert result.exit_code == 1 assert any("version did not change" in r.message for r in caplog.records) - assert any("--major/--minor/--patch required" in r.message for r in caplog.records) + assert any("[--major/--minor/--patch] required" in r.message for r in caplog.records) result = runner.invoke(cli.cli, ['bump', "-vv", "-n", "--dry", "--patch"]) assert result.exit_code == 0