diff --git a/CHANGELOG.md b/CHANGELOG.md index abdcaa6..c2f31a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ - Better support for optional parts. - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. - - New: enable globs for filenames in `pycalver:file_patterns` - Fix gitlab #8: Push tags only pushed tags, not actual commit. - Fix gitlab #9: Make commit message configurable. diff --git a/pylint-ignore.md b/pylint-ignore.md index 789183b..3d65cdb 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -23,7 +23,8 @@ The recommended approach to using `pylint-ignore` is: # Overview - - [W0511: fixme (7x)](#w0511-fixme) + - [E1123: unexpected-keyword-arg (1x)](#e1123-unexpected-keyword-arg) + - [W0511: fixme (9x)](#w0511-fixme) - [W0703: broad-except (1x)](#w0703-broad-except) @@ -36,7 +37,7 @@ The recommended approach to using `pylint-ignore` is: - `date : 2020-09-18T17:01:05` ``` - 12: import pycalver2.patterns as v2patterns + 12: from pycalver import v2patterns 13: > 14: # TODO (mb 2020-09-06): test for v2patterns 15: @@ -61,107 +62,138 @@ The recommended approach to using `pylint-ignore` is: ``` -## File test/test_version.py - Line 167 - W0511 (fixme) +## File test/test_config.py - Line 156 - W0511 (fixme) + +- `message: TODO (mb 2020-09-18):` +- `author : Manuel Barkhau ` +- `date : 2020-09-18T19:04:06` + +``` + 143: def test_parse_v2_cfg(): + ... + 154: assert "setup.py" in cfg.file_patterns + 155: assert "setup.cfg" in cfg.file_patterns +> 156: # TODO (mb 2020-09-18): + 157: # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"] + 158: # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-TAG]"'] +``` + + +## File test/test_version.py - Line 168 - W0511 (fixme) - `message: TODO (mb 2020-09-06): add tests for new style patterns` - `author : Manuel Barkhau ` - `date : 2020-09-18T17:01:05` ``` - 162: def vnfo(**field_values): + 163: def vnfo(**field_values): ... - 165: - 166: PARSE_VERSION_TEST_CASES = [ -> 167: # TODO (mb 2020-09-06): add tests for new style patterns - 168: # ["YYYY.MM.DD" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], - 169: ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], + 166: + 167: PARSE_VERSION_TEST_CASES = [ +> 168: # TODO (mb 2020-09-06): add tests for new style patterns + 169: # ["YYYY.MM.DD" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], + 170: ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")], ``` -## File src/pycalver/__main__.py - Line 229 - W0511 (fixme) +## File src/pycalver/v1patterns.py - Line 212 - W0511 (fixme) -- `message: TODO (mb 2020-09-05): version switch` +- `message: TODO (mb 2020-09-19): replace {version} etc with version_pattern` - `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` +- `date : 2020-09-19T16:24:10` ``` - 209: def _bump( + 199: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: ... - 227: - 228: try: -> 229: # TODO (mb 2020-09-05): version switch - 230: v1cli.rewrite(cfg, new_version) - 231: # v2cli.rewrite(cfg, new_version) + 210: escaped_pattern = escaped_pattern.replace(char, escaped) + 211: +> 212: # TODO (mb 2020-09-19): replace {version} etc with version_pattern + 213: pattern_str = _replace_pattern_parts(escaped_pattern) + 214: return re.compile(pattern_str) ``` -## File src/pycalver/config.py - Line 236 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 247 - W0511 (fixme) -- `message: TODO (mb 2020-09-06): new style pattern by default` +- `message: TODO (mb 2020-09-18): Investigate error messages` - `author : Manuel Barkhau ` -- `date : 2020-09-18T17:01:05` +- `date : 2020-09-19T16:24:10` ``` - 227: def _parse_config(raw_cfg: RawConfig) -> Config: + 219: def _bump( ... - 234: version_str = raw_cfg['current_version'] = version_str.strip("'\" ") - 235: -> 236: # TODO (mb 2020-09-06): new style pattern by default - 237: # version_pattern: str = raw_cfg.get('version_pattern', "vYYYY0M.BUILD[-TAG]") - 238: version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}") + 245: sys.exit(1) + 246: except Exception as ex: +> 247: # TODO (mb 2020-09-18): Investigate error messages + 248: logger.error(str(ex)) + 249: sys.exit(1) ``` -## File src/pycalver/__main__.py - Line 285 - W0511 (fixme) +## File src/pycalver/v2patterns.py - Line 256 - W0511 (fixme) -- `message: TODO (mb 2020-09-05): version switch` +- `message: TODO (mb 2020-09-19): replace {version} etc with version_pattern` - `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` +- `date : 2020-09-19T16:24:10` ``` - 282: def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: + 240: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: ... - 283: all_tags = vcs.get_tags(fetch=fetch) - 284: -> 285: # TODO (mb 2020-09-05): version switch - 286: cfg = v1cli.update_cfg_from_vcs(cfg, all_tags) - 287: # cfg = v2cli.update_cfg_from_vcs(cfg, all_tags) + 254: print("<<<<", (normalized_pattern,)) + 255: +> 256: # TODO (mb 2020-09-19): replace {version} etc with version_pattern + 257: pattern_str = _replace_pattern_parts(escaped_pattern) + 258: return re.compile(pattern_str) ``` -## File src/pycalver/__main__.py - Line 392 - W0511 (fixme) +## File src/pycalver/config.py - Line 264 - W0511 (fixme) -- `message: # TODO (mb 2020-09-05): format from config` +- `message: TODO (mb 2020-09-18): Validate Pattern` - `author : Manuel Barkhau ` -- `date : 2020-09-05T14:30:17` +- `date : 2020-09-18T19:04:06` ``` - 336: def bump( + 250: def _parse_config(raw_cfg: RawConfig) -> Config: ... - 390: return - 391: -> 392: # # TODO (mb 2020-09-05): format from config - 393: # commit_message_kwargs = { - 394: # new_version + 262: is_new_pattern = "{" not in version_pattern and "}" not in version_pattern + 263: +> 264: # TODO (mb 2020-09-18): Validate Pattern + 265: # detect YY with WW or UU -> suggest GG with VV + 266: # detect YYMM -> suggest YY0M +``` + + +## File test/test_cli.py - Line 536 - W0511 (fixme) + +- `message: # TODO (mb 2020-09-18):` +- `author : Manuel Barkhau ` +- `date : 2020-09-18T19:35:32` + +``` + 534: + 535: # def test_custom_commit_message(runner): +> 536: # # TODO (mb 2020-09-18): + 537: # assert False ``` # W0703: broad-except -## File src/pycalver/__main__.py - Line 232 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 246 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 209: def _bump( + 219: def _bump( ... - 230: v1cli.rewrite(cfg, new_version) - 231: # v2cli.rewrite(cfg, new_version) -> 232: except Exception as ex: - 233: logger.error(str(ex)) - 234: sys.exit(1) + 244: logger.error(str(ex)) + 245: sys.exit(1) +> 246: except Exception as ex: + 247: # TODO (mb 2020-09-18): Investigate error messages + 248: logger.error(str(ex)) ``` diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index bf7ae8e..2806a45 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -16,14 +16,14 @@ import subprocess as sp import click -import pycalver.cli as v1cli -import pycalver2.cli as v2cli -import pycalver.version as v1version -import pycalver2.version as v2version -import pycalver.rewrite as v1rewrite - -from pycalver import vcs -from pycalver import config +from . import vcs +from . import v1cli +from . import v2cli +from . import config +from . import rewrite +from . import version +from . import v1version +from . import v2version _VERBOSE = 0 @@ -110,7 +110,7 @@ def test( new_version = _incr( old_version, - pattern=pattern, + raw_pattern=pattern, release=release, major=major, minor=minor, @@ -121,9 +121,7 @@ def test( logger.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.") sys.exit(1) - # TODO (mb 2020-09-05): version switch - pep440_version = v1version.to_pep440(new_version) - # pep440_version = v2version.to_pep440(new_version) + pep440_version = version.to_pep440(new_version) click.echo(f"New Version: {new_version}") click.echo(f"PEP440 : {pep440_version}") @@ -150,7 +148,7 @@ def show(verbose: int = 0, fetch: bool = True) -> None: click.echo(f"PEP440 : {cfg.pep440_version}") -def _print_diff(diff: str) -> None: +def _print_diff_str(diff: str) -> None: if sys.stdout.isatty(): for line in diff.splitlines(): if line.startswith("+++") or line.startswith("---"): @@ -167,9 +165,27 @@ def _print_diff(diff: str) -> None: click.echo(diff) +def _print_diff(cfg: config.Config, new_version: str) -> None: + try: + if cfg.is_new_pattern: + diff = v2cli.get_diff(cfg, new_version) + else: + diff = v1cli.get_diff(cfg, new_version) + + _print_diff_str(diff) + except rewrite.NoPatternMatch as ex: + logger.error(str(ex)) + sys.exit(1) + except Exception as ex: + # pylint:disable=broad-except; Mostly we expect IOError here, but + # could be other things and there's no option to recover anyway. + logger.error(str(ex)) + sys.exit(1) + + def _incr( old_version: str, - pattern : str = "{pycalver}", + raw_pattern: str = "{pycalver}", *, release : str = None, major : bool = False, @@ -177,11 +193,11 @@ def _incr( patch : bool = False, pin_date: bool = False, ) -> typ.Optional[str]: - is_v1_pattern = "{" in pattern - if is_v1_pattern: - return v1version.incr( + is_new_pattern = "{" in raw_pattern and "}" in raw_pattern + if is_new_pattern: + return v2version.incr( old_version, - pattern=pattern, + raw_pattern=raw_pattern, release=release, major=major, minor=minor, @@ -189,9 +205,9 @@ def _incr( pin_date=pin_date, ) else: - return v2version.incr( + return v1version.incr( old_version, - pattern=pattern, + raw_pattern=raw_pattern, release=release, major=major, minor=minor, @@ -221,10 +237,10 @@ def _bump( try: if cfg.is_new_pattern: - v2cli.rewrite(cfg, new_version) + v2cli.rewrite_files(cfg, new_version) else: - v1cli.rewrite(cfg, new_version) - except v1rewrite.NoPatternMatch as ex: + v1cli.rewrite_files(cfg, new_version) + except rewrite.NoPatternMatch as ex: logger.error(str(ex)) sys.exit(1) except Exception as ex: @@ -266,11 +282,11 @@ def init(verbose: int = 0, dry: bool = False) -> None: cfg: config.MaybeConfig = config.parse(ctx) if cfg: - logger.error(f"Configuration already initialized in {ctx.config_filepath}") + logger.error(f"Configuration already initialized in {ctx.config_rel_path}") sys.exit(1) if dry: - click.echo(f"Exiting because of '--dry'. Would have written to {ctx.config_filepath}:") + click.echo(f"Exiting because of '--dry'. Would have written to {ctx.config_rel_path}:") cfg_text: str = config.default_config(ctx) click.echo("\n " + "\n ".join(cfg_text.splitlines())) sys.exit(0) @@ -362,7 +378,7 @@ def bump( old_version = cfg.current_version new_version = _incr( old_version, - pattern=cfg.version_pattern, + raw_pattern=cfg.version_pattern, release=release, major=major, minor=minor, @@ -387,20 +403,7 @@ def bump( logger.info(f"New Version: {new_version}") if dry or verbose >= 2: - try: - if cfg.is_new_pattern: - diff = v2cli.get_diff(cfg, new_version) - else: - diff = v1cli.get_diff(cfg, new_version) - _print_diff(diff) - except v1rewrite.NoPatternMatch as ex: - logger.error(str(ex)) - sys.exit(1) - except Exception as ex: - # pylint:disable=broad-except; Mostly we expect IOError here, but - # could be other things and there's no option to recover anyway. - logger.error(str(ex)) - sys.exit(1) + _print_diff(cfg, new_version) if dry: return @@ -408,8 +411,8 @@ def bump( commit_message_kwargs = { 'new_version' : new_version, 'old_version' : old_version, - 'new_version_pep440': v1version.to_pep440(new_version), - 'old_version_pep440': v1version.to_pep440(old_version), + 'new_version_pep440': version.to_pep440(new_version), + 'old_version_pep440': version.to_pep440(old_version), } commit_message = cfg.commit_message.format(**commit_message_kwargs) diff --git a/src/pycalver/config.py b/src/pycalver/config.py index 5576ac5..d1ff1ea 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -14,13 +14,22 @@ import configparser import toml import pathlib2 as pl -import pycalver.version as v1version -import pycalver2.version as v2version +from . import version +from . import v1version +from . import v2version +from . import v1patterns +from . import v2patterns +from .patterns import Pattern logger = logging.getLogger("pycalver.config") -Patterns = typ.List[str] -PatternsByGlob = typ.Dict[str, Patterns] +RawPatterns = typ.List[str] +RawPatternsByFile = typ.Dict[str, RawPatterns] +FileRawPatternsItem = typ.Tuple[str, RawPatterns] + +PatternsByFile = typ.Dict[str, typ.List[Pattern]] +FilePatternsItem = typ.Tuple[str, typ.List[Pattern]] + SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml"] @@ -32,6 +41,7 @@ class ProjectContext(typ.NamedTuple): path : pl.Path config_filepath: pl.Path + config_rel_path: str config_format : str vcs_type : typ.Optional[str] @@ -60,6 +70,12 @@ def init_project_ctx(project_path: typ.Union[str, pl.Path, None] = ".") -> Proje config_filepath = path / "pycalver.toml" config_format = 'toml' + if config_filepath.is_absolute(): + config_rel_path = str(config_filepath.relative_to(path.absolute())) + else: + config_rel_path = str(config_filepath) + config_filepath = pl.Path.cwd() / config_filepath + vcs_type: typ.Optional[str] if (path / ".git").exists(): @@ -69,10 +85,11 @@ def init_project_ctx(project_path: typ.Union[str, pl.Path, None] = ".") -> Proje else: vcs_type = None - return ProjectContext(path, config_filepath, config_format, vcs_type) + return ProjectContext(path, config_filepath, config_rel_path, config_format, vcs_type) -RawConfig = typ.Dict[str, typ.Any] +RawConfig = typ.Dict[str, typ.Any] +MaybeRawConfig = typ.Optional[RawConfig] class Config(typ.NamedTuple): @@ -88,56 +105,46 @@ class Config(typ.NamedTuple): push : bool is_new_pattern: bool - file_patterns: PatternsByGlob + file_patterns: PatternsByFile + + +MaybeConfig = typ.Optional[Config] def _debug_str(cfg: Config) -> str: cfg_str_parts = [ "Config Parsed: Config(", - f"current_version='{cfg.current_version}'", - f"version_pattern='{cfg.version_pattern}'", - f"pep440_version='{cfg.pep440_version}'", - f"commit_message='{cfg.commit_message}'", - f"commit={cfg.commit}", - f"tag={cfg.tag}", - f"push={cfg.push}", - f"is_new_pattern={cfg.is_new_pattern}", - "file_patterns={", + f"\n current_version='{cfg.current_version}',", + f"\n version_pattern='{cfg.version_pattern}',", + f"\n pep440_version='{cfg.pep440_version}',", + f"\n commit_message='{cfg.commit_message}',", + f"\n commit={cfg.commit},", + f"\n tag={cfg.tag},", + f"\n push={cfg.push},", + f"\n is_new_pattern={cfg.is_new_pattern},", + "\n file_patterns={", ] for filepath, patterns in cfg.file_patterns.items(): for pattern in patterns: - cfg_str_parts.append(f"\n '{filepath}': '{pattern}'") + cfg_str_parts.append(f"\n '{filepath}': '{pattern.raw_pattern}',") - cfg_str_parts += ["\n})"] - return ", ".join(cfg_str_parts) + cfg_str_parts += ["\n }\n)"] + return "".join(cfg_str_parts) -MaybeConfig = typ.Optional[Config] -MaybeRawConfig = typ.Optional[RawConfig] +def _parse_cfg_file_patterns( + cfg_parser: configparser.RawConfigParser, +) -> typ.Iterable[FileRawPatternsItem]: + if not cfg_parser.has_section("pycalver:file_patterns"): + return -FilePatterns = typ.Dict[str, typ.List[str]] - - -def _parse_cfg_file_patterns(cfg_parser: configparser.RawConfigParser) -> FilePatterns: - file_patterns: FilePatterns = {} - - file_pattern_items: typ.List[typ.Tuple[str, str]] - if cfg_parser.has_section("pycalver:file_patterns"): - file_pattern_items = cfg_parser.items("pycalver:file_patterns") - else: - file_pattern_items = [] + file_pattern_items: typ.List[typ.Tuple[str, str]] = cfg_parser.items("pycalver:file_patterns") for filepath, patterns_str in file_pattern_items: - patterns: typ.List[str] = [] - for line in patterns_str.splitlines(): - pattern = line.strip() - if pattern: - patterns.append(pattern) - - file_patterns[filepath] = patterns - - return file_patterns + maybe_patterns = (line.strip() for line in patterns_str.splitlines()) + patterns = [p for p in maybe_patterns if p] + yield filepath, patterns class _ConfigParser(configparser.RawConfigParser): @@ -178,7 +185,7 @@ def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig: val = val.lower() in ("yes", "true", "1", "on") raw_cfg[option] = val - raw_cfg['file_patterns'] = _parse_cfg_file_patterns(cfg_parser) + raw_cfg['file_patterns'] = dict(_parse_cfg_file_patterns(cfg_parser)) return raw_cfg @@ -193,64 +200,66 @@ def _parse_toml(cfg_buffer: typ.IO[str]) -> RawConfig: return raw_cfg -def _normalize_file_patterns(raw_cfg: RawConfig) -> FilePatterns: - """Create consistent representation of file_patterns. +def _iter_glob_expanded_file_patterns( + raw_patterns_by_file: RawPatternsByFile, +) -> typ.Iterable[FileRawPatternsItem]: + for filepath_glob, raw_patterns in raw_patterns_by_file.items(): + filepaths = glob.glob(filepath_glob) + if filepaths: + for filepath in filepaths: + yield filepath, raw_patterns + else: + logger.warning(f"Invalid config, no such file: {filepath_glob}") + # fallback to treating it as a simple path + yield filepath_glob, raw_patterns + + +def _compile_v1_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsItem]: + """Create inernal/compiled representation of the file_patterns config field. The result the same, regardless of the config format. """ - version_str : str = raw_cfg['current_version'] - version_pattern: str = raw_cfg['version_pattern'] - pep440_version : str = v1version.to_pep440(version_str) + # current_version: str = raw_cfg['current_version'] + # current_pep440_version = version.pep440_version(current_version) - file_patterns: FilePatterns - if 'file_patterns' in raw_cfg: - file_patterns = raw_cfg['file_patterns'] - else: - file_patterns = {} + version_pattern : str = raw_cfg['version_pattern'] + raw_patterns_by_file: RawPatternsByFile = raw_cfg['file_patterns'] - for filepath_glob, patterns in list(file_patterns.items()): - filepaths = glob.glob(filepath_glob) - if not filepaths: - logger.warning(f"Invalid config, no such file: {filepath_glob}") - # fallback to treating it as a simple path - filepaths = [filepath_glob] + for filepath, raw_patterns in _iter_glob_expanded_file_patterns(raw_patterns_by_file): + compiled_patterns = [ + v1patterns.compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns + ] + yield filepath, compiled_patterns - normalized_patterns: typ.List[str] = [] - for pattern in patterns: - normalized_pattern = pattern.replace("{version}", version_pattern) - if version_pattern == "{pycalver}": - normalized_pattern = normalized_pattern.replace( - "{pep440_version}", "{pep440_pycalver}" - ) - elif version_pattern == "{semver}": - normalized_pattern = normalized_pattern.replace("{pep440_version}", "{semver}") - elif "{pep440_version}" in pattern: - logger.warning(f"Invalid config, cannot match '{pattern}' for '{filepath_glob}'.") - logger.warning(f"No mapping of '{version_pattern}' to '{pep440_version}'") - normalized_patterns.append(normalized_pattern) - for filepath in filepaths: - file_patterns[filepath] = normalized_patterns +def _compile_v2_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsItem]: + """Create inernal/compiled representation of the file_patterns config field. - return file_patterns + The result the same, regardless of the config format. + """ + version_pattern : str = raw_cfg['version_pattern'] + raw_patterns_by_file: RawPatternsByFile = raw_cfg['file_patterns'] + + for filepath, raw_patterns in _iter_glob_expanded_file_patterns(raw_patterns_by_file): + compiled_patterns = [ + v2patterns.compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns + ] + yield filepath, compiled_patterns def _parse_config(raw_cfg: RawConfig) -> Config: """Parse configuration which was loaded from an .ini/.cfg or .toml file.""" - if 'current_version' not in raw_cfg: - raise ValueError("Missing 'pycalver.current_version'") + current_version: str = raw_cfg['current_version'] + current_version = raw_cfg['current_version'] = current_version.strip("'\" ") - version_str: str = raw_cfg['current_version'] - version_str = raw_cfg['current_version'] = version_str.strip("'\" ") - - version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}") + version_pattern: str = raw_cfg['version_pattern'] version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ") commit_message: str = raw_cfg.get('commit_message', DEFAULT_COMMIT_MESSAGE) commit_message = raw_cfg['commit_message'] = commit_message.strip("'\" ") - is_new_pattern = not ("{" in version_pattern or "}" in version_pattern) + is_new_pattern = "{" not in version_pattern and "}" not in version_pattern # TODO (mb 2020-09-18): Validate Pattern # detect YY with WW or UU -> suggest GG with VV @@ -260,11 +269,11 @@ def _parse_config(raw_cfg: RawConfig) -> Config: # NOTE (mb 2019-01-05): Provoke ValueError if version_pattern # and current_version are not compatible. if is_new_pattern: - v2version.parse_version_info(version_str, version_pattern) + v2version.parse_version_info(current_version, version_pattern) else: - v1version.parse_version_info(version_str, version_pattern) + v1version.parse_version_info(current_version, version_pattern) - pep440_version = v1version.to_pep440(version_str) + pep440_version = version.to_pep440(current_version) commit = raw_cfg['commit'] tag = raw_cfg['tag'] @@ -281,10 +290,13 @@ def _parse_config(raw_cfg: RawConfig) -> Config: if push and not commit: raise ValueError("pycalver.commit = true required if pycalver.push = true") - file_patterns = _normalize_file_patterns(raw_cfg) + if is_new_pattern: + file_patterns = dict(_compile_v2_file_patterns(raw_cfg)) + else: + file_patterns = dict(_compile_v1_file_patterns(raw_cfg)) cfg = Config( - current_version=version_str, + current_version=current_version, version_pattern=version_pattern, pep440_version=pep440_version, commit_message=commit_message, @@ -298,11 +310,18 @@ def _parse_config(raw_cfg: RawConfig) -> Config: return cfg -def _parse_current_version_default_pattern(cfg: Config, raw_cfg_text: str) -> str: +def _parse_current_version_default_pattern(ctx: ProjectContext, raw_cfg: RawConfig) -> str: + fobj: typ.IO[str] + + with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: + raw_cfg_text = fobj.read() + is_pycalver_section = False for line in raw_cfg_text.splitlines(): if is_pycalver_section and line.startswith("current_version"): - return line.replace(cfg.current_version, cfg.version_pattern) + current_version: str = raw_cfg['current_version'] + version_pattern: str = raw_cfg['version_pattern'] + return line.replace(current_version, version_pattern) if line.strip() == "[pycalver]": is_pycalver_section = True @@ -312,44 +331,56 @@ def _parse_current_version_default_pattern(cfg: Config, raw_cfg_text: str) -> st raise ValueError("Could not parse pycalver.current_version") +def _parse_raw_config(ctx: ProjectContext) -> RawConfig: + with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: + if ctx.config_format == 'toml': + raw_cfg = _parse_toml(fobj) + elif ctx.config_format == 'cfg': + raw_cfg = _parse_cfg(fobj) + else: + err_msg = ( + f"Invalid config_format='{ctx.config_format}'." + "Supported formats are 'setup.cfg' and 'pyproject.toml'" + ) + raise RuntimeError(err_msg) + + if 'current_version' in raw_cfg: + if not isinstance(raw_cfg['current_version'], str): + err = f"Invalid type for pycalver.current_version = {raw_cfg['current_version']}" + raise TypeError(err) + else: + raise ValueError("Missing 'pycalver.current_version'") + + if 'version_pattern' in raw_cfg: + if not isinstance(raw_cfg['version_pattern'], str): + err = f"Invalid type for pycalver.version_pattern = {raw_cfg['version_pattern']}" + raise TypeError(err) + else: + raw_cfg['version_pattern'] = "{pycalver}" + + if 'file_patterns' not in raw_cfg: + raw_cfg['file_patterns'] = {} + + if ctx.config_rel_path not in raw_cfg['file_patterns']: + # NOTE (mb 2020-09-19): By default we always add + # a pattern for the config section itself. + raw_version_pattern = _parse_current_version_default_pattern(ctx, raw_cfg) + raw_cfg['file_patterns'][ctx.config_rel_path] = [raw_version_pattern] + + return raw_cfg + + def parse(ctx: ProjectContext) -> MaybeConfig: """Parse config file if available.""" if not ctx.config_filepath.exists(): - logger.warning(f"File not found: {ctx.config_filepath}") + logger.warning(f"File not found: {ctx.config_rel_path}") return None - fobj: typ.IO[str] - - cfg_path: str - if ctx.config_filepath.is_absolute(): - cfg_path = str(ctx.config_filepath.relative_to(ctx.path.absolute())) - else: - cfg_path = str(ctx.config_filepath) - - raw_cfg: RawConfig - try: - with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: - if ctx.config_format == 'toml': - raw_cfg = _parse_toml(fobj) - elif ctx.config_format == 'cfg': - raw_cfg = _parse_cfg(fobj) - else: - err_msg = "Invalid config_format='{ctx.config_format}'" - raise RuntimeError(err_msg) - - cfg: Config = _parse_config(raw_cfg) - - if cfg_path not in cfg.file_patterns: - fobj.seek(0) - raw_cfg_text = fobj.read() - cfg.file_patterns[cfg_path] = [ - _parse_current_version_default_pattern(cfg, raw_cfg_text) - ] - - return cfg - except ValueError as ex: - logger.warning(f"Couldn't parse {cfg_path}: {str(ex)}") + raw_cfg = _parse_raw_config(ctx) + return _parse_config(raw_cfg) + except (TypeError, ValueError) as ex: + logger.warning(f"Couldn't parse {ctx.config_rel_path}: {str(ex)}") return None @@ -445,11 +476,11 @@ DEFAULT_TOML_README_MD_STR = """ def _initial_version() -> str: - return dt.datetime.now().strftime("v%Y%m.0001-alpha") + return dt.datetime.now().strftime("v%Y%m.1001-alpha") def _initial_version_pep440() -> str: - return dt.datetime.now().strftime("%Y%m.1a0") + return dt.datetime.now().strftime("%Y%m.1001a0") def default_config(ctx: ProjectContext) -> str: @@ -506,4 +537,4 @@ def write_content(ctx: ProjectContext) -> None: with ctx.config_filepath.open(mode="at", encoding="utf-8") as fobj: fobj.write(cfg_content) - print(f"Updated {ctx.config_filepath}") + print(f"Updated {ctx.config_rel_path}") diff --git a/src/pycalver/parse.py b/src/pycalver/parse.py index 427d30c..0b80519 100644 --- a/src/pycalver/parse.py +++ b/src/pycalver/parse.py @@ -7,7 +7,7 @@ import typing as typ -import pycalver.patterns as v1patterns +from .patterns import Pattern class PatternMatch(typ.NamedTuple): @@ -15,7 +15,7 @@ class PatternMatch(typ.NamedTuple): lineno : int # zero based line : str - pattern: v1patterns.Pattern + pattern: Pattern span : typ.Tuple[int, int] match : str @@ -23,25 +23,26 @@ class PatternMatch(typ.NamedTuple): PatternMatches = typ.Iterable[PatternMatch] -def _iter_for_pattern(lines: typ.List[str], pattern: v1patterns.Pattern) -> PatternMatches: +def _iter_for_pattern(lines: typ.List[str], pattern: Pattern) -> PatternMatches: for lineno, line in enumerate(lines): match = pattern.regexp.search(line) if match: yield PatternMatch(lineno, line, pattern, match.span(), match.group(0)) -def iter_matches(lines: typ.List[str], patterns: typ.List[v1patterns.Pattern]) -> PatternMatches: +def iter_matches(lines: typ.List[str], patterns: typ.List[Pattern]) -> PatternMatches: """Iterate over all matches of any pattern on any line. - >>> import pycalver.patterns as v1patterns + >>> from . import v1patterns >>> lines = ["__version__ = 'v201712.0002-alpha'"] - >>> patterns = ["{pycalver}", "{pep440_pycalver}"] - >>> patterns = [v1patterns.compile_pattern(p) for p in patterns] + >>> version_pattern = "{pycalver}" + >>> raw_patterns = ["{pycalver}", "{pep440_pycalver}"] + >>> patterns = [v1patterns.compile_pattern(version_pattern, p) for p in raw_patterns] >>> matches = list(iter_matches(lines, patterns)) >>> assert matches[0] == PatternMatch( ... lineno = 0, ... line = "__version__ = 'v201712.0002-alpha'", - ... pattern= v1patterns.compile_pattern("{pycalver}"), + ... pattern= v1patterns.compile_pattern(version_pattern), ... span = (15, 33), ... match = "v201712.0002-alpha", ... ) diff --git a/src/pycalver/patterns.py b/src/pycalver/patterns.py index a09bb01..b5ad928 100644 --- a/src/pycalver/patterns.py +++ b/src/pycalver/patterns.py @@ -1,62 +1,14 @@ -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -"""Compose Regular Expressions from Patterns. - ->>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict() ->>> assert version_info == { -... "pycalver" : "v201712.0123-alpha", -... "vYYYYMM" : "v201712", -... "year" : "2017", -... "month" : "12", -... "build" : ".0123", -... "build_no" : "0123", -... "release" : "-alpha", -... "release_tag" : "alpha", -... } ->>> ->>> version_info = PYCALVER_RE.match("v201712.0033").groupdict() ->>> assert version_info == { -... "pycalver" : "v201712.0033", -... "vYYYYMM" : "v201712", -... "year" : "2017", -... "month" : "12", -... "build" : ".0033", -... "build_no" : "0033", -... "release" : None, -... "release_tag": None, -... } -""" - -import re import typing as typ -# https://regex101.com/r/fnj60p/10 -PYCALVER_PATTERN = r""" -\b -(?P - (?P - v # "v" version prefix - (?P\d{4}) - (?P\d{2}) - ) - (?P - \. # "." build nr prefix - (?P\d{4,}) - ) - (?P - \- # "-" release prefix - (?Palpha|beta|dev|rc|post) - )? -)(?:\s|$) -""" -PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE) +class Pattern(typ.NamedTuple): + + version_pattern: str # "{pycalver}", "{year}.{month}", "vYYYY0M.BUILD" + raw_pattern : str # '__version__ = "{version}"', "Copyright (c) YYYY" + regexp : typ.Pattern[str] -PATTERN_ESCAPES = [ +RE_PATTERN_ESCAPES = [ ("\u005c", "\u005c\u005c"), ("-" , "\u005c-"), ("." , "\u005c."), @@ -70,158 +22,3 @@ PATTERN_ESCAPES = [ ("(" , "\u005c("), (")" , "\u005c)"), ] - -COMPOSITE_PART_PATTERNS = { - 'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?", - 'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?", - 'calver' : r"v{year}{month}", - 'semver' : r"{MAJOR}\.{MINOR}\.{PATCH}", - 'release_tag' : r"{tag}", - 'build' : r"\.{bid}", - 'release' : r"(?:-{tag})?", - # depricated - 'pep440_version': r"{year}{month}\.{BID}(?:{pep440_tag})?", -} - - -PART_PATTERNS = { - '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])", - '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,}", - '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,}", -} - - -PATTERN_PART_FIELDS = { - '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', - 'dom_short' : 'dom', - 'doy_short' : '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', -} - - -FULL_PART_FORMATS = { - 'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}", - 'pycalver' : "v{year}{month:02}.{bid}{release}", - 'calver' : "v{year}{month:02}", - 'semver' : "{MAJOR}.{MINOR}.{PATCH}", - 'release_tag' : "{tag}", - 'build' : ".{bid}", - # NOTE (mb 2019-01-04): since release is optional, it - # is treates specially in version.format - # 'release' : "-{tag}", - '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}", - 'dom_short' : "{dom}", - 'doy_short' : "{doy}", - # depricated - 'pep440_version': "{year}{month:02}.{BID}{pep440_tag}", - 'version' : "v{year}{month:02}.{bid}{release}", -} - - -class Pattern(typ.NamedTuple): - - raw : str # "{pycalver}", "{year}.{month}", "YYYY0M.BUILD" - regexp: typ.Pattern[str] - - -Patterns = typ.List[typ.Pattern[str]] - - -def _replace_pattern_parts(pattern: str) -> str: - # The pattern is escaped, so that everything besides the format - # string variables is treated literally. - 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 - - -def compile_pattern_str(pattern: str) -> str: - for char, escaped in PATTERN_ESCAPES: - pattern = pattern.replace(char, escaped) - - return _replace_pattern_parts(pattern) - - -def compile_pattern(pattern: str) -> Pattern: - pattern_str = compile_pattern_str(pattern) - pattern_re = re.compile(pattern_str) - return Pattern(pattern, pattern_re) - - -def _init_composite_patterns() -> None: - for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items(): - part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}") - pattern_str = _replace_pattern_parts(part_pattern) - PART_PATTERNS[part_name] = pattern_str - - -_init_composite_patterns() diff --git a/src/pycalver/rewrite.py b/src/pycalver/rewrite.py index f63dc5a..cbd1a1a 100644 --- a/src/pycalver/rewrite.py +++ b/src/pycalver/rewrite.py @@ -1,24 +1,22 @@ -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -"""Rewrite files, updating occurences of version strings.""" - -import io -import glob import typing as typ import difflib -import logging import pathlib2 as pl -import pycalver.version as v1version -import pycalver.patterns as v1patterns -from pycalver import parse -from pycalver import config +from . import config +from .patterns import Pattern -logger = logging.getLogger("pycalver.rewrite") + +class NoPatternMatch(Exception): + """Pattern not found in content. + + logger.error is used to show error info about the patterns so + that users can debug what is wrong with them. The class + itself doesn't capture that info. This approach is used so + that all patter issues can be shown, rather than bubbling + all the way up the stack on the very first pattern with no + matches. + """ def detect_line_sep(content: str) -> str: @@ -41,18 +39,6 @@ def detect_line_sep(content: str) -> str: return "\n" -class NoPatternMatch(Exception): - """Pattern not found in content. - - logger.error is used to show error info about the patterns so - that users can debug what is wrong with them. The class - itself doesn't capture that info. This approach is used so - that all patter issues can be shown, rather than bubbling - all the way up the stack on the very first pattern with no - matches. - """ - - class RewrittenFileData(typ.NamedTuple): """Container for line-wise content of rewritten files.""" @@ -62,117 +48,19 @@ class RewrittenFileData(typ.NamedTuple): new_lines: typ.List[str] -def iter_file_paths( - file_patterns: config.PatternsByGlob, -) -> typ.Iterable[typ.Tuple[pl.Path, config.Patterns]]: - for globstr, pattern_strs in file_patterns.items(): - file_paths = glob.glob(globstr) - if not any(file_paths): - errmsg = f"No files found for path/glob '{globstr}'" +PathPatternsItem = typ.Tuple[pl.Path, typ.List[Pattern]] + + +def iter_path_patterns_items( + file_patterns: config.PatternsByFile, +) -> typ.Iterable[PathPatternsItem]: + for filepath_str, patterns in file_patterns.items(): + filepath_obj = pl.Path(filepath_str) + if filepath_obj.exists(): + yield (filepath_obj, patterns) + else: + errmsg = f"File does not exist: '{filepath_str}'" raise IOError(errmsg) - for file_path_str in file_paths: - file_path = pl.Path(file_path_str) - yield (file_path, pattern_strs) - - -def rewrite_lines( - pattern_strs: typ.List[str], - new_vinfo : v1version.VersionInfo, - old_lines : typ.List[str], -) -> typ.List[str]: - """Replace occurances of pattern_strs in old_lines with new_vinfo. - - >>> new_vinfo = v1version.parse_version_info("v201811.0123-beta") - >>> pattern_strs = ['__version__ = "{pycalver}"'] - >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"']) - ['__version__ = "v201811.0123-beta"'] - - >>> pattern_strs = ['__version__ = "{pep440_version}"'] - >>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "201809.2b0"']) - ['__version__ = "201811.123b0"'] - """ - new_lines = old_lines[:] - found_patterns = set() - - patterns = [v1patterns.compile_pattern(p) for p in pattern_strs] - matches = parse.iter_matches(old_lines, patterns) - for match in matches: - found_patterns.add(match.pattern.raw) - replacement = v1version.format_version(new_vinfo, match.pattern.raw) - span_l, span_r = match.span - new_line = match.line[:span_l] + replacement + match.line[span_r:] - new_lines[match.lineno] = new_line - - non_matched_patterns = set(pattern_strs) - found_patterns - if non_matched_patterns: - for non_matched_pattern in non_matched_patterns: - logger.error(f"No match for pattern '{non_matched_pattern}'") - compiled_pattern_str = v1patterns.compile_pattern_str(non_matched_pattern) - logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'") - raise NoPatternMatch("Invalid pattern(s)") - else: - return new_lines - - -def rfd_from_content( - pattern_strs: typ.List[str], - new_vinfo : v1version.VersionInfo, - content : str, -) -> RewrittenFileData: - r"""Rewrite pattern occurrences with version string. - - >>> new_vinfo = v1version.parse_version_info("v201809.0123") - >>> pattern_strs = ['__version__ = "{pycalver}"'] - >>> content = '__version__ = "v201809.0001-alpha"' - >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - >>> rfd.new_lines - ['__version__ = "v201809.0123"'] - >>> - >>> new_vinfo = v1version.parse_version_info("v1.2.3", "v{semver}") - >>> pattern_strs = ['__version__ = "v{semver}"'] - >>> content = '__version__ = "v1.2.2"' - >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - >>> rfd.new_lines - ['__version__ = "v1.2.3"'] - """ - line_sep = detect_line_sep(content) - old_lines = content.split(line_sep) - new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines) - return RewrittenFileData("", line_sep, old_lines, new_lines) - - -def iter_rewritten( - file_patterns: config.PatternsByGlob, - new_vinfo : v1version.VersionInfo, -) -> typ.Iterable[RewrittenFileData]: - r'''Iterate over files with version string replaced. - - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - >>> new_vinfo = v1version.parse_version_info("v201809.0123") - >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) - >>> rfd = list(rewritten_datas)[0] - >>> expected = [ - ... '# This file is part of the pycalver project', - ... '# https://github.com/mbarkhau/pycalver', - ... '#', - ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', - ... '# SPDX-License-Identifier: MIT', - ... '"""PyCalVer: CalVer for Python Packages."""', - ... '', - ... '__version__ = "v201809.0123"', - ... '', - ... ] - >>> assert rfd.new_lines[:len(expected)] == expected - ''' - - fobj: typ.IO[str] - - for file_path, pattern_strs in iter_file_paths(file_patterns): - with file_path.open(mode="rt", encoding="utf-8") as fobj: - content = fobj.read() - - rfd = rfd_from_content(pattern_strs, new_vinfo, content) - yield rfd._replace(path=str(file_path)) def diff_lines(rfd: RewrittenFileData) -> typ.List[str]: @@ -188,57 +76,10 @@ def diff_lines(rfd: RewrittenFileData) -> typ.List[str]: ['--- ', '+++ ', '@@ -1 +1 @@', '-foo', '+bar'] """ lines = difflib.unified_diff( - a=rfd.old_lines, b=rfd.new_lines, lineterm="", fromfile=rfd.path, tofile=rfd.path + a=rfd.old_lines, + b=rfd.new_lines, + lineterm="", + fromfile=rfd.path, + tofile=rfd.path, ) return list(lines) - - -def diff(new_vinfo: v1version.VersionInfo, file_patterns: config.PatternsByGlob) -> str: - r"""Generate diffs of rewritten files. - - >>> new_vinfo = v1version.parse_version_info("v201809.0123") - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} - >>> diff_str = diff(new_vinfo, file_patterns) - >>> lines = diff_str.split("\n") - >>> lines[:2] - ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] - >>> assert lines[6].startswith('-__version__ = "v2') - >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') - >>> lines[7] - '+__version__ = "v201809.0123"' - """ - - full_diff = "" - fobj: typ.IO[str] - - for file_path, pattern_strs in sorted(iter_file_paths(file_patterns)): - with file_path.open(mode="rt", encoding="utf-8") as fobj: - content = fobj.read() - - try: - rfd = rfd_from_content(pattern_strs, new_vinfo, content) - except NoPatternMatch: - # pylint:disable=raise-missing-from ; we support py2, so not an option - errmsg = f"No patterns matched for '{file_path}'" - raise NoPatternMatch(errmsg) - - rfd = rfd._replace(path=str(file_path)) - lines = diff_lines(rfd) - if len(lines) == 0: - errmsg = f"No patterns matched for '{file_path}'" - raise NoPatternMatch(errmsg) - - full_diff += "\n".join(lines) + "\n" - - full_diff = full_diff.rstrip("\n") - return full_diff - - -def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: v1version.VersionInfo) -> None: - """Rewrite project files, updating each with the new version.""" - fobj: typ.IO[str] - - for file_data in iter_rewritten(file_patterns, new_vinfo): - new_content = file_data.line_sep.join(file_data.new_lines) - with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj: - fobj.write(new_content) diff --git a/src/pycalver/cli.py b/src/pycalver/v1cli.py similarity index 71% rename from src/pycalver/cli.py rename to src/pycalver/v1cli.py index da4cea7..d33ba12 100755 --- a/src/pycalver/cli.py +++ b/src/pycalver/v1cli.py @@ -12,11 +12,12 @@ Provided subcommands: show, test, init, bump import typing as typ import logging -import pycalver.rewrite as v1rewrite -import pycalver.version as v1version -from pycalver import config +from . import config +from . import version +from . import v1rewrite +from . import v1version -logger = logging.getLogger("pycalver.cli") +logger = logging.getLogger("pycalver.v1cli") def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config: @@ -28,7 +29,7 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C version_tags.sort(reverse=True) logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") latest_version_tag = version_tags[0] - latest_version_pep440 = v1version.to_pep440(latest_version_tag) + latest_version_pep440 = version.to_pep440(latest_version_tag) if latest_version_tag <= cfg.current_version: return cfg @@ -40,14 +41,15 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C ) -def rewrite( +def rewrite_files( cfg : config.Config, new_version: str, ) -> None: new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) - v1rewrite.rewrite(cfg.file_patterns, new_vinfo) + v1rewrite.rewrite_files(cfg.file_patterns, new_vinfo) def get_diff(cfg: config.Config, new_version: str) -> str: - new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) - return v1rewrite.diff(new_vinfo, cfg.file_patterns) + old_vinfo = v1version.parse_version_info(cfg.current_version, cfg.version_pattern) + new_vinfo = v1version.parse_version_info(new_version , cfg.version_pattern) + return v1rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns) diff --git a/src/pycalver/v1patterns.py b/src/pycalver/v1patterns.py new file mode 100644 index 0000000..2171ac3 --- /dev/null +++ b/src/pycalver/v1patterns.py @@ -0,0 +1,220 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Compose Regular Expressions from Patterns. + +>>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict() +>>> assert version_info == { +... "pycalver" : "v201712.0123-alpha", +... "vYYYYMM" : "v201712", +... "year" : "2017", +... "month" : "12", +... "build" : ".0123", +... "build_no" : "0123", +... "release" : "-alpha", +... "release_tag" : "alpha", +... } +>>> +>>> version_info = PYCALVER_RE.match("v201712.0033").groupdict() +>>> assert version_info == { +... "pycalver" : "v201712.0033", +... "vYYYYMM" : "v201712", +... "year" : "2017", +... "month" : "12", +... "build" : ".0033", +... "build_no" : "0033", +... "release" : None, +... "release_tag": None, +... } +""" + +import re +import typing as typ +import logging + +from .patterns import RE_PATTERN_ESCAPES +from .patterns import Pattern + +logger = logging.getLogger("pycalver.v1patterns") + +# https://regex101.com/r/fnj60p/10 +PYCALVER_PATTERN = r""" +\b +(?P + (?P + v # "v" version prefix + (?P\d{4}) + (?P\d{2}) + ) + (?P + \. # "." build nr prefix + (?P\d{4,}) + ) + (?P + \- # "-" release prefix + (?Palpha|beta|dev|rc|post) + )? +)(?:\s|$) +""" + +PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE) + + +COMPOSITE_PART_PATTERNS = { + 'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?", + 'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?", + 'calver' : r"v{year}{month}", + 'semver' : r"{MAJOR}\.{MINOR}\.{PATCH}", + 'release_tag' : r"{tag}", + 'build' : r"\.{bid}", + 'release' : r"(?:-{tag})?", + # depricated + 'pep440_version': r"{year}{month}\.{BID}(?:{pep440_tag})?", +} + + +PART_PATTERNS = { + '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])", + '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,}", + '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,}", +} + + +PATTERN_PART_FIELDS = { + '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', + 'dom_short' : 'dom', + 'doy_short' : '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', +} + + +FULL_PART_FORMATS = { + 'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}", + 'pycalver' : "v{year}{month:02}.{bid}{release}", + 'calver' : "v{year}{month:02}", + 'semver' : "{MAJOR}.{MINOR}.{PATCH}", + 'release_tag' : "{tag}", + 'build' : ".{bid}", + # NOTE (mb 2019-01-04): since release is optional, it + # is treates specially in version.format + # 'release' : "-{tag}", + '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}", + 'dom_short' : "{dom}", + 'doy_short' : "{doy}", + # depricated + 'pep440_version': "{year}{month:02}.{BID}{pep440_tag}", + 'version' : "v{year}{month:02}.{bid}{release}", +} + + +def _replace_pattern_parts(pattern: str) -> str: + # The pattern is escaped, so that everything besides the format + # string variables is treated literally. + 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 + + +def _init_composite_patterns() -> None: + for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items(): + part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}") + pattern_str = _replace_pattern_parts(part_pattern) + PART_PATTERNS[part_name] = pattern_str + + +_init_composite_patterns() + + +def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: + normalized_pattern = raw_pattern.replace(r"{version}", version_pattern) + if version_pattern == r"{pycalver}": + normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{pep440_pycalver}") + elif version_pattern == r"{semver}": + normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{semver}") + elif r"{pep440_version}" in raw_pattern: + logger.warning(f"No mapping of '{version_pattern}' to '{{pep440_version}}'") + + escaped_pattern = normalized_pattern + for char, escaped in RE_PATTERN_ESCAPES: + escaped_pattern = escaped_pattern.replace(char, escaped) + + # TODO (mb 2020-09-19): replace {version} etc with version_pattern + pattern_str = _replace_pattern_parts(escaped_pattern) + return re.compile(pattern_str) + + +def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern: + _raw_pattern = version_pattern if raw_pattern is None else raw_pattern + regexp = _compile_pattern_re(version_pattern, _raw_pattern) + return Pattern(version_pattern, _raw_pattern, regexp) diff --git a/src/pycalver/v1rewrite.py b/src/pycalver/v1rewrite.py new file mode 100644 index 0000000..2cdb487 --- /dev/null +++ b/src/pycalver/v1rewrite.py @@ -0,0 +1,193 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Rewrite files, updating occurences of version strings.""" + +import io +import typing as typ +import logging + +from . import parse +from . import config +from . import rewrite +from . import version +from . import v1version +from .patterns import Pattern + +logger = logging.getLogger("pycalver.v1rewrite") + + +def rewrite_lines( + patterns : typ.List[Pattern], + new_vinfo: version.V1VersionInfo, + old_lines: typ.List[str], +) -> typ.List[str]: + """Replace occurances of patterns in old_lines with new_vinfo. + + >>> from .v1patterns import compile_pattern + >>> version_pattern = "{pycalver}" + >>> new_vinfo = v1version.parse_version_info("v201811.0123-beta", version_pattern) + >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pycalver}"')] + >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-beta"']) + ['__version__ = "v201811.0123-beta"'] + + >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pep440_version}"')] + >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "201809.2b0"']) + ['__version__ = "201811.123b0"'] + """ + new_lines = old_lines[:] + found_patterns = set() + + for match in parse.iter_matches(old_lines, patterns): + found_patterns.add(match.pattern.raw_pattern) + replacement = v1version.format_version(new_vinfo, match.pattern.raw_pattern) + span_l, span_r = match.span + new_line = match.line[:span_l] + replacement + match.line[span_r:] + new_lines[match.lineno] = new_line + + non_matched_patterns = set(patterns) - found_patterns + if non_matched_patterns: + for nmp in non_matched_patterns: + logger.error(f"No match for pattern '{nmp.raw_pattern}'") + logger.error(f"Pattern compiles to regex '{nmp.regexp.pattern}'") + raise rewrite.NoPatternMatch("Invalid pattern(s)") + else: + return new_lines + + +def rfd_from_content( + patterns : typ.List[Pattern], + new_vinfo: version.V1VersionInfo, + content : str, + path : str = "", +) -> rewrite.RewrittenFileData: + r"""Rewrite pattern occurrences with version string. + + >>> from .v1patterns import compile_pattern + >>> patterns = [compile_pattern("{pycalver}", '__version__ = "{pycalver}"'] + >>> new_vinfo = v1version.parse_version_info("v201809.0123") + + >>> content = '__version__ = "v201809.0001-alpha"' + >>> rfd = rfd_from_content(patterns, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v201809.0123"'] + + >>> patterns = [compile_pattern('{semver}', '__version__ = "v{semver}"')] + >>> new_vinfo = v1version.parse_version_info("v1.2.3", "v{semver}") + + >>> content = '__version__ = "v1.2.2"' + >>> rfd = rfd_from_content(patterns, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v1.2.3"'] + """ + line_sep = rewrite.detect_line_sep(content) + old_lines = content.split(line_sep) + new_lines = rewrite_lines(patterns, new_vinfo, old_lines) + return rewrite.RewrittenFileData(path, line_sep, old_lines, new_lines) + + +def iter_rewritten( + file_patterns: config.PatternsByFile, + new_vinfo : version.V1VersionInfo, +) -> typ.Iterable[rewrite.RewrittenFileData]: + r'''Iterate over files with version string replaced. + + >>> version_pattern = "{pycalver}" + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} + >>> new_vinfo = v1version.parse_version_info("v201809.0123") + >>> rewritten_datas = iter_rewritten(version_pattern, file_patterns, new_vinfo) + >>> rfd = list(rewritten_datas)[0] + >>> expected = [ + ... '# This file is part of the pycalver project', + ... '# https://github.com/mbarkhau/pycalver', + ... '#', + ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', + ... '# SPDX-License-Identifier: MIT', + ... '"""PyCalVer: CalVer for Python Packages."""', + ... '', + ... '__version__ = "v201809.0123"', + ... '', + ... ] + >>> assert rfd.new_lines == expected + ''' + + fobj: typ.IO[str] + + for file_path, pattern_strs in rewrite.iter_path_patterns_items(file_patterns): + with file_path.open(mode="rt", encoding="utf-8") as fobj: + content = fobj.read() + + rfd = rfd_from_content(pattern_strs, new_vinfo, content) + yield rfd._replace(path=str(file_path)) + + +def diff( + old_vinfo : version.V1VersionInfo, + new_vinfo : version.V1VersionInfo, + file_patterns: config.PatternsByFile, +) -> str: + r"""Generate diffs of rewritten files. + + >>> old_vinfo = v1version.parse_version_info("v201809.0123") + >>> new_vinfo = v1version.parse_version_info("v201810.1124") + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']} + >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) + >>> lines = diff_str.split("\n") + >>> lines[:2] + ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] + >>> assert lines[6].startswith('-__version__ = "v2') + >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') + >>> lines[7] + '+__version__ = "v201809.0123"' + + >>> file_patterns = {"LICENSE": ['Copyright (c) 2018-{year}']} + >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) + >>> assert not diff_str + """ + + full_diff = "" + fobj: typ.IO[str] + + for file_path, patterns in sorted(rewrite.iter_path_patterns_items(file_patterns)): + with file_path.open(mode="rt", encoding="utf-8") as fobj: + content = fobj.read() + + has_updated_version = False + for pattern in patterns: + old_str = v1version.format_version(old_vinfo, pattern.raw_pattern) + new_str = v1version.format_version(new_vinfo, pattern.raw_pattern) + if old_str != new_str: + has_updated_version = True + + try: + rfd = rfd_from_content(patterns, new_vinfo, content) + except rewrite.NoPatternMatch: + # pylint:disable=raise-missing-from ; we support py2, so not an option + errmsg = f"No patterns matched for '{file_path}'" + raise rewrite.NoPatternMatch(errmsg) + + rfd = rfd._replace(path=str(file_path)) + lines = rewrite.diff_lines(rfd) + if len(lines) == 0 and has_updated_version: + errmsg = f"No patterns matched for '{file_path}'" + raise rewrite.NoPatternMatch(errmsg) + + full_diff += "\n".join(lines) + "\n" + + full_diff = full_diff.rstrip("\n") + return full_diff + + +def rewrite_files( + file_patterns: config.PatternsByFile, + new_vinfo : version.V1VersionInfo, +) -> None: + """Rewrite project files, updating each with the new version.""" + fobj: typ.IO[str] + + for file_data in iter_rewritten(file_patterns, new_vinfo): + new_content = file_data.line_sep.join(file_data.new_lines) + with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj: + fobj.write(new_content) diff --git a/src/pycalver/v1version.py b/src/pycalver/v1version.py new file mode 100644 index 0000000..d007cdd --- /dev/null +++ b/src/pycalver/v1version.py @@ -0,0 +1,407 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Functions related to version string manipulation.""" + +import typing as typ +import logging +import datetime as dt + +from . import version +from . import v1patterns + +logger = logging.getLogger("pycalver.v1version") + + +CalInfo = typ.Union[version.V1CalendarInfo, version.V1VersionInfo] + + +def _is_later_than(old: CalInfo, new: CalInfo) -> bool: + """Is old > new based on non None fields.""" + for field in version.V1CalendarInfo._fields: + aval = getattr(old, field) + bval = getattr(new, field) + if not (aval is None or bval is None): + if aval > bval: + return True + return False + + +def _ver_to_cal_info(vnfo: version.V1VersionInfo) -> version.V1CalendarInfo: + return version.V1CalendarInfo( + vnfo.year, + vnfo.quarter, + vnfo.month, + vnfo.dom, + vnfo.doy, + vnfo.iso_week, + vnfo.us_week, + ) + + +def cal_info(date: dt.date = None) -> version.V1CalendarInfo: + """Generate calendar components for current date. + + >>> from datetime import date + + >>> c = cal_info(date(2019, 1, 5)) + >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 1, 1, 5, 5, 0, 0) + + >>> c = cal_info(date(2019, 1, 6)) + >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 1, 1, 6, 6, 0, 1) + + >>> c = cal_info(date(2019, 1, 7)) + >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 1, 1, 7, 7, 1, 1) + + >>> c = cal_info(date(2019, 4, 7)) + >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) + (2019, 2, 4, 7, 97, 13, 14) + """ + if date is None: + date = version.TODAY + + kwargs = { + 'year' : date.year, + 'quarter' : version.quarter_from_month(date.month), + 'month' : date.month, + 'dom' : date.day, + 'doy' : int(date.strftime("%j"), base=10), + 'iso_week': int(date.strftime("%W"), base=10), + 'us_week' : int(date.strftime("%U"), base=10), + } + + return version.V1CalendarInfo(**kwargs) + + +FieldKey = str +MatchGroupKey = str +MatchGroupStr = str + +PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr] +FieldValues = typ.Dict[FieldKey , MatchGroupStr] + + +def _parse_field_values(field_values: FieldValues) -> version.V1VersionInfo: + fvals = field_values + tag = fvals.get('tag') + if tag is None: + tag = "final" + tag = version.TAG_BY_PEP440_TAG.get(tag, tag) + assert tag is not None + + bid = fvals['bid'] if 'bid' in fvals else "0001" + + year = int(fvals['year']) if 'year' in fvals else None + doy = int(fvals['doy' ]) if 'doy' in fvals else None + + month: typ.Optional[int] + dom : typ.Optional[int] + + if year and doy: + date = version.date_from_doy(year, doy) + month = date.month + dom = date.day + else: + month = int(fvals['month']) if 'month' in fvals else None + dom = int(fvals['dom' ]) if 'dom' in fvals 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(fvals['quarter']) if 'quarter' in fvals else None + if quarter is None and month: + quarter = version.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 version.V1VersionInfo( + 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(cinfo: CalInfo) -> bool: + """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 version.V1CalendarInfo._fields: + maybe_val: typ.Any = getattr(cinfo, field, None) + if isinstance(maybe_val, int): + return True + + return False + + +VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] + + +def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues: + for part_name in pattern_groups.keys(): + is_valid_part_name = ( + part_name in v1patterns.COMPOSITE_PART_PATTERNS + or part_name in v1patterns.PATTERN_PART_FIELDS + ) + if not is_valid_part_name: + err_msg = f"Invalid part '{part_name}'" + raise version.PatternError(err_msg) + + field_value_items = [ + (field_name, pattern_groups[part_name]) + for part_name, field_name in v1patterns.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 version.PatternError(err_msg) + else: + return dict(field_value_items) + + +def _parse_version_info(pattern_groups: PatternGroups) -> version.V1VersionInfo: + """Parse normalized V1VersionInfo from groups of a matched pattern. + + >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"}) + >>> (vnfo.year, 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, 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, raw_pattern: str = "{pycalver}") -> version.V1VersionInfo: + """Parse normalized V1VersionInfo. + + >>> vnfo = parse_version_info("v201712.0033-beta", raw_pattern="{pycalver}") + >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}) + + >>> vnfo = parse_version_info("1.23.456", raw_pattern="{semver}") + >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) + """ + pattern = v1patterns.compile_pattern(raw_pattern) + match = pattern.regexp.match(version_str) + if match is None: + err_msg = ( + f"Invalid version string '{version_str}' " + f"for pattern '{raw_pattern}'/'{pattern.regexp}'" + ) + raise version.PatternError(err_msg) + else: + return _parse_version_info(match.groupdict()) + + +def is_valid(version_str: str, raw_pattern: str = "{pycalver}") -> bool: + """Check if a version matches a pattern. + + >>> is_valid("v201712.0033-beta", raw_pattern="{pycalver}") + True + >>> is_valid("v201712.0033-beta", raw_pattern="{semver}") + False + >>> is_valid("1.2.3", raw_pattern="{semver}") + True + >>> is_valid("v201712.0033-beta", raw_pattern="{semver}") + False + """ + try: + parse_version_info(version_str, raw_pattern) + return True + except version.PatternError: + return False + + +ID_FIELDS_BY_PART = { + 'MAJOR' : 'major', + 'MINOR' : 'minor', + 'MM' : 'minor', + 'MMM' : 'minor', + 'MMMM' : 'minor', + 'MMMMM' : 'minor', + 'MMMMMM' : 'minor', + 'MMMMMMM': 'minor', + 'PATCH' : 'patch', + 'PP' : 'patch', + 'PPP' : 'patch', + 'PPPP' : 'patch', + 'PPPPP' : 'patch', + 'PPPPPP' : 'patch', + 'PPPPPPP': 'patch', + 'BID' : 'bid', + 'BB' : 'bid', + 'BBB' : 'bid', + 'BBBB' : 'bid', + 'BBBBB' : 'bid', + 'BBBBBB' : 'bid', + 'BBBBBBB': 'bid', +} + + +def format_version(vinfo: version.V1VersionInfo, raw_pattern: str) -> str: + """Generate version string. + + >>> import datetime as dt + >>> vinfo = parse_version_info("v201712.0033-beta", raw_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()) + >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') + + >>> format_version(vinfo_a, raw_pattern="v{yy}.{BID}{release}") + 'v17.33-beta' + >>> format_version(vinfo_a, raw_pattern="{pep440_version}") + '201701.33b0' + + >>> format_version(vinfo_a, raw_pattern="{pycalver}") + 'v201701.0033-beta' + >>> format_version(vinfo_b, raw_pattern="{pycalver}") + 'v201712.0033-beta' + + >>> format_version(vinfo_a, raw_pattern="v{year}w{iso_week}.{BID}{release}") + 'v2017w00.33-beta' + >>> format_version(vinfo_b, raw_pattern="v{year}w{iso_week}.{BID}{release}") + 'v2017w52.33-beta' + + >>> format_version(vinfo_a, raw_pattern="v{year}d{doy}.{bid}{release}") + 'v2017d001.0033-beta' + >>> format_version(vinfo_b, raw_pattern="v{year}d{doy}.{bid}{release}") + 'v2017d365.0033-beta' + + >>> format_version(vinfo_c, raw_pattern="v{year}w{iso_week}.{BID}-{tag}") + 'v2017w52.33-final' + >>> format_version(vinfo_c, raw_pattern="v{year}w{iso_week}.{BID}{release}") + 'v2017w52.33' + + >>> format_version(vinfo_c, raw_pattern="v{MAJOR}.{MINOR}.{PATCH}") + 'v1.2.34' + >>> format_version(vinfo_c, raw_pattern="v{MAJOR}.{MM}.{PPP}") + 'v1.02.034' + """ + full_pattern = raw_pattern + for part_name, full_part_format in v1patterns.FULL_PART_FORMATS.items(): + full_pattern = full_pattern.replace("{" + part_name + "}", full_part_format) + + kwargs: typ.Dict[str, typ.Union[str, int, None]] = vinfo._asdict() + + tag = vinfo.tag + if tag == 'final': + kwargs['release' ] = "" + kwargs['pep440_tag'] = "" + else: + kwargs['release' ] = "-" + tag + kwargs['pep440_tag'] = version.PEP440_TAG_BY_TAG[tag] + "0" + + kwargs['release_tag'] = tag + + year = vinfo.year + if year: + kwargs['yy' ] = str(year)[-2:] + kwargs['yyyy'] = year + + kwargs['BID'] = 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 full_pattern.format(**kwargs) + + +def incr( + old_version: str, + raw_pattern: str = "{pycalver}", + *, + release : typ.Optional[str] = None, + major : bool = False, + minor : bool = False, + patch : bool = False, + pin_date: bool = False, +) -> typ.Optional[str]: + """Increment version string. + + 'old_version' is assumed to be a string that matches 'pattern' + """ + try: + old_vinfo = parse_version_info(old_version, raw_pattern) + except version.PatternError as ex: + logger.error(str(ex)) + return None + + cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() + + if _is_later_than(old_vinfo, cur_cinfo): + cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict()) + else: + logger.warning(f"Version appears to be from the future '{old_version}'") + cur_vinfo = old_vinfo + + cur_vinfo = version.incr_non_cal_parts( + cur_vinfo, + release, + major, + minor, + patch, + ) + new_version = format_version(cur_vinfo, raw_pattern) + if new_version == old_version: + logger.error("Invalid arguments or pattern, version did not change.") + return None + else: + return new_version diff --git a/src/pycalver2/cli.py b/src/pycalver/v2cli.py similarity index 71% rename from src/pycalver2/cli.py rename to src/pycalver/v2cli.py index 753f587..243f2f5 100644 --- a/src/pycalver2/cli.py +++ b/src/pycalver/v2cli.py @@ -12,12 +12,12 @@ Provided subcommands: show, test, init, bump import typing as typ import logging -import pycalver.version as v1version -import pycalver2.rewrite as v2rewrite -import pycalver2.version as v2version -from pycalver import config +from . import config +from . import version +from . import v2rewrite +from . import v2version -logger = logging.getLogger("pycalver2.cli") +logger = logging.getLogger("pycalver.v2cli") def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config: @@ -29,7 +29,7 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C version_tags.sort(reverse=True) logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}") latest_version_tag = version_tags[0] - latest_version_pep440 = v1version.to_pep440(latest_version_tag) + latest_version_pep440 = version.to_pep440(latest_version_tag) if latest_version_tag <= cfg.current_version: return cfg @@ -41,14 +41,15 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C ) -def rewrite( +def rewrite_files( cfg : config.Config, new_version: str, ) -> None: new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) - v2rewrite.rewrite(cfg.file_patterns, new_vinfo) + v2rewrite.rewrite_files(cfg.file_patterns, new_vinfo) def get_diff(cfg: config.Config, new_version: str) -> str: - new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) - return v2rewrite.diff(new_vinfo, cfg.file_patterns) + old_vinfo = v2version.parse_version_info(cfg.current_version, cfg.version_pattern) + new_vinfo = v2version.parse_version_info(new_version , cfg.version_pattern) + return v2rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns) diff --git a/src/pycalver2/patterns.py b/src/pycalver/v2patterns.py similarity index 74% rename from src/pycalver2/patterns.py rename to src/pycalver/v2patterns.py index 8219422..57a52b9 100644 --- a/src/pycalver2/patterns.py +++ b/src/pycalver/v2patterns.py @@ -33,30 +33,28 @@ import re import typing as typ +import logging -import pycalver.patterns as v1patterns +from .patterns import RE_PATTERN_ESCAPES +from .patterns import Pattern -PATTERN_ESCAPES = [ - ("\u005c", "\u005c\u005c"), - ("-" , "\u005c-"), - ("." , "\u005c."), - ("+" , "\u005c+"), - ("*" , "\u005c*"), - ("?" , "\u005c?"), - ("{" , "\u005c{"), - ("}" , "\u005c}"), - # ("[" , "\u005c["), # [braces] are used for optional parts - # ("]" , "\u005c]"), - ("(", "\u005c("), - (")", "\u005c)"), -] +logger = logging.getLogger("pycalver.v2patterns") + +# NOTE (mb 2020-09-17): For patterns with different options '(AAA|BB|C)', the +# patterns with more digits should be first/left of those with fewer digits: +# +# good: (?:1[0-2]|[1-9]) +# bad: (?:[1-9]|1[0-2]) +# +# This ensures that the longest match is done for a pattern. +# +# This implies that patterns for smaller numbers sometimes must be right of +# those for larger numbers. To be consistent we use this ordering not +# sometimes but always (even though in theory it wouldn't matter): +# +# good: (?:3[0-1]|[1-2][0-9]|[1-9]) +# bad: (?:[1-2][0-9]|3[0-1]|[1-9]) -# 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 @@ -239,14 +237,28 @@ def _replace_pattern_parts(pattern: str) -> str: return result_pattern -def compile_pattern_str(pattern: str) -> str: - for char, escaped in PATTERN_ESCAPES: - pattern = pattern.replace(char, escaped) +def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: + escaped_pattern = raw_pattern + for char, escaped in RE_PATTERN_ESCAPES: + # [] braces are used for optional parts, such as [-TAG]/[-beta] + is_semantic_char = char in "[]" + if not is_semantic_char: + # escape it so it is a literal in the re pattern + escaped_pattern = escaped_pattern.replace(char, escaped) - return _replace_pattern_parts(pattern) + escaped_pattern = raw_pattern.replace("[", "\u005c[").replace("]", "\u005c]") + normalized_pattern = escaped_pattern.replace("{version}", version_pattern) + print(">>>>", (raw_pattern ,)) + print("....", (escaped_pattern ,)) + print("....", (normalized_pattern,)) + print("<<<<", (normalized_pattern,)) + + # TODO (mb 2020-09-19): replace {version} etc with version_pattern + pattern_str = _replace_pattern_parts(escaped_pattern) + return re.compile(pattern_str) -def compile_pattern(pattern: str) -> v1patterns.Pattern: - pattern_str = compile_pattern_str(pattern) - pattern_re = re.compile(pattern_str) - return v1patterns.Pattern(pattern, pattern_re) +def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern: + _raw_pattern = version_pattern if raw_pattern is None else raw_pattern + regexp = _compile_pattern_re(version_pattern, _raw_pattern) + return Pattern(version_pattern, _raw_pattern, regexp) diff --git a/src/pycalver/v2rewrite.py b/src/pycalver/v2rewrite.py new file mode 100644 index 0000000..c82222a --- /dev/null +++ b/src/pycalver/v2rewrite.py @@ -0,0 +1,198 @@ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License +# SPDX-License-Identifier: MIT +"""Rewrite files, updating occurences of version strings.""" + +import io +import typing as typ +import logging + +from . import parse +from . import config +from . import rewrite +from . import version +from . import v2version +from .patterns import Pattern + +logger = logging.getLogger("pycalver.v2rewrite") + + +def rewrite_lines( + patterns : typ.List[Pattern], + new_vinfo: version.V2VersionInfo, + old_lines: typ.List[str], +) -> typ.List[str]: + """Replace occurances of patterns in old_lines with new_vinfo. + + >>> from .v2patterns import compile_pattern + >>> version_pattern = "vYYYY0M.BUILD[-TAG]" + >>> new_vinfo = v2version.parse_version_info("v201811.0123-beta", version_pattern) + >>> patterns = [compile_pattern(version_pattern, '__version__ = "{version}"')] + >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" ']) + ['__version__ = "v201811.0123-beta" '] + + >>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" # comment']) + ['__version__ = "v201811.0123-beta" # comment'] + + >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pep440_version}"')] + >>> old_lines = ['__version__ = "201809.2a0"'] + >>> rewrite_lines(patterns, new_vinfo, old_lines) + ['__version__ = "201811.123b0"'] + """ + new_lines = old_lines[:] + found_patterns = set() + + for match in parse.iter_matches(old_lines, patterns): + found_patterns.add(match.pattern.raw_pattern) + replacement = v2version.format_version(new_vinfo, match.pattern.raw_pattern) + span_l, span_r = match.span + new_line = match.line[:span_l] + replacement + match.line[span_r:] + new_lines[match.lineno] = new_line + + non_matched_patterns = set(patterns) - found_patterns + if non_matched_patterns: + for nmp in non_matched_patterns: + logger.error(f"No match for pattern '{nmp.raw_pattern}'") + logger.error(f"Pattern compiles to regex '{nmp.regexp.pattern}'") + raise rewrite.NoPatternMatch("Invalid pattern(s)") + else: + return new_lines + + +def rfd_from_content( + patterns : typ.List[Pattern], + new_vinfo: version.V2VersionInfo, + content : str, + path : str = "", +) -> rewrite.RewrittenFileData: + r"""Rewrite pattern occurrences with version string. + + >>> version_pattern = "vYYYY0M.BUILD[-TAG]" + >>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + >>> raw_patterns = ['__version__ = "vYYYY0M.BUILD[-TAG]"'] + >>> patterns = + >>> content = '__version__ = "v201809.0001-alpha"' + >>> rfd = rfd_from_content(patterns, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v201809.0123"'] + + >>> version_pattern = "vMAJOR.MINOR.PATCH" + >>> new_vinfo = v2version.parse_version_info("v1.2.3", version_pattern) + >>> raw_patterns = ['__version__ = "vMAJOR.MINOR.PATCH"'] + >>> patterns = + >>> content = '__version__ = "v1.2.2"' + >>> rfd = rfd_from_content(patterns, new_vinfo, content) + >>> rfd.new_lines + ['__version__ = "v1.2.3"'] + """ + line_sep = rewrite.detect_line_sep(content) + old_lines = content.split(line_sep) + new_lines = rewrite_lines(patterns, new_vinfo, old_lines) + return rewrite.RewrittenFileData(path, line_sep, old_lines, new_lines) + + +def iter_rewritten( + file_patterns: config.PatternsByFile, + new_vinfo : version.V2VersionInfo, +) -> typ.Iterable[rewrite.RewrittenFileData]: + r'''Iterate over files with version string replaced. + + >>> version_pattern = "vYYYY0M.BUILD[-TAG]" + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} + >>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) + >>> rfd = list(rewritten_datas)[0] + >>> expected = [ + ... '# This file is part of the pycalver project', + ... '# https://github.com/mbarkhau/pycalver', + ... '#', + ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', + ... '# SPDX-License-Identifier: MIT', + ... '"""PyCalVer: CalVer for Python Packages."""', + ... '', + ... '__version__ = "v201809.0123"', + ... '', + ... ] + >>> assert rfd.new_lines == expected + ''' + + fobj: typ.IO[str] + + for file_path, patterns in rewrite.iter_path_patterns_items(file_patterns): + with file_path.open(mode="rt", encoding="utf-8") as fobj: + content = fobj.read() + + rfd = rfd_from_content(patterns, new_vinfo, content) + yield rfd._replace(path=str(file_path)) + + +def diff( + old_vinfo : version.V2VersionInfo, + new_vinfo : version.V2VersionInfo, + file_patterns: config.PatternsByFile, +) -> str: + r"""Generate diffs of rewritten files. + + >>> old_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + >>> new_vinfo = v2version.parse_version_info("v201810.1124", version_pattern) + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} + >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) + >>> lines = diff_str.split("\n") + >>> lines[:2] + ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] + >>> assert lines[6].startswith('-__version__ = "v2') + >>> assert not lines[6].startswith('-__version__ = "v201810.1124"') + >>> lines[7] + '+__version__ = "v201810.1124"' + + >>> file_patterns = {"LICENSE": ['Copyright (c) 2018-YYYY']} + >>> diff_str = diff(old_vinfo, new_vinfo, file_patterns) + >>> assert not diff_str + """ + + full_diff = "" + fobj: typ.IO[str] + + for file_path, patterns in sorted(rewrite.iter_path_patterns_items(file_patterns)): + with file_path.open(mode="rt", encoding="utf-8") as fobj: + content = fobj.read() + + patterns_with_change = 0 + for pattern in patterns: + old_str = v2version.format_version(old_vinfo, pattern.raw_pattern) + new_str = v2version.format_version(new_vinfo, pattern.raw_pattern) + if old_str != new_str: + patterns_with_change += 1 + + try: + rfd = rfd_from_content(patterns, new_vinfo, content) + except rewrite.NoPatternMatch: + # pylint:disable=raise-missing-from ; we support py2, so not an option + errmsg = f"No patterns matched for '{file_path}'" + raise rewrite.NoPatternMatch(errmsg) + + rfd = rfd._replace(path=str(file_path)) + lines = rewrite.diff_lines(rfd) + if len(lines) == 0 and patterns_with_change > 0: + errmsg = f"No patterns matched for '{file_path}'" + raise rewrite.NoPatternMatch(errmsg) + + full_diff += "\n".join(lines) + "\n" + + full_diff = full_diff.rstrip("\n") + return full_diff + + +def rewrite_files( + file_patterns: config.PatternsByFile, + new_vinfo : version.V2VersionInfo, +) -> None: + """Rewrite project files, updating each with the new version.""" + fobj: typ.IO[str] + + for file_data in iter_rewritten(file_patterns, new_vinfo): + new_content = file_data.line_sep.join(file_data.new_lines) + with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj: + fobj.write(new_content) diff --git a/src/pycalver2/version.py b/src/pycalver/v2version.py similarity index 62% rename from src/pycalver2/version.py rename to src/pycalver/v2version.py index 21f2edd..72c7e9b 100644 --- a/src/pycalver2/version.py +++ b/src/pycalver/v2version.py @@ -9,102 +9,28 @@ import typing as typ import logging import datetime as dt -import lexid +from . import version +from . import 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.v2version") -# The test suite may replace this. -TODAY = dt.datetime.utcnow().date() +CalInfo = typ.Union[version.V2CalendarInfo, version.V2VersionInfo] -ZERO_VALUES = { - 'MAJOR': "0", - 'MINOR': "0", - 'PATCH': "0", - 'TAG' : "final", - 'PYTAG': "", - 'NUM' : "0", -} +def _is_later_than(old: CalInfo, new: CalInfo) -> bool: + """Is old > new based on non None fields.""" + for field in version.V1CalendarInfo._fields: + aval = getattr(old, field) + bval = getattr(new, field) + if not (aval is None or bval is None): + if aval > bval: + return True + return False -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', -# } - - -MaybeInt = typ.Optional[int] - - -class CalendarInfo(typ.NamedTuple): - """Container for calendar components of version strings.""" - - year_y : MaybeInt - year_g : MaybeInt - quarter: MaybeInt - month : MaybeInt - dom : MaybeInt - doy : MaybeInt - week_w : MaybeInt - week_u : MaybeInt - week_v : MaybeInt - - -class VersionInfo(typ.NamedTuple): - """Container for parsed version string.""" - - 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 - num : int - bid : str - tag : str - pytag : str - - -def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo: - return CalendarInfo( +def _ver_to_cal_info(vinfo: version.V2VersionInfo) -> version.V2CalendarInfo: + return version.V2CalendarInfo( vinfo.year_y, vinfo.year_g, vinfo.quarter, @@ -117,32 +43,7 @@ def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo: ) -def _date_from_doy(year: int, doy: int) -> dt.date: - """Parse date from year and day of year (1 indexed). - - >>> cases = [ - ... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30), - ... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29), - ... ] - >>> dates = [_date_from_doy(year, month) for year, month in cases] - >>> assert [(d.month, d.day) for d in dates] == [ - ... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1), - ... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1), - ... ] - """ - return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1) - - -def _quarter_from_month(month: int) -> int: - """Calculate quarter (1 indexed) from month (1 indexed). - - >>> [_quarter_from_month(month) for month in range(1, 13)] - [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4] - """ - return ((month - 1) // 3) + 1 - - -def cal_info(date: dt.date = None) -> CalendarInfo: +def cal_info(date: dt.date = None) -> version.V2CalendarInfo: """Generate calendar components for current date. >>> import datetime as dt @@ -164,12 +65,12 @@ def cal_info(date: dt.date = None) -> CalendarInfo: (2019, 2, 4, 7, 97, 13, 14, 14) """ if date is None: - date = TODAY + date = version.TODAY kwargs = { 'year_y' : date.year, 'year_g' : int(date.strftime("%G"), base=10), - 'quarter': _quarter_from_month(date.month), + 'quarter': version.quarter_from_month(date.month), 'month' : date.month, 'dom' : date.day, 'doy' : int(date.strftime("%j"), base=10), @@ -178,10 +79,12 @@ def cal_info(date: dt.date = None) -> CalendarInfo: 'week_v' : int(date.strftime("%V"), base=10), } - return CalendarInfo(**kwargs) + return version.V2CalendarInfo(**kwargs) -VALID_FIELD_KEYS = set(VersionInfo._fields) | {'version'} +VALID_FIELD_KEYS = set(version.V2VersionInfo._fields) | {'version'} + +MaybeInt = typ.Optional[int] FieldKey = str MatchGroupKey = str @@ -190,44 +93,46 @@ MatchGroupStr = str PatternGroups = typ.Dict[FieldKey, MatchGroupStr] FieldValues = typ.Dict[FieldKey, MatchGroupStr] +VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] -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) +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') - >>> 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) + >>> 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') - >>> vnfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"}) - >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.doy) + >>> vinfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"}) + >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy) (2018, 6, 15, 166) - >>> vnfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"}) - >>> (vnfo.major, vnfo.minor, vnfo.patch) + >>> vinfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"}) + >>> (vinfo.major, vinfo.minor, vinfo.patch) (1, 23, 45) - >>> vnfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"}) - >>> (vnfo.major, vnfo.minor, vnfo.patch) + >>> vinfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"}) + >>> (vinfo.major, vinfo.minor, vinfo.patch) (1, 23, 45) - >>> vnfo = _parse_version_info({'year_y': "2021", 'week_w': "02"}) - >>> (vnfo.year_y, vnfo.week_w) + >>> vinfo = _parse_version_info({'year_y': "2021", 'week_w': "02"}) + >>> (vinfo.year_y, vinfo.week_w) (2021, 2) - >>> vnfo = _parse_version_info({'year_y': "2021", 'week_u': "02"}) - >>> (vnfo.year_y, vnfo.week_u) + >>> vinfo = _parse_version_info({'year_y': "2021", 'week_u': "02"}) + >>> (vinfo.year_y, vinfo.week_u) (2021, 2) - >>> vnfo = _parse_version_info({'year_g': "2021", 'week_v': "02"}) - >>> (vnfo.year_g, vnfo.week_v) + >>> vinfo = _parse_version_info({'year_g': "2021", 'week_v': "02"}) + >>> (vinfo.year_g, vinfo.week_v) (2021, 2) - >>> vnfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) - >>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.tag) + >>> vinfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"}) + >>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.tag) (2021, 1, 3, 'final') - >>> (vnfo.year_y, vnfo.week_w, vnfo.year_y, vnfo.week_u,vnfo.year_g, vnfo.week_v) + >>> (vinfo.year_y, vinfo.week_w, vinfo.year_y, vinfo.week_u,vinfo.year_g, vinfo.week_v) (2021, 0, 2021, 1, 2020, 53) """ for key in field_values: @@ -238,9 +143,9 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: pytag = fvals.get('pytag') or "" if tag and not pytag: - pytag = PEP440_TAG_BY_TAG[tag] + pytag = version.PEP440_TAG_BY_TAG[tag] elif pytag and not tag: - tag = TAG_BY_PEP440_TAG[pytag] + tag = version.TAG_BY_PEP440_TAG[pytag] date: typ.Optional[dt.date] = None @@ -256,7 +161,7 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: 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) + date = version.date_from_doy(year_y, doy) month = date.month dom = date.day else: @@ -279,7 +184,7 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: quarter = int(fvals['quarter']) if 'quarter' in fvals else None if quarter is None and month: - quarter = _quarter_from_month(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) @@ -288,7 +193,7 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: num = int(fvals.get('num' ) or 0) bid = fvals['bid'] if 'bid' in fvals else "1000" - vnfo = VersionInfo( + vinfo = version.V2VersionInfo( year_y=year_y, year_g=year_g, quarter=quarter, @@ -306,74 +211,69 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo: tag=tag, pytag=pytag, ) - return vnfo + return vinfo -VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]] +def parse_version_info( + version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG[NUM]]" +) -> version.V2VersionInfo: + """Parse normalized V2VersionInfo. - -class PatternError(Exception): - pass - - -def parse_version_info(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG[NUM]]") -> VersionInfo: - """Parse normalized VersionInfo. - - >>> vnfo = parse_version_info("v201712.0033-beta0", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> vinfo = parse_version_info("v201712.0033-beta0", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta", 'num': 0} - >>> assert vnfo == _parse_version_info(fvals) + >>> assert vinfo == _parse_version_info(fvals) - >>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"} - >>> assert vnfo == _parse_version_info(fvals) + >>> assert vinfo == _parse_version_info(fvals) - >>> vnfo = parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} - >>> assert vnfo == _parse_version_info(fvals) + >>> assert vinfo == _parse_version_info(fvals) - >>> vnfo = parse_version_info("1.23.456", pattern="MAJOR.MINOR.PATCH") + >>> vinfo = parse_version_info("1.23.456", raw_pattern="MAJOR.MINOR.PATCH") >>> fvals = {'major': "1", 'minor': "23", 'patch': "456"} - >>> assert vnfo == _parse_version_info(fvals) + >>> assert vinfo == _parse_version_info(fvals) """ - pattern_tup = v2patterns.compile_pattern(pattern) - match = pattern_tup.regexp.match(version_str) + pattern = v2patterns.compile_pattern(raw_pattern) + match = pattern.regexp.match(version_str) if match is None: err_msg = ( f"Invalid version string '{version_str}' " - f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'" + f"for pattern '{raw_pattern}'/'{pattern.regexp.pattern}'" ) - raise PatternError(err_msg) + raise version.PatternError(err_msg) else: field_values = match.groupdict() return _parse_version_info(field_values) -def is_valid(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool: +def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool: """Check if a version matches a pattern. - >>> is_valid("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]") + >>> is_valid("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]") True - >>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH") + >>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH") False - >>> is_valid("1.2.3", pattern="MAJOR.MINOR.PATCH") + >>> is_valid("1.2.3", raw_pattern="MAJOR.MINOR.PATCH") True - >>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH") + >>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH") False """ try: - parse_version_info(version_str, pattern) + parse_version_info(version_str, raw_pattern) return True - except PatternError: + except version.PatternError: return False TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]] -def _format_part_values(vinfo: VersionInfo) -> typ.Dict[str, str]: - """Generate kwargs for template from minimal VersionInfo. +def _format_part_values(vinfo: version.V2VersionInfo) -> typ.Dict[str, str]: + """Generate kwargs for template from minimal V2VersionInfo. - The VersionInfo Tuple only has the minimal representation + The V2VersionInfo 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'. @@ -402,18 +302,20 @@ def _format_part_values(vinfo: VersionInfo) -> typ.Dict[str, str]: return kwargs -def _make_segments(pattern: str) -> typ.List[str]: +def _make_segments(raw_pattern: str) -> typ.List[str]: pattern_segs_l: typ.List[str] = [] pattern_segs_r: typ.List[str] = [] - pattern_rest = pattern + pattern_rest = raw_pattern while "[" in pattern_rest and "]" in pattern_rest: try: seg_l , pattern_rest = pattern_rest.split("[", 1) pattern_rest, seg_r = pattern_rest.rsplit("]", 1) except ValueError as val_err: if "values to unpack" in str(val_err): - pat_err = PatternError(f"Unbalanced braces [] in '{pattern}'") + err = f"Unbalanced braces [] in '{raw_pattern}'" + pat_err = version.PatternError(err) + pat_err.__cause__ = val_err raise pat_err else: @@ -444,7 +346,7 @@ def _clear_zero_segments( def _format_segments( - vinfo : VersionInfo, + vinfo : version.V2VersionInfo, pattern_segs: typ.List[str], ) -> typ.List[str]: kwargs = _format_part_values(vinfo) @@ -470,12 +372,12 @@ def _format_segments( for part, part_value in part_values: if part in seg_l: seg_l = seg_l.replace(part, part_value) - if not (is_optional and str(part_value) == ZERO_VALUES.get(part)): + if not (is_optional and str(part_value) == version.ZERO_VALUES.get(part)): is_zero_segment[idx_l] = False if part in seg_r: seg_r = seg_r.replace(part, part_value) - if not (is_optional and str(part_value) == ZERO_VALUES[part]): + if not (is_optional and str(part_value) == version.ZERO_VALUES[part]): is_zero_segment[idx_r] = False formatted_segs_l.append(seg_l) @@ -489,7 +391,7 @@ def _format_segments( return _clear_zero_segments(formatted_segs, is_zero_segment) -def format_version(vinfo: VersionInfo, pattern: str) -> str: +def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: """Generate version string. >>> import datetime as dt @@ -575,7 +477,7 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str: >>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]"') '__version__ = "v1.0.0-rc2"' """ - pattern_segs = _make_segments(pattern) + pattern_segs = _make_segments(raw_pattern) formatted_segs = _format_segments(vinfo, pattern_segs) return "".join(formatted_segs) @@ -583,9 +485,9 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str: def incr( old_version: str, - pattern : str = "vYYYY0M.BUILD[-TAG]", + raw_pattern: str = "vYYYY0M.BUILD[-TAG]", *, - release : str = None, + release : typ.Optional[str] = None, major : bool = False, minor : bool = False, patch : bool = False, @@ -593,44 +495,30 @@ def incr( ) -> typ.Optional[str]: """Increment version string. - 'old_version' is assumed to be a string that matches 'pattern' + 'old_version' is assumed to be a string that matches 'raw_pattern' """ try: - old_vinfo = parse_version_info(old_version, pattern) - except PatternError as ex: + old_vinfo = parse_version_info(old_version, raw_pattern) + except version.PatternError as ex: logger.error(str(ex)) return None - cur_vinfo = old_vinfo + cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() - cur_cal_nfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() - - old_date = (old_vinfo.year_y or 0 , old_vinfo.month or 0 , old_vinfo.dom or 0) - cur_date = (cur_cal_nfo.year_y or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0) - - if old_date <= cur_date: - cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict()) - else: + if _is_later_than(old_vinfo, cur_cinfo): logger.warning(f"Version appears to be from the future '{old_version}'") + cur_vinfo = old_vinfo + else: + cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict()) - _bid = cur_vinfo.bid - if int(_bid) < 1000: - # prevent truncation of leading zeros - _bid = str(int(_bid) + 1000) - - cur_vinfo = cur_vinfo._replace(bid=lexid.incr(_bid)) - - if major: - cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) - if minor: - cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) - if patch: - cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) - - if release: - cur_vinfo = cur_vinfo._replace(tag=release) - - new_version = format_version(cur_vinfo, pattern) + cur_vinfo = version.incr_non_cal_parts( + cur_vinfo, + release, + major, + minor, + patch, + ) + new_version = format_version(cur_vinfo, raw_pattern) if new_version == old_version: logger.error("Invalid arguments or pattern, version did not change.") return None diff --git a/src/pycalver/version.py b/src/pycalver/version.py index d11c56e..76a9acb 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -1,30 +1,13 @@ -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -"""Functions related to version string manipulation.""" - import typing as typ -import logging import datetime as dt import lexid import pkg_resources -import pycalver.patterns as v1patterns - -logger = logging.getLogger("pycalver.version") - - -# The test suite may replace this. -TODAY = dt.datetime.utcnow().date() - - MaybeInt = typ.Optional[int] -class CalendarInfo(typ.NamedTuple): +class V1CalendarInfo(typ.NamedTuple): """Container for calendar components of version strings.""" year : MaybeInt @@ -36,7 +19,7 @@ class CalendarInfo(typ.NamedTuple): us_week : MaybeInt -class VersionInfo(typ.NamedTuple): +class V1VersionInfo(typ.NamedTuple): """Container for parsed version string.""" year : MaybeInt @@ -53,26 +36,94 @@ class VersionInfo(typ.NamedTuple): tag : str -def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo: - return CalendarInfo( - vinfo.year, - vinfo.quarter, - vinfo.month, - vinfo.dom, - vinfo.doy, - vinfo.iso_week, - vinfo.us_week, - ) +class V2CalendarInfo(typ.NamedTuple): + """Container for calendar components of version strings.""" + + year_y : MaybeInt + year_g : MaybeInt + quarter: MaybeInt + month : MaybeInt + dom : MaybeInt + doy : MaybeInt + week_w : MaybeInt + week_u : MaybeInt + week_v : MaybeInt -def _date_from_doy(year: int, doy: int) -> dt.date: +class V2VersionInfo(typ.NamedTuple): + """Container for parsed version string.""" + + 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 + num : int + bid : str + tag : str + pytag : str + + +VersionInfoType = typ.TypeVar('VersionInfoType', V1VersionInfo, V2VersionInfo) + + +# The test suite may replace this. +TODAY = dt.datetime.utcnow().date() + + +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()) + + +ZERO_VALUES = { + 'MAJOR': "0", + 'MINOR': "0", + 'PATCH': "0", + 'TAG' : "final", + 'PYTAG': "", + 'NUM' : "0", +} + + +class PatternError(Exception): + pass + + +def date_from_doy(year: int, doy: int) -> dt.date: """Parse date from year and day of year (1 indexed). >>> cases = [ ... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30), ... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29), ... ] - >>> dates = [_date_from_doy(year, month) for year, month in cases] + >>> dates = [date_from_doy(year, month) for year, month in cases] >>> assert [(d.month, d.day) for d in dates] == [ ... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1), ... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1), @@ -81,407 +132,15 @@ def _date_from_doy(year: int, doy: int) -> dt.date: return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1) -def _quarter_from_month(month: int) -> int: +def quarter_from_month(month: int) -> int: """Calculate quarter (1 indexed) from month (1 indexed). - >>> [_quarter_from_month(month) for month in range(1, 13)] + >>> [quarter_from_month(month) for month in range(1, 13)] [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4] """ return ((month - 1) // 3) + 1 -def cal_info(date: dt.date = None) -> CalendarInfo: - """Generate calendar components for current date. - - >>> from datetime import date - - >>> c = cal_info(date(2019, 1, 5)) - >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 1, 1, 5, 5, 0, 0) - - >>> c = cal_info(date(2019, 1, 6)) - >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 1, 1, 6, 6, 0, 1) - - >>> c = cal_info(date(2019, 1, 7)) - >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 1, 1, 7, 7, 1, 1) - - >>> c = cal_info(date(2019, 4, 7)) - >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week) - (2019, 2, 4, 7, 97, 13, 14) - """ - if date is None: - date = TODAY - - kwargs = { - 'year' : date.year, - 'quarter' : _quarter_from_month(date.month), - 'month' : date.month, - 'dom' : date.day, - 'doy' : int(date.strftime("%j"), base=10), - 'iso_week': int(date.strftime("%W"), base=10), - 'us_week' : int(date.strftime("%U"), base=10), - } - - return CalendarInfo(**kwargs) - - -FieldKey = str -MatchGroupKey = str -MatchGroupStr = str - -PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr] -FieldValues = typ.Dict[FieldKey , MatchGroupStr] - - -def _parse_field_values(field_values: FieldValues) -> VersionInfo: - fvals = field_values - tag = fvals.get('tag') - if tag is None: - tag = "final" - tag = TAG_ALIASES.get(tag, tag) - assert tag is not None - - bid = fvals['bid'] if 'bid' in fvals else "0001" - - year = int(fvals['year']) if 'year' in fvals else None - doy = int(fvals['doy' ]) if 'doy' in fvals 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(fvals['month']) if 'month' in fvals else None - dom = int(fvals['dom' ]) if 'dom' in fvals 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(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( - 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. - - >>> _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]] - - -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 v1patterns.COMPOSITE_PART_PATTERNS - or part_name in v1patterns.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 v1patterns.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) - else: - return dict(field_value_items) - - -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"}) - >>> (vnfo.year, 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, 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 = "{pycalver}") -> VersionInfo: - """Parse normalized VersionInfo. - - >>> vnfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}") - >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}) - - >>> vnfo = parse_version_info("1.23.456", pattern="{semver}") - >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"}) - """ - pattern_tup = v1patterns.compile_pattern(pattern) - match = pattern_tup.regexp.match(version_str) - if match is None: - err_msg = ( - f"Invalid version string '{version_str}' " - f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'" - ) - raise PatternError(err_msg) - else: - return _parse_version_info(match.groupdict()) - - -def is_valid(version_str: str, pattern: str = "{pycalver}") -> 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 - """ - try: - parse_version_info(version_str, pattern) - return True - except PatternError: - return False - - -ID_FIELDS_BY_PART = { - 'MAJOR' : 'major', - 'MINOR' : 'minor', - 'MM' : 'minor', - 'MMM' : 'minor', - 'MMMM' : 'minor', - 'MMMMM' : 'minor', - 'MMMMMM' : 'minor', - 'MMMMMMM': 'minor', - 'PATCH' : 'patch', - 'PP' : 'patch', - 'PPP' : 'patch', - 'PPPP' : 'patch', - 'PPPPP' : 'patch', - 'PPPPPP' : 'patch', - 'PPPPPPP': 'patch', - 'BID' : 'bid', - 'BB' : 'bid', - 'BBB' : 'bid', - 'BBBB' : 'bid', - 'BBBBB' : 'bid', - 'BBBBBB' : 'bid', - 'BBBBBBB': 'bid', -} - - -def format_version(vinfo: VersionInfo, pattern: str) -> str: - """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()) - >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') - - >>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}") - 'v17.33-beta' - >>> format_version(vinfo_a, pattern="{pep440_version}") - '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="v{year}w{iso_week}.{BID}{release}") - 'v2017w00.33-beta' - >>> format_version(vinfo_b, pattern="v{year}w{iso_week}.{BID}{release}") - '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_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="v{MAJOR}.{MINOR}.{PATCH}") - 'v1.2.34' - >>> format_version(vinfo_c, pattern="v{MAJOR}.{MM}.{PPP}") - 'v1.02.034' - """ - full_pattern = pattern - for part_name, full_part_format in v1patterns.FULL_PART_FORMATS.items(): - full_pattern = full_pattern.replace("{" + part_name + "}", full_part_format) - - kwargs: typ.Dict[str, typ.Union[str, int, None]] = vinfo._asdict() - - tag = vinfo.tag - if tag == 'final': - kwargs['release' ] = "" - kwargs['pep440_tag'] = "" - else: - kwargs['release' ] = "-" + tag - kwargs['pep440_tag'] = PEP440_TAGS[tag] + "0" - - kwargs['release_tag'] = tag - - year = vinfo.year - if year: - kwargs['yy' ] = str(year)[-2:] - kwargs['yyyy'] = year - - kwargs['BID'] = 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 full_pattern.format(**kwargs) - - -def incr( - old_version: str, - pattern : str = "{pycalver}", - *, - release : str = None, - major : bool = False, - minor : bool = False, - patch : bool = False, - pin_date: bool = False, -) -> typ.Optional[str]: - """Increment version string. - - 'old_version' is assumed to be a string that matches 'pattern' - """ - try: - old_vinfo = parse_version_info(old_version, pattern) - except PatternError as ex: - logger.error(str(ex)) - return None - - cur_vinfo = old_vinfo - - cur_cal_nfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info() - - old_date = (old_vinfo.year or 0 , old_vinfo.month or 0 , old_vinfo.dom or 0) - cur_date = (cur_cal_nfo.year or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0) - - if old_date <= cur_date: - cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict()) - else: - logger.warning(f"Version appears to be from the future '{old_version}'") - - cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid)) - - if major: - cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0) - if minor: - cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0) - if patch: - cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) - - if release: - cur_vinfo = cur_vinfo._replace(tag=release) - - new_version = format_version(cur_vinfo, pattern) - if new_version == old_version: - logger.error("Invalid arguments or pattern, version did not change.") - return None - else: - return new_version - - def to_pep440(version: str) -> str: """Derive pep440 compliant version string from PyCalVer version string. @@ -489,3 +148,28 @@ def to_pep440(version: str) -> str: '201811.7b0' """ return str(pkg_resources.parse_version(version)) + + +def incr_non_cal_parts( + vinfo : VersionInfoType, + release: typ.Optional[str], + major : bool, + minor : bool, + patch : bool, +) -> VersionInfoType: + _bid = vinfo.bid + if int(_bid) < 1000: + # prevent truncation of leading zeros + _bid = str(int(_bid) + 1000) + + vinfo = vinfo._replace(bid=lexid.next_id(_bid)) + + if release: + vinfo = vinfo._replace(tag=release) + if major: + vinfo = vinfo._replace(major=vinfo.major + 1, minor=0, patch=0) + if minor: + vinfo = vinfo._replace(minor=vinfo.minor + 1, patch=0) + if patch: + vinfo = vinfo._replace(patch=vinfo.patch + 1) + return vinfo diff --git a/src/pycalver2/__init__.py b/src/pycalver2/__init__.py deleted file mode 100644 index 094d463..0000000 --- a/src/pycalver2/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -"""PyCalVer: CalVer for Python Packages.""" - -__version__ = "v202007.1036" diff --git a/src/pycalver2/rewrite.py b/src/pycalver2/rewrite.py deleted file mode 100644 index 03c1f86..0000000 --- a/src/pycalver2/rewrite.py +++ /dev/null @@ -1,178 +0,0 @@ -# This file is part of the pycalver project -# https://github.com/mbarkhau/pycalver -# -# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License -# SPDX-License-Identifier: MIT -"""Rewrite files, updating occurences of version strings.""" - -import io -import typing as typ -import logging - -import pycalver.rewrite as v1rewrite -import pycalver2.version as v2version -import pycalver2.patterns as v2patterns -from pycalver import parse -from pycalver import config - -logger = logging.getLogger("pycalver2.rewrite") - - -def rewrite_lines( - pattern_strs: typ.List[str], - new_vinfo : v2version.VersionInfo, - old_lines : typ.List[str], -) -> typ.List[str]: - """Replace occurances of pattern_strs in old_lines with new_vinfo. - - >>> new_vinfo = v2version.parse_version_info("v201811.0123-beta") - >>> pattern_strs = ['__version__ = "vYYYY0M.BUILD[-TAG]"'] - >>> old_lines = ['__version__ = "v201809.0002-alpha" '] - >>> rewrite_lines(pattern_strs, new_vinfo, old_lines) - ['__version__ = "v201811.0123-beta" '] - - >>> old_lines = ['__version__ = "v201809.0002-alpha" # comment'] - >>> rewrite_lines(pattern_strs, new_vinfo, old_lines) - ['__version__ = "v201811.0123-beta" # comment'] - - >>> pattern_strs = ['__version__ = "YYYY0M.BLD[PYTAGNUM]"'] - >>> old_lines = ['__version__ = "201809.2a0"'] - >>> rewrite_lines(pattern_strs, new_vinfo, old_lines) - ['__version__ = "201811.123b0"'] - """ - new_lines = old_lines[:] - found_patterns = set() - - patterns = [v2patterns.compile_pattern(p) for p in pattern_strs] - matches = parse.iter_matches(old_lines, patterns) - for match in matches: - found_patterns.add(match.pattern.raw) - replacement = v2version.format_version(new_vinfo, match.pattern.raw) - span_l, span_r = match.span - new_line = match.line[:span_l] + replacement + match.line[span_r:] - new_lines[match.lineno] = new_line - - non_matched_patterns = set(pattern_strs) - found_patterns - if non_matched_patterns: - for non_matched_pattern in non_matched_patterns: - logger.error(f"No match for pattern '{non_matched_pattern}'") - compiled_pattern_str = v2patterns.compile_pattern_str(non_matched_pattern) - logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'") - raise v1rewrite.NoPatternMatch("Invalid pattern(s)") - else: - return new_lines - - -def rfd_from_content( - pattern_strs: typ.List[str], - new_vinfo : v2version.VersionInfo, - content : str, -) -> v1rewrite.RewrittenFileData: - r"""Rewrite pattern occurrences with version string. - - >>> new_vinfo = v2version.parse_version_info("v201809.0123") - >>> pattern_strs = ['__version__ = "vYYYY0M.BUILD[-TAG]"'] - >>> content = '__version__ = "v201809.0001-alpha"' - >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - >>> rfd.new_lines - ['__version__ = "v201809.0123"'] - >>> - >>> new_vinfo = v2version.parse_version_info("v1.2.3", "vMAJOR.MINOR.PATCH") - >>> pattern_strs = ['__version__ = "vMAJOR.MINOR.PATCH"'] - >>> content = '__version__ = "v1.2.2"' - >>> rfd = rfd_from_content(pattern_strs, new_vinfo, content) - >>> rfd.new_lines - ['__version__ = "v1.2.3"'] - """ - line_sep = v1rewrite.detect_line_sep(content) - old_lines = content.split(line_sep) - new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines) - return v1rewrite.RewrittenFileData("", line_sep, old_lines, new_lines) - - -def iter_rewritten( - file_patterns: config.PatternsByGlob, - new_vinfo : v2version.VersionInfo, -) -> typ.Iterable[v1rewrite.RewrittenFileData]: - r'''Iterate over files with version string replaced. - - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} - >>> new_vinfo = v2version.parse_version_info("v201809.0123") - >>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo) - >>> rfd = list(rewritten_datas)[0] - >>> assert rfd.new_lines == [ - ... '# This file is part of the pycalver project', - ... '# https://github.com/mbarkhau/pycalver', - ... '#', - ... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License', - ... '# SPDX-License-Identifier: MIT', - ... '"""PyCalVer: CalVer for Python Packages."""', - ... '', - ... '__version__ = "v201809.0123"', - ... '', - ... ] - >>> - ''' - - fobj: typ.IO[str] - - for file_path, pattern_strs in v1rewrite.iter_file_paths(file_patterns): - with file_path.open(mode="rt", encoding="utf-8") as fobj: - content = fobj.read() - - rfd = rfd_from_content(pattern_strs, new_vinfo, content) - yield rfd._replace(path=str(file_path)) - - -def diff( - new_vinfo : v2version.VersionInfo, - file_patterns: config.PatternsByGlob, -) -> str: - r"""Generate diffs of rewritten files. - - >>> new_vinfo = v2version.parse_version_info("v201809.0123") - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']} - >>> diff_str = diff(new_vinfo, file_patterns) - >>> lines = diff_str.split("\n") - >>> lines[:2] - ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] - >>> assert lines[6].startswith('-__version__ = "v2') - >>> assert not lines[6].startswith('-__version__ = "v201809.0123"') - >>> lines[7] - '+__version__ = "v201809.0123"' - """ - - full_diff = "" - fobj: typ.IO[str] - - for file_path, pattern_strs in sorted(v1rewrite.iter_file_paths(file_patterns)): - with file_path.open(mode="rt", encoding="utf-8") as fobj: - content = fobj.read() - - try: - rfd = rfd_from_content(pattern_strs, new_vinfo, content) - except v1rewrite.NoPatternMatch: - # pylint:disable=raise-missing-from ; we support py2, so not an option - errmsg = f"No patterns matched for '{file_path}'" - raise v1rewrite.NoPatternMatch(errmsg) - - rfd = rfd._replace(path=str(file_path)) - lines = v1rewrite.diff_lines(rfd) - if len(lines) == 0: - errmsg = f"No patterns matched for '{file_path}'" - raise v1rewrite.NoPatternMatch(errmsg) - - full_diff += "\n".join(lines) + "\n" - - full_diff = full_diff.rstrip("\n") - return full_diff - - -def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: v2version.VersionInfo) -> None: - """Rewrite project files, updating each with the new version.""" - fobj: typ.IO[str] - - for file_data in iter_rewritten(file_patterns, new_vinfo): - new_content = file_data.line_sep.join(file_data.new_lines) - with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj: - fobj.write(new_content) diff --git a/test/test_cli.py b/test/test_cli.py index 118ff55..1983c89 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -13,8 +13,8 @@ import pytest import pathlib2 as pl from click.testing import CliRunner -import pycalver.config as config -import pycalver.patterns as v1patterns +from pycalver import config +from pycalver import v1patterns from pycalver.__main__ import cli # pylint:disable=redefined-outer-name ; pytest fixtures diff --git a/test/test_parse.py b/test/test_parse.py index ebd90e4..33ec7b6 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -4,8 +4,8 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import unicode_literals -import pycalver.patterns as v1patterns from pycalver import parse +from pycalver import v1patterns SETUP_PY_FIXTURE = """ # setup.py diff --git a/test/test_patterns.py b/test/test_patterns.py index cf96507..37c3b45 100644 --- a/test/test_patterns.py +++ b/test/test_patterns.py @@ -8,8 +8,8 @@ import re import pytest -import pycalver.patterns as v1patterns -import pycalver2.patterns as v2patterns +from pycalver import v1patterns +from pycalver import v2patterns # TODO (mb 2020-09-06): test for v2patterns diff --git a/test/test_rewrite.py b/test/test_rewrite.py index c9107fe..9b2e61e 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -8,10 +8,11 @@ import copy from test import util from pycalver import config -from pycalver import rewrite as v1rewrite -from pycalver import version as v1version -from pycalver2 import rewrite as v2rewrite -from pycalver2 import version as v2version +from pycalver import rewrite +from pycalver import v1rewrite +from pycalver import v1version +from pycalver import v2rewrite +from pycalver import v2version # pylint:disable=protected-access ; allowed for test code @@ -54,7 +55,7 @@ def test_iter_file_paths(): cfg = config.parse(ctx) assert cfg - _paths_and_patterns = v1rewrite.iter_file_paths(cfg.file_patterns) + _paths_and_patterns = rewrite.iter_path_patterns_items(cfg.file_patterns) file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns} assert file_paths == {"pycalver.toml", "README.md"} @@ -66,7 +67,7 @@ def test_iter_file_globs(): cfg = config.parse(ctx) assert cfg - _paths_and_patterns = v1rewrite.iter_file_paths(cfg.file_patterns) + _paths_and_patterns = rewrite.iter_path_patterns_items(cfg.file_patterns) file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns} assert file_paths == { @@ -86,7 +87,7 @@ def test_error_bad_path(): (project.dir / "setup.py").unlink() try: - list(v1rewrite.iter_file_paths(cfg.file_patterns)) + list(rewrite.iter_path_patterns_items(cfg.file_patterns)) assert False, "expected IOError" except IOError as ex: assert "setup.py" in str(ex) @@ -102,10 +103,11 @@ def test_error_bad_pattern(): patterns["setup.py"] = patterns["setup.py"][0] + "invalid" try: + old_vinfo = v1version.parse_version_info("v201808.0233") new_vinfo = v1version.parse_version_info("v201809.1234") - list(v1rewrite.diff(new_vinfo, patterns)) - assert False, "expected v1rewrite.NoPatternMatch" - except v1rewrite.NoPatternMatch as ex: + list(v1rewrite.diff(old_vinfo, new_vinfo, patterns)) + assert False, "expected rewrite.NoPatternMatch" + except rewrite.NoPatternMatch as ex: assert "setup.py" in str(ex) diff --git a/test/test_version.py b/test/test_version.py index ee010a5..db90f24 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -9,9 +9,10 @@ import datetime as dt import pytest -import pycalver.version as v1version -import pycalver2.version as v2version -import pycalver.patterns as v1patterns +from pycalver import version +from pycalver import v1version +from pycalver import v2version +from pycalver import v1patterns # import pycalver2.patterns as v2patterns @@ -51,7 +52,7 @@ def test_bump_random(monkeypatch): cur_date = dt.date(2016, 1, 1) + dt.timedelta(days=random.randint(1, 2000)) cur_version = cur_date.strftime("v%Y%m") + ".0001-dev" - monkeypatch.setattr(v1version, 'TODAY', cur_date) + monkeypatch.setattr(version, 'TODAY', cur_date) for _ in range(1000): cur_date += dt.timedelta(days=int((1 + random.random()) ** 10)) @@ -120,7 +121,7 @@ def test_parse_error_empty(): try: v1version.parse_version_info("") assert False - except v1version.PatternError as err: + except version.PatternError as err: assert "Invalid version string" in str(err) @@ -128,7 +129,7 @@ def test_parse_error_noprefix(): try: v1version.parse_version_info("201809.0002") assert False - except v1version.PatternError as err: + except version.PatternError as err: assert "Invalid version string" in str(err) @@ -136,7 +137,7 @@ def test_parse_error_nopadding(): try: v1version.parse_version_info("v201809.2b0") assert False - except v1version.PatternError as err: + except version.PatternError as err: assert "Invalid version string" in str(err) @@ -151,7 +152,7 @@ def test_part_field_mapping_v1(): assert not any(b_extra_names), sorted(b_extra_names) a_fields = set(v1patterns.PATTERN_PART_FIELDS.values()) - b_fields = set(v1version.VersionInfo._fields) + b_fields = set(version.V1VersionInfo._fields) a_extra_fields = a_fields - b_fields b_extra_fields = b_fields - a_fields @@ -200,7 +201,7 @@ def test_v1_parse_versions(pattern_str, line, expected_vinfo): # def test_v2_parse_versions(pattern_str, line, expected_vinfo): def test_v2_parse_versions(): - _vnfo = v2version.parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]") + _vnfo = v2version.parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]") fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} assert _vnfo == v2version._parse_version_info(fvals) @@ -217,24 +218,24 @@ def test_make_segments(): def test_v2_format_version(): - pattern = "vYYYY0M.BUILD[-TAG[NUM]]" + version_pattern = "vYYYY0M.BUILD[-TAG[NUM]]" in_version = "v200701.0033-beta" - vinfo = v2version.parse_version_info(in_version, pattern=pattern) - out_version = v2version.format_version(vinfo, pattern=pattern) + vinfo = v2version.parse_version_info(in_version, raw_pattern=version_pattern) + out_version = v2version.format_version(vinfo, raw_pattern=version_pattern) assert in_version == out_version - result = v2version.format_version(vinfo, pattern="v0Y.BUILD[-TAG]") + result = v2version.format_version(vinfo, raw_pattern="v0Y.BUILD[-TAG]") assert result == "v07.0033-beta" - result = v2version.format_version(vinfo, pattern="vYY.BLD[-TAG]") + result = v2version.format_version(vinfo, raw_pattern="vYY.BLD[-TAG]") assert result == "v7.33-beta" - result = v2version.format_version(vinfo, pattern="vYY.BLD-TAG") + result = v2version.format_version(vinfo, raw_pattern="vYY.BLD-TAG") assert result == "v7.33-beta" - result = v2version.format_version(vinfo, pattern='__version__ = "YYYY.BUILD[-TAG]"') + result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BUILD[-TAG]"') assert result == '__version__ = "2007.0033-beta"' - result = v2version.format_version(vinfo, pattern='__version__ = "YYYY.BLD"') + result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BLD"') assert result == '__version__ = "2007.33"'