From 49e19fbf8915504e8c8c95f1c85c814a7024dad3 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Fri, 2 Oct 2020 20:52:54 +0000 Subject: [PATCH] much bugfixing --- pylint-ignore.md | 104 +++++++--------- setup.cfg | 2 +- src/pycalver/__main__.py | 45 +++---- src/pycalver/config.py | 60 +++++---- src/pycalver/parse.py | 39 +++++- src/pycalver/v1cli.py | 8 -- src/pycalver/v1patterns.py | 33 ++--- src/pycalver/v1rewrite.py | 64 ++-------- src/pycalver/v1version.py | 12 +- src/pycalver/v2cli.py | 8 -- src/pycalver/v2patterns.py | 25 ++-- src/pycalver/v2rewrite.py | 69 ++--------- src/pycalver/v2version.py | 248 ++++++++++++++++++++++--------------- src/pycalver/vcs.py | 4 +- src/pycalver/version.py | 4 + test/test_cli.py | 118 ++++++++++++++---- test/test_config.py | 69 +++++++---- test/test_rewrite.py | 226 ++++++++++++++++++++++++++++----- 18 files changed, 687 insertions(+), 451 deletions(-) diff --git a/pylint-ignore.md b/pylint-ignore.md index ed68081..9bbd051 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -23,7 +23,7 @@ The recommended approach to using `pylint-ignore` is: # Overview - - [W0511: fixme (9x)](#w0511-fixme) + - [W0511: fixme (8x)](#w0511-fixme) - [W0703: broad-except (1x)](#w0703-broad-except) @@ -44,37 +44,20 @@ The recommended approach to using `pylint-ignore` is: ``` -## File src/pycalver/vcs.py - Line 78 - W0511 (fixme) +## File src/pycalver/vcs.py - Line 80 - W0511 (fixme) - `message: TODO (mb 2018-11-15): Detect encoding of output? Use chardet?` - `author : Manuel Barkhau ` - `date : 2020-09-18T17:24:49` ``` - 68: def __call__(self, cmd_name: str, env: Env = None, **kwargs: str) -> str: + 69: def __call__(self, cmd_name: str, env: Env = None, **kwargs: str) -> str: ... - 76: output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT) - 77: -> 78: # TODO (mb 2018-11-15): Detect encoding of output? Use chardet? - 79: _encoding = "utf-8" - 80: return output_data.decode(_encoding) -``` - - -## 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[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] - 158: # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] + 78: output_data: bytes = sp.check_output(cmd_parts, env=env, stderr=sp.STDOUT) + 79: +> 80: # TODO (mb 2018-11-15): Detect encoding of output? Use chardet? + 81: _encoding = "utf-8" + 82: return output_data.decode(_encoding) ``` @@ -95,37 +78,37 @@ The recommended approach to using `pylint-ignore` is: ``` -## File src/pycalver/v1patterns.py - Line 214 - W0511 (fixme) +## File test/test_config.py - Line 170 - W0511 (fixme) -- `message: TODO (mb 2020-09-19): replace {version} etc with version_pattern` +- `message: TODO (mb 2020-09-18):` - `author : Manuel Barkhau ` -- `date : 2020-09-19T16:24:10` +- `date : 2020-09-18T19:04:06` ``` - 201: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: + 156: def test_parse_v2_cfg(): ... - 212: escaped_pattern = escaped_pattern.replace(char, escaped) - 213: -> 214: # TODO (mb 2020-09-19): replace {version} etc with version_pattern - 215: pattern_str = _replace_pattern_parts(escaped_pattern) - 216: return re.compile(pattern_str) + 168: assert "setup.cfg" in cfg.file_patterns + 169: +> 170: # TODO (mb 2020-09-18): + 171: # raw_patterns_by_file = _parse_raw_patterns_by_file(cfg) + 172: # assert raw_patterns_by_file["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] ``` -## File src/pycalver/__main__.py - Line 250 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 259 - W0511 (fixme) - `message: TODO (mb 2020-09-18): Investigate error messages` - `author : Manuel Barkhau ` - `date : 2020-09-19T16:24:10` ``` - 222: def _bump( + 231: def _bump( ... - 248: sys.exit(1) - 249: except Exception as ex: -> 250: # TODO (mb 2020-09-18): Investigate error messages - 251: logger.error(str(ex)) - 252: sys.exit(1) + 257: sys.exit(1) + 258: except Exception as ex: +> 259: # TODO (mb 2020-09-18): Investigate error messages + 260: logger.error(str(ex)) + 261: sys.exit(1) ``` @@ -146,53 +129,52 @@ The recommended approach to using `pylint-ignore` is: ``` -## File test/test_cli.py - Line 536 - W0511 (fixme) +## File test/test_cli.py - Line 599 - 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 + 597: + 598: # def test_custom_commit_message(runner): +> 599: # # TODO (mb 2020-09-18): + 600: # assert False ``` -## File src/pycalver/v2version.py - Line 551 - W0511 (fixme) +## File src/pycalver/v2version.py - Line 616 - W0511 (fixme) - `message: TODO (mb 2020-09-20): New Rollover Behaviour:` - `author : Manuel Barkhau ` - `date : 2020-09-20T17:36:38` ``` - 508: def incr( + 578: def incr( ... - 549: cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) - 550: -> 551: # TODO (mb 2020-09-20): New Rollover Behaviour: - 552: # Reset major, minor, patch to zero if any part to the left of it is incremented - 553: + 614: ) + 615: +> 616: # TODO (mb 2020-09-20): New Rollover Behaviour: + 617: # Reset major, minor, patch to zero if any part to the left of it is incremented + 618: ``` # W0703: broad-except -## File src/pycalver/__main__.py - Line 249 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 258 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 222: def _bump( + 231: def _bump( ... - 247: logger.error(str(ex)) - 248: sys.exit(1) -> 249: except Exception as ex: - 250: # TODO (mb 2020-09-18): Investigate error messages - 251: logger.error(str(ex)) + 256: logger.error(str(ex)) + 257: sys.exit(1) +> 258: except Exception as ex: + 259: # TODO (mb 2020-09-18): Investigate error messages + 260: logger.error(str(ex)) ``` - diff --git a/setup.cfg b/setup.cfg index 22e319c..3e3ebc7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -137,7 +137,7 @@ output-format = colorized max-locals = 21 # Maximum number of arguments for function / method -max-args = 9 +max-args = 12 good-names = logger,i,ex diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 04ae1f6..1d474de 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -22,7 +22,9 @@ from . import v2cli from . import config from . import rewrite from . import version +from . import v1rewrite from . import v1version +from . import v2rewrite from . import v2version from . import v1patterns @@ -93,10 +95,10 @@ def cli(verbose: int = 0) -> None: f"{', '.join(VALID_RELEASE_VALUES)}." ), ) -@click.option("--major", is_flag=True, default=False, help="Increment major component.") -@click.option("-m", "--minor", is_flag=True, default=False, help="Increment minor component.") -@click.option("-p", "--patch", is_flag=True, default=False, help="Increment patch component.") -@click.option("-r", "--release-num", is_flag=True, default=False, help="Increment release number.") +@click.option("--major" , is_flag=True, default=False, help="Increment major component.") +@click.option("-m" , "--minor" , is_flag=True, default=False, help="Increment minor component.") +@click.option("-p" , "--patch" , is_flag=True, default=False, help="Increment patch component.") +@click.option("-r" , "--release-num", is_flag=True, default=False, help="Increment release number.") @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") def test( old_version: str, @@ -145,8 +147,7 @@ def show(verbose: int = 0, fetch: bool = True) -> None: """Show current version of your project.""" _configure_logging(verbose=max(_VERBOSE, verbose)) - ctx: config.ProjectContext = config.init_project_ctx(project_path=".") - cfg: config.MaybeConfig = config.parse(ctx) + _, cfg = config.init(project_path=".") if cfg is None: logger.error("Could not parse configuration. Perhaps try 'pycalver init'.") @@ -188,7 +189,7 @@ def _print_diff(cfg: config.Config, new_version: str) -> None: 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)) + logger.error(str(ex), exc_info=True) sys.exit(1) @@ -196,12 +197,12 @@ def _incr( old_version: str, raw_pattern: str, *, - release : str = None, - major : bool = False, - minor : bool = False, - patch : bool = False, + release : str = None, + major : bool = False, + minor : bool = False, + patch : bool = False, release_num: bool = False, - pin_date: bool = False, + pin_date : bool = False, ) -> typ.Optional[str]: v1_parts = list(v1patterns.PART_PATTERNS) + list(v1patterns.FULL_PART_FORMATS) has_v1_part = any("{" + part + "}" in raw_pattern for part in v1_parts) @@ -250,9 +251,11 @@ def _bump( try: if cfg.is_new_pattern: - v2cli.rewrite_files(cfg, new_version) + new_v2_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) + v2rewrite.rewrite_files(cfg.file_patterns, new_v2_vinfo) else: - v1cli.rewrite_files(cfg, new_version) + new_v1_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) + v1rewrite.rewrite_files(cfg.file_patterns, new_v1_vinfo) except rewrite.NoPatternMatch as ex: logger.error(str(ex)) sys.exit(1) @@ -291,8 +294,7 @@ def init(verbose: int = 0, dry: bool = False) -> None: """Initialize [pycalver] configuration.""" _configure_logging(verbose=max(_VERBOSE, verbose)) - ctx: config.ProjectContext = config.init_project_ctx(project_path=".") - cfg: config.MaybeConfig = config.parse(ctx) + ctx, cfg = config.init(project_path=".") if cfg: logger.error(f"Configuration already initialized in {ctx.config_rel_path}") @@ -355,10 +357,10 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: "to files with version strings." ), ) -@click.option("--major", is_flag=True, default=False, help="Increment major component.") -@click.option("-m", "--minor", is_flag=True, default=False, help="Increment minor component.") -@click.option("-p", "--patch", is_flag=True, default=False, help="Increment patch component.") -@click.option("-r", "--release-num", is_flag=True, default=False, help="Increment release number.") +@click.option("--major" , is_flag=True, default=False, help="Increment major component.") +@click.option("-m" , "--minor" , is_flag=True, default=False, help="Increment minor component.") +@click.option("-p" , "--patch" , is_flag=True, default=False, help="Increment patch component.") +@click.option("-r" , "--release-num", is_flag=True, default=False, help="Increment release number.") @click.option("--pin-date", is_flag=True, default=False, help="Leave date components unchanged.") def bump( release : typ.Optional[str] = None, @@ -379,8 +381,7 @@ def bump( if release: _validate_release_tag(release) - ctx: config.ProjectContext = config.init_project_ctx(project_path=".") - cfg: config.MaybeConfig = config.parse(ctx) + _, cfg = config.init(project_path=".") if cfg is None: logger.error("Could not parse configuration. Perhaps try 'pycalver init'.") diff --git a/src/pycalver/config.py b/src/pycalver/config.py index ecfb774..717709e 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -188,6 +188,8 @@ def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig: raw_cfg['file_patterns'] = dict(_parse_cfg_file_patterns(cfg_parser)) + _set_raw_config_defaults(raw_cfg) + return raw_cfg @@ -198,6 +200,8 @@ def _parse_toml(cfg_buffer: typ.IO[str]) -> RawConfig: for option, default_val in BOOL_OPTIONS.items(): raw_cfg[option] = raw_cfg.get(option, default_val) + _set_raw_config_defaults(raw_cfg) + return raw_cfg @@ -227,9 +231,7 @@ def _compile_v1_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsIt 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 = [ - v1patterns.compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns - ] + compiled_patterns = v1patterns.compile_patterns(version_pattern, raw_patterns) yield filepath, compiled_patterns @@ -242,9 +244,7 @@ def _compile_v2_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsIt 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 - ] + compiled_patterns = v2patterns.compile_patterns(version_pattern, raw_patterns) yield filepath, compiled_patterns @@ -319,12 +319,7 @@ def _parse_config(raw_cfg: RawConfig) -> Config: return cfg -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() - +def _parse_current_version_default_pattern(raw_cfg: RawConfig, raw_cfg_text: str) -> str: is_pycalver_section = False for line in raw_cfg_text.splitlines(): if is_pycalver_section and line.startswith("current_version"): @@ -340,19 +335,7 @@ def _parse_current_version_default_pattern(ctx: ProjectContext, raw_cfg: RawConf 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) - +def _set_raw_config_defaults(raw_cfg: RawConfig) -> None: 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']}" @@ -370,10 +353,27 @@ def _parse_raw_config(ctx: ProjectContext) -> RawConfig: if 'file_patterns' not in raw_cfg: raw_cfg['file_patterns'] = {} + +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 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) + with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj: + raw_cfg_text = fobj.read() + + raw_version_pattern = _parse_current_version_default_pattern(raw_cfg, raw_cfg_text) raw_cfg['file_patterns'][ctx.config_rel_path] = [raw_version_pattern] return raw_cfg @@ -393,6 +393,14 @@ def parse(ctx: ProjectContext) -> MaybeConfig: return None +def init( + project_path: typ.Union[str, pl.Path, None] = "." +) -> typ.Tuple[ProjectContext, MaybeConfig]: + ctx = init_project_ctx(project_path) + cfg = parse(ctx) + return (ctx, cfg) + + DEFAULT_CONFIGPARSER_BASE_TMPL = """ [pycalver] current_version = "{initial_version}" diff --git a/src/pycalver/parse.py b/src/pycalver/parse.py index 0b80519..205c01a 100644 --- a/src/pycalver/parse.py +++ b/src/pycalver/parse.py @@ -9,14 +9,43 @@ import typing as typ from .patterns import Pattern +LineNo = int +Start = int +End = int + + +class LineSpan(typ.NamedTuple): + lineno: LineNo + start : Start + end : End + + +LineSpans = typ.List[LineSpan] + + +def _has_overlap(needle: LineSpan, haystack: LineSpans) -> bool: + for span in haystack: + # assume needle is in the center + has_overlap = ( + span.lineno == needle.lineno + # needle starts before (or at) span end + and needle.start <= span.end + # needle ends after (or at) span start + and needle.end >= span.start + ) + if has_overlap: + return True + + return False + class PatternMatch(typ.NamedTuple): """Container to mark a version string in a file.""" - lineno : int # zero based + lineno : LineNo # zero based line : str pattern: Pattern - span : typ.Tuple[int, int] + span : typ.Tuple[Start, End] match : str @@ -47,6 +76,10 @@ def iter_matches(lines: typ.List[str], patterns: typ.List[Pattern]) -> PatternMa ... match = "v201712.0002-alpha", ... ) """ + matched_spans: LineSpans = [] for pattern in patterns: for match in _iter_for_pattern(lines, pattern): - yield match + needle_span = LineSpan(match.lineno, *match.span) + if not _has_overlap(needle_span, matched_spans): + yield match + matched_spans.append(needle_span) diff --git a/src/pycalver/v1cli.py b/src/pycalver/v1cli.py index b34d0b2..47c3c2d 100755 --- a/src/pycalver/v1cli.py +++ b/src/pycalver/v1cli.py @@ -42,14 +42,6 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C ) -def rewrite_files( - cfg : config.Config, - new_version: str, -) -> None: - new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern) - v1rewrite.rewrite_files(cfg.file_patterns, new_vinfo) - - def get_diff(cfg: config.Config, new_version: str) -> str: old_vinfo = v1version.parse_version_info(cfg.current_version, cfg.version_pattern) new_vinfo = v1version.parse_version_info(new_version , cfg.version_pattern) diff --git a/src/pycalver/v1patterns.py b/src/pycalver/v1patterns.py index 8569674..f3d22ce 100644 --- a/src/pycalver/v1patterns.py +++ b/src/pycalver/v1patterns.py @@ -81,8 +81,8 @@ PART_PATTERNS = { 'month' : r"(?:0[0-9]|1[0-2])", 'month_short': r"(?:1[0-2]|[1-9])", 'build_no' : r"\d{4,}", - 'pep440_tag' : r"(?:post|dev|rc|a|b)?\d*", - 'tag' : r"(?:preview|final|alpha|beta|post|pre|dev|rc|a|b|c|r)", + '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]", @@ -198,26 +198,29 @@ def _init_composite_patterns() -> None: _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}}'") - +def _compile_pattern_re(normalized_pattern: str) -> typ.Pattern[str]: 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) @utils.memo 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) + _raw_pattern = version_pattern if raw_pattern is None else raw_pattern + 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}}'") + + regexp = _compile_pattern_re(normalized_pattern) + return Pattern(version_pattern, normalized_pattern, regexp) + + +def compile_patterns(version_pattern: str, raw_patterns: typ.List[str]) -> typ.List[Pattern]: + return [compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns] diff --git a/src/pycalver/v1rewrite.py b/src/pycalver/v1rewrite.py index 1bf652c..e2f6313 100644 --- a/src/pycalver/v1rewrite.py +++ b/src/pycalver/v1rewrite.py @@ -24,19 +24,7 @@ def rewrite_lines( 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"'] - """ + """Replace occurances of patterns in old_lines with new_vinfo.""" found_patterns: typ.Set[Pattern] = set() new_lines = old_lines[:] @@ -65,10 +53,12 @@ def rfd_from_content( ) -> rewrite.RewrittenFileData: r"""Rewrite pattern occurrences with version string. - >>> from .v1patterns import compile_pattern - >>> patterns = [compile_pattern("{pycalver}", '__version__ = "{pycalver}"'] + >>> version_pattern = "{pycalver}" >>> new_vinfo = v1version.parse_version_info("v201809.0123") + >>> from .v1patterns import compile_pattern + >>> patterns = [compile_pattern(version_pattern, '__version__ = "{pycalver}"')] + >>> content = '__version__ = "v201809.0001-alpha"' >>> rfd = rfd_from_content(patterns, new_vinfo, content) >>> rfd.new_lines @@ -92,26 +82,7 @@ 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 - ''' + """Iterate over files with version string replaced.""" fobj: typ.IO[str] @@ -128,24 +99,7 @@ def diff( 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 - """ + """Generate diffs of rewritten files.""" full_diff = "" fobj: typ.IO[str] @@ -165,13 +119,13 @@ def diff( 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}'" + errmsg = f"No patterns matched for file '{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}'" + errmsg = f"No patterns matched for file '{file_path}'" raise rewrite.NoPatternMatch(errmsg) full_diff += "\n".join(lines) + "\n" diff --git a/src/pycalver/v1version.py b/src/pycalver/v1version.py index 7659d8e..defc4a9 100644 --- a/src/pycalver/v1version.py +++ b/src/pycalver/v1version.py @@ -374,12 +374,12 @@ def incr( old_version: str, raw_pattern: str = "{pycalver}", *, - release : typ.Optional[str] = None, - major : bool = False, - minor : bool = False, - patch : bool = False, + release : typ.Optional[str] = None, + major : bool = False, + minor : bool = False, + patch : bool = False, release_num: bool = False, - pin_date: bool = False, + pin_date : bool = False, ) -> typ.Optional[str]: """Increment version string. @@ -408,7 +408,7 @@ def incr( if patch: cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1) if release_num: - cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1) + raise NotImplementedError("--release-num not supported for old style patterns") if release: cur_vinfo = cur_vinfo._replace(tag=release) diff --git a/src/pycalver/v2cli.py b/src/pycalver/v2cli.py index f29ecac..88f23a4 100644 --- a/src/pycalver/v2cli.py +++ b/src/pycalver/v2cli.py @@ -42,14 +42,6 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C ) -def rewrite_files( - cfg : config.Config, - new_version: str, -) -> None: - new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern) - v2rewrite.rewrite_files(cfg.file_patterns, new_vinfo) - - def get_diff(cfg: config.Config, new_version: str) -> str: old_vinfo = v2version.parse_version_info(cfg.current_version, cfg.version_pattern) new_vinfo = v2version.parse_version_info(new_version , cfg.version_pattern) diff --git a/src/pycalver/v2patterns.py b/src/pycalver/v2patterns.py index 5306209..6e33891 100644 --- a/src/pycalver/v2patterns.py +++ b/src/pycalver/v2patterns.py @@ -8,7 +8,6 @@ >>> pattern = compile_pattern("vYYYY0M.BUILD[-RELEASE]") >>> version_info = pattern.regexp.match("v201712.0123-alpha") >>> assert version_info.groupdict() == { -... "version": "v201712.0123-alpha", ... "year_y" : "2017", ... "month" : "12", ... "bid" : "0123", @@ -23,7 +22,6 @@ >>> version_info = pattern.regexp.match("v201712.1234") >>> assert version_info.groupdict() == { -... "version": "v201712.1234", ... "year_y" : "2017", ... "month" : "12", ... "bid" : "1234", @@ -251,6 +249,13 @@ def _convert_to_pep440(version_pattern: str) -> str: else: pep440_pattern = pep440_pattern.replace(part_name, substitution) + # PYTAG and NUM must be adjacent and also be the last (optional) part + if 'PYTAGNUM' not in pep440_pattern: + pep440_pattern = pep440_pattern.replace("PYTAG", "") + pep440_pattern = pep440_pattern.replace("NUM" , "") + pep440_pattern = pep440_pattern.replace("[]" , "") + pep440_pattern += "[PYTAGNUM]" + return pep440_pattern @@ -304,9 +309,8 @@ def _replace_pattern_parts(pattern: str) -> str: return result_pattern -def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]: - normalized_pattern = normalize_pattern(version_pattern, raw_pattern) - escaped_pattern = normalized_pattern +def _compile_pattern_re(normalized_pattern: str) -> typ.Pattern[str]: + escaped_pattern = normalized_pattern for char, escaped in RE_PATTERN_ESCAPES: # [] braces are used for optional parts, such as [-RELEASE]/[-beta] # and need to be escaped manually. @@ -321,6 +325,11 @@ def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[s @utils.memo 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) + _raw_pattern = version_pattern if raw_pattern is None else raw_pattern + normalized_pattern = normalize_pattern(version_pattern, _raw_pattern) + regexp = _compile_pattern_re(normalized_pattern) + return Pattern(version_pattern, normalized_pattern, regexp) + + +def compile_patterns(version_pattern: str, raw_patterns: typ.List[str]) -> typ.List[Pattern]: + return [compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns] diff --git a/src/pycalver/v2rewrite.py b/src/pycalver/v2rewrite.py index ce1013c..a6d85bd 100644 --- a/src/pycalver/v2rewrite.py +++ b/src/pycalver/v2rewrite.py @@ -25,23 +25,7 @@ def rewrite_lines( 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[-RELEASE]" - >>> 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"'] - """ + """Replace occurances of patterns in old_lines with new_vinfo.""" found_patterns: typ.Set[Pattern] = set() new_lines = old_lines[:] @@ -73,10 +57,10 @@ def rfd_from_content( ) -> rewrite.RewrittenFileData: r"""Rewrite pattern occurrences with version string. + >>> from .v2patterns import compile_pattern >>> version_pattern = "vYYYY0M.BUILD[-RELEASE]" >>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) - >>> raw_patterns = ['__version__ = "vYYYY0M.BUILD[-RELEASE]"'] - >>> patterns = + >>> patterns = [compile_pattern(version_pattern, '__version__ = "vYYYY0M.BUILD[-RELEASE]"')] >>> content = '__version__ = "v201809.0001-alpha"' >>> rfd = rfd_from_content(patterns, new_vinfo, content) >>> rfd.new_lines @@ -84,8 +68,7 @@ def rfd_from_content( >>> version_pattern = "vMAJOR.MINOR.PATCH" >>> new_vinfo = v2version.parse_version_info("v1.2.3", version_pattern) - >>> raw_patterns = ['__version__ = "vMAJOR.MINOR.PATCH"'] - >>> patterns = + >>> patterns = [compile_pattern(version_pattern, '__version__ = "vMAJOR.MINOR.PATCH"')] >>> content = '__version__ = "v1.2.2"' >>> rfd = rfd_from_content(patterns, new_vinfo, content) >>> rfd.new_lines @@ -113,26 +96,7 @@ 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[-RELEASE]" - >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-RELEASE]"']} - >>> 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 - ''' + """Iterate over files with version string replaced.""" fobj: typ.IO[str] @@ -149,24 +113,7 @@ def diff( 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[-RELEASE]"']} - >>> 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 - """ + r"""Generate diffs of rewritten files.""" full_diff = "" fobj: typ.IO[str] @@ -179,7 +126,7 @@ def diff( 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}'" + errmsg = f"No patterns matched for file '{file_path}'" raise rewrite.NoPatternMatch(errmsg) rfd = rfd._replace(path=str(file_path)) @@ -187,7 +134,7 @@ def diff( patterns_with_change = _patterns_with_change(old_vinfo, new_vinfo, patterns) if len(lines) == 0 and patterns_with_change > 0: - errmsg = f"No patterns matched for '{file_path}'" + errmsg = f"No patterns matched for file '{file_path}'" raise rewrite.NoPatternMatch(errmsg) full_diff += "\n".join(lines) + "\n" diff --git a/src/pycalver/v2version.py b/src/pycalver/v2version.py index a4ef187..764af2c 100644 --- a/src/pycalver/v2version.py +++ b/src/pycalver/v2version.py @@ -145,7 +145,7 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: assert key in VALID_FIELD_KEYS, key fvals = field_values - tag = fvals.get('tag' ) or "final" + tag = fvals.get('tag' ) or "" pytag = fvals.get('pytag') or "" if tag and not pytag: @@ -153,6 +153,9 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: elif pytag and not tag: tag = version.RELEASE_BY_PEP440_TAG[pytag] + if not tag: + tag = "final" + date: typ.Optional[dt.date] = None year_y: MaybeInt = int(fvals['year_y']) if 'year_y' in fvals else None @@ -221,22 +224,22 @@ def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo: def parse_version_info( - version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE[NUM]]" + version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE]" ) -> version.V2VersionInfo: """Parse normalized V2VersionInfo. - >>> vinfo = parse_version_info("v201712.0033-beta0", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") - >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta", 'num': 0} - >>> assert vinfo == _parse_version_info(fvals) - - >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"} >>> assert vinfo == _parse_version_info(fvals) - >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-RELEASE]") >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"} >>> assert vinfo == _parse_version_info(fvals) + >>> vinfo = parse_version_info("201712.33b0", raw_pattern="YYYY0M.BLD[PYTAGNUM]") + >>> fvals = {'year_y': 2017, 'month': 12, 'bid': "33", 'tag': "beta", 'num': 0} + >>> assert vinfo == _parse_version_info(fvals) + >>> vinfo = parse_version_info("1.23.456", raw_pattern="MAJOR.MINOR.PATCH") >>> fvals = {'major': "1", 'minor': "23", 'patch': "456"} >>> assert vinfo == _parse_version_info(fvals) @@ -291,14 +294,14 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues: It may for example have month=9, but not the formatted representation '09' for '0M'. - >>> vinfo = parse_version_info("v200709.1033-beta", pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> vinfo = parse_version_info("v200709.1033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") >>> kwargs = dict(_format_part_values(vinfo)) - >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['RELEASE[NUM]']) + >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['RELEASE']) ('2007', '09', '1033', 'beta') >>> (kwargs['YY'], kwargs['0Y'], kwargs['MM'], kwargs['PYTAG']) ('7', '07', '9', 'b') - >>> vinfo = parse_version_info("200709.1033b1", pattern="YYYY0M.BLD[PYTAGNUM]") + >>> vinfo = parse_version_info("200709.1033b1", raw_pattern="YYYY0M.BLD[PYTAGNUM]") >>> kwargs = dict(_format_part_values(vinfo)) >>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['PYTAG'], kwargs['NUM']) ('2007', '09', '1033', 'b', '1') @@ -386,141 +389,202 @@ def _parse_segment_tree(raw_pattern: str) -> SegmentTree: FormattedSegmentParts = typ.List[str] +class FormatedSeg(typ.NamedTuple): + is_literal: bool + is_zero : bool + result : str + + +def _format_segment(seg: Segment, part_values: PartValues) -> FormatedSeg: + zero_part_count = 0 + + # find all parts, regardless of zero value + used_parts: typ.List[typ.Tuple[str, str]] = [] + + for part, part_value in part_values: + if part in seg: + used_parts.append((part, part_value)) + if version.is_zero_val(part, part_value): + zero_part_count += 1 + + result = seg + # unescape braces + result = result.replace(r"\[", r"[") + result = result.replace(r"\]", r"]") + + for part, part_value in used_parts: + result = result.replace(part, part_value) + + # If a segment has no parts at all, it is a literal string + # (typically a prefix or sufix) and should be output as is. + is_literal_seg = len(used_parts) == 0 + if is_literal_seg: + return FormatedSeg(True, False, result) + elif zero_part_count > 0 and zero_part_count == len(used_parts): + # all zero, omit segment completely + return FormatedSeg(False, True, result) + else: + return FormatedSeg(False, False, result) + + def _format_segment_tree( seg_tree : SegmentTree, part_values: PartValues, -) -> FormattedSegmentParts: - result_parts = [] +) -> FormatedSeg: + # print("??>>>", seg_tree) + # NOTE (mb 2020-10-02): starting from the right, if there is any non-zero + # part, all further parts going left will be used. In other words, a part + # is only omitted, if all parts to the right of it were also omitted. + result_parts: typ.List[str] = [] + is_zero = True for seg in seg_tree: if isinstance(seg, list): - result_parts.extend(_format_segment_tree(seg, part_values)) + formatted_seg = _format_segment_tree(seg, part_values) else: - # If a segment has any non-zero parts, the whole segment is used. - non_zero_parts = 0 - formatted_seg = seg - # unescape braces - formatted_seg = formatted_seg.replace(r"\[", r"[") - formatted_seg = formatted_seg.replace(r"\]", r"]") - # replace non zero parts - for part, part_value in part_values: - if part in formatted_seg: - is_zero_part = ( - part in version.ZERO_VALUES and str(part_value) == version.ZERO_VALUES[part] - ) - if is_zero_part: - formatted_seg = formatted_seg.replace(part, "") - else: - non_zero_parts += 1 - formatted_seg = formatted_seg.replace(part, part_value) + formatted_seg = _format_segment(seg, part_values) - if non_zero_parts: - result_parts.append(formatted_seg) + if formatted_seg.is_literal: + result_parts.append(formatted_seg.result) + else: + is_zero = is_zero and formatted_seg.is_zero + result_parts.append(formatted_seg.result) - return result_parts + # print("<<<<", is_zero, result_parts) + result = "" if is_zero else "".join(result_parts) + return FormatedSeg(False, is_zero, result) def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str: """Generate version string. >>> import datetime as dt - >>> vinfo = parse_version_info("v200712.0033-beta", pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> vinfo = parse_version_info("v200712.0033-beta", raw_pattern="vYYYY0M.BUILD[-RELEASE]") >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2007, 1, 1))._asdict()) >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2007, 12, 31))._asdict()) - >>> format_version(vinfo_a, pattern="vYY.BLD[-PYTAGNUM]") + >>> format_version(vinfo_a, raw_pattern="vYY.BLD[-PYTAGNUM]") 'v7.33-b0' - >>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG[NUM]]") + >>> format_version(vinfo_a, raw_pattern="YYYY0M.BUILD[PYTAG[NUM]]") '200701.0033b' - >>> format_version(vinfo_a, pattern="vYY.BLD[-PYTAGNUM]") + >>> format_version(vinfo_a, raw_pattern="vYY.BLD[-PYTAGNUM]") 'v7.33-b0' - >>> format_version(vinfo_a, pattern="v0Y.BLD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="v0Y.BLD[-RELEASE[NUM]]") 'v07.33-beta' - >>> format_version(vinfo_a, pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") 'v200701.0033-beta' - >>> format_version(vinfo_b, pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vYYYY0M.BUILD[-RELEASE[NUM]]") 'v200712.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") 'v2007w01.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYwWW.BLD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYwWW.BLD[-RELEASE[NUM]]") 'v2007w1.33-beta' - >>> format_version(vinfo_b, pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vYYYYw0W.BUILD[-RELEASE[NUM]]") 'v2007w53.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") 'v2007d001.0033-beta' - >>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vYYYYdJJJ.BUILD[-RELEASE[NUM]]") 'v2007d1.0033-beta' - >>> format_version(vinfo_b, pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vYYYYd00J.BUILD[-RELEASE[NUM]]") 'v2007d365.0033-beta' - >>> format_version(vinfo_a, pattern="vGGGGwVV.BLD[PYTAGNUM]") + >>> format_version(vinfo_a, raw_pattern="vGGGGwVV.BLD[PYTAGNUM]") 'v2007w1.33b0' - >>> format_version(vinfo_a, pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_a, raw_pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") 'v2007w01.0033-beta' - >>> format_version(vinfo_b, pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_b, raw_pattern="vGGGGw0V.BUILD[-RELEASE[NUM]]") 'v2008w01.0033-beta' >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final') - >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-RELEASE") + >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD-RELEASE") 'v2007w53.0033-final' - >>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-RELEASE[NUM]]") + >>> format_version(vinfo_c, raw_pattern="vYYYYwWW.BUILD[-RELEASE[NUM]]") 'v2007w53.0033' - >>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH") + >>> format_version(vinfo_c, raw_pattern="vMAJOR.MINOR.PATCH") 'v1.2.34' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final') - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-RELEASENUM") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASENUM") 'v1.0.0-final0' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-RELEASE[NUM]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASE[NUM]") 'v1.0.0-final' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-RELEASE") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH-RELEASE") 'v1.0.0-final' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-RELEASE[NUM]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR.PATCH[-RELEASE[NUM]]") 'v1.0.0' - >>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-RELEASE[NUM]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR.MINOR[.PATCH[-RELEASE[NUM]]]") 'v1.0' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") 'v1' - >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=1, tag='rc', num=0) - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH]]") - 'v1.0.1' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") - 'v1.0.1-rc' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASENUM]]]") - 'v1.0.1-rc0' - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH]]") - 'v1.0.1' + >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=2, tag='rc', pytag='rc', num=0) + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH]]") + 'v1.0.2' + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") + 'v1.0.2-rc' + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[PYTAGNUM]]]") + 'v1.0.2rc0' + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH]]") + 'v1.0.2' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) - >>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") + >>> format_version(vinfo_d, raw_pattern="vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]") 'v1.0.0-rc2' >>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='rc', num=2) - >>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]"') + >>> format_version(vinfo_d, raw_pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-RELEASE[NUM]]]]"') '__version__ = "v1.0.0-rc2"' """ - part_values = _format_part_values(vinfo) - seg_tree = _parse_segment_tree(raw_pattern) - version_str_parts = _format_segment_tree(seg_tree, part_values) - return "".join(version_str_parts) + part_values = _format_part_values(vinfo) + seg_tree = _parse_segment_tree(raw_pattern) + formatted_seg = _format_segment_tree(seg_tree, part_values) + return formatted_seg.result + + +def _incr_numeric( + vinfo : version.V2VersionInfo, + major : bool, + minor : bool, + patch : bool, + release : typ.Optional[str], + release_num: bool, +) -> version.V2VersionInfo: + # prevent truncation of leading zeros + if int(vinfo.bid) < 1000: + vinfo = vinfo._replace(bid=str(int(vinfo.bid) + 1000)) + + vinfo = vinfo._replace(bid=lexid.next_id(vinfo.bid)) + + 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) + if release_num: + vinfo = vinfo._replace(num=vinfo.num + 1) + if release: + if release != vinfo.tag: + vinfo = vinfo._replace(num=0) + vinfo = vinfo._replace(tag=release) + return vinfo def incr( old_version: str, raw_pattern: str = "vYYYY0M.BUILD[-RELEASE[NUM]]", *, - release : typ.Optional[str] = None, - major : bool = False, - minor : bool = False, - patch : bool = False, + release : typ.Optional[str] = None, + major : bool = False, + minor : bool = False, + patch : bool = False, release_num: bool = False, - pin_date: bool = False, + pin_date : bool = False, ) -> typ.Optional[str]: """Increment version string. @@ -540,24 +604,14 @@ def incr( else: cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict()) - # prevent truncation of leading zeros - if int(cur_vinfo.bid) < 1000: - cur_vinfo = cur_vinfo._replace(bid=str(int(cur_vinfo.bid) + 1000)) - - 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_num: - cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1) - if release: - if release != cur_vinfo.tag: - cur_vinfo = cur_vinfo._replace(num=0) - cur_vinfo = cur_vinfo._replace(tag=release) + cur_vinfo = _incr_numeric( + cur_vinfo, + major=major, + minor=minor, + patch=patch, + release=release, + release_num=release_num, + ) # TODO (mb 2020-09-20): New Rollover Behaviour: # Reset major, minor, patch to zero if any part to the left of it is incremented diff --git a/src/pycalver/vcs.py b/src/pycalver/vcs.py index 5eed4ce..a1f062a 100644 --- a/src/pycalver/vcs.py +++ b/src/pycalver/vcs.py @@ -16,6 +16,7 @@ mercurial, then the git terms are used. For example "fetch" import os import sys +import shlex import typing as typ import logging import tempfile @@ -73,7 +74,8 @@ class VCSAPI: logger.info(cmd_str) else: logger.debug(cmd_str) - output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT) + cmd_parts = shlex.split(cmd_str) + output_data: bytes = sp.check_output(cmd_parts, env=env, stderr=sp.STDOUT) # TODO (mb 2018-11-15): Detect encoding of output? Use chardet? _encoding = "utf-8" diff --git a/src/pycalver/version.py b/src/pycalver/version.py index 9169cd2..dfe7c1f 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -119,6 +119,10 @@ ZERO_VALUES = { } +def is_zero_val(part: str, part_value: str) -> bool: + return part in ZERO_VALUES and part_value == ZERO_VALUES[part] + + class PatternError(Exception): pass diff --git a/test/test_cli.py b/test/test_cli.py index 308af00..75a83d6 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -4,7 +4,9 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import unicode_literals +import io import os +import re import time import shutil import subprocess as sp @@ -13,6 +15,8 @@ import pytest import pathlib2 as pl from click.testing import CliRunner +from pycalver import v1cli +from pycalver import v2cli from pycalver import config from pycalver import v1patterns from pycalver.__main__ import cli @@ -21,6 +25,12 @@ from pycalver.__main__ import cli # pylint:disable=protected-access ; allowed for test code +README_TEXT_FIXTURE = """ + Hello World v201701.1002-alpha ! + aka. 201701.1002a0 ! +""" + + SETUP_CFG_FIXTURE = """ [metadata] license_file = LICENSE @@ -110,31 +120,33 @@ def test_incr_pin_date(runner): def test_incr_semver(runner): - semver_pattern = "{MAJOR}.{MINOR}.{PATCH}" - old_version = "0.1.0" - new_version = "0.1.1" + semver_patterns = [ + "{semver}", + "{MAJOR}.{MINOR}.{PATCH}", + "MAJOR.MINOR.PATCH", + ] - result = runner.invoke(cli, ['test', "-vv", "--patch", old_version, "{semver}"]) - assert result.exit_code == 0 - assert f"Version: {new_version}\n" in result.output + for semver_pattern in semver_patterns: + old_version = "0.1.0" + new_version = "0.1.1" - result = runner.invoke(cli, ['test', "-vv", "--patch", old_version, semver_pattern]) - assert result.exit_code == 0 - assert f"Version: {new_version}\n" in result.output + result = runner.invoke(cli, ['test', "-vv", "--patch", old_version, semver_pattern]) + assert result.exit_code == 0 + assert f"Version: {new_version}\n" in result.output - old_version = "0.1.1" - new_version = "0.2.0" + old_version = "0.1.1" + new_version = "0.2.0" - result = runner.invoke(cli, ['test', "-vv", "--minor", old_version, semver_pattern]) - assert result.exit_code == 0 - assert f"Version: {new_version}\n" in result.output + result = runner.invoke(cli, ['test', "-vv", "--minor", old_version, semver_pattern]) + assert result.exit_code == 0 + assert f"Version: {new_version}\n" in result.output - old_version = "0.1.1" - new_version = "1.0.0" + old_version = "0.1.1" + new_version = "1.0.0" - result = runner.invoke(cli, ['test', "-vv", "--major", old_version, semver_pattern]) - assert result.exit_code == 0 - assert f"Version: {new_version}\n" in result.output + result = runner.invoke(cli, ['test', "-vv", "--major", old_version, semver_pattern]) + assert result.exit_code == 0 + assert f"Version: {new_version}\n" in result.output def test_incr_semver_invalid(runner, caplog): @@ -175,12 +187,8 @@ def test_incr_invalid(runner): def _add_project_files(*files): if "README.md" in files: - README_TEXT = """ - Hello World v201701.1002-alpha ! - aka. 201701.1002a0 ! - """ with pl.Path("README.md").open(mode="wt", encoding="utf-8") as fobj: - fobj.write(README_TEXT) + fobj.write(README_TEXT_FIXTURE) if "setup.cfg" in files: with pl.Path("setup.cfg").open(mode="wt", encoding="utf-8") as fobj: @@ -195,6 +203,22 @@ def _add_project_files(*files): fobj.write(PYPROJECT_TOML_FIXTURE) +def _update_config_val(filename, **kwargs): + with io.open(filename, mode="r", encoding="utf-8") as fobj: + old_cfg_text = fobj.read() + + new_cfg_text = old_cfg_text + for key, val in kwargs.items(): + replacement = "{} = {}\n".format(key, val) + if replacement not in new_cfg_text: + pattern = r"^{} = .*$".format(key) + new_cfg_text = re.sub(pattern, replacement, new_cfg_text, flags=re.MULTILINE) + assert old_cfg_text != new_cfg_text + + with io.open(filename, mode="w", encoding="utf-8") as fobj: + fobj.write(new_cfg_text) + + def test_nocfg(runner, caplog): _add_project_files("README.md") result = runner.invoke(cli, ['show', "-vv"]) @@ -491,7 +515,7 @@ setup.cfg = """ -def test_bump_semver_warning(runner, caplog): +def test_v1_bump_semver_warning(runner, caplog): _add_project_files("README.md") with pl.Path("setup.cfg").open(mode="w") as fobj: @@ -509,7 +533,7 @@ def test_bump_semver_warning(runner, caplog): assert result.exit_code == 0 -def test_bump_semver_diff(runner, caplog): +def test_v1_bump_semver_diff(runner, caplog): _add_project_files("README.md") with pl.Path("setup.cfg").open(mode="w") as fobj: @@ -531,6 +555,48 @@ def test_bump_semver_diff(runner, caplog): assert f"+current_version = \"{expected}\"" in out_lines +def test_v1_get_diff(runner): + _add_project_files("README.md", "setup.cfg") + result = runner.invoke(cli, ['init', "-vv"]) + assert result.exit_code == 0 + + _update_config_val("setup.cfg", version_pattern='"{pycalver}"') + + _, cfg = config.init() + new_version = "v202010.1003-beta" + diff_str = v1cli.get_diff(cfg, new_version) + diff_lines = set(diff_str.splitlines()) + + assert "- Hello World v201701.1002-alpha !" in diff_lines + assert "- aka. 201701.1002a0 !" in diff_lines + assert "+ Hello World v202010.1003-beta !" in diff_lines + assert "+ aka. 202010.1003b0 !" in diff_lines + + assert '-current_version = "v202010.1001-alpha"' in diff_lines + assert '+current_version = "v202010.1003-beta"' in diff_lines + + +def test_v2_get_diff(runner): + _add_project_files("README.md", "setup.cfg") + result = runner.invoke(cli, ['init', "-vv"]) + assert result.exit_code == 0 + + _update_config_val("setup.cfg", version_pattern='"vYYYY0M.BUILD[-RELEASE]"') + + _, cfg = config.init() + new_version = "v202010.1003-beta" + diff_str = v2cli.get_diff(cfg, new_version) + diff_lines = set(diff_str.splitlines()) + + assert "- Hello World v201701.1002-alpha !" in diff_lines + assert "- aka. 201701.1002a0 !" in diff_lines + assert "+ Hello World v202010.1003-beta !" in diff_lines + assert "+ aka. 202010.1003b0 !" in diff_lines + + assert '-current_version = "v202010.1001-alpha"' in diff_lines + assert '+current_version = "v202010.1003-beta"' in diff_lines + + # def test_custom_commit_message(runner): # # TODO (mb 2020-09-18): # assert False diff --git a/test/test_config.py b/test/test_config.py index 5953f8d..09807aa 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -90,6 +90,13 @@ def mk_buf(text): return buf +def _parse_raw_patterns_by_filepath(cfg): + return { + filepath: [pattern.raw_pattern for pattern in patterns] + for filepath, patterns in cfg.file_patterns.items() + } + + def test_parse_toml_1(): buf = mk_buf(PYCALVER_TOML_FIXTURE_1) @@ -103,8 +110,10 @@ def test_parse_toml_1(): assert cfg.push is True assert "pycalver.toml" in cfg.file_patterns - assert cfg.file_patterns["README.md" ] == ["{pycalver}", "{pep440_pycalver}"] - assert cfg.file_patterns["pycalver.toml"] == ['current_version = "{pycalver}"'] + + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath["README.md" ] == ["{pycalver}", "{pep440_pycalver}"] + assert raw_patterns_by_filepath["pycalver.toml"] == ['current_version = "{pycalver}"'] def test_parse_toml_2(): @@ -120,8 +129,10 @@ def test_parse_toml_2(): assert cfg.push is False assert "pycalver.toml" in cfg.file_patterns - assert cfg.file_patterns["README.md" ] == ["{semver}", "{semver}"] - assert cfg.file_patterns["pycalver.toml"] == ['current_version = "{semver}"'] + + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath["README.md" ] == ["{semver}", "{semver}"] + assert raw_patterns_by_filepath["pycalver.toml"] == ['current_version = "{semver}"'] def test_parse_v1_cfg(): @@ -136,8 +147,10 @@ def test_parse_v1_cfg(): assert cfg.push is True assert "setup.cfg" in cfg.file_patterns - assert cfg.file_patterns["setup.py" ] == ["{pycalver}", "{pep440_pycalver}"] - assert cfg.file_patterns["setup.cfg"] == ['current_version = "{pycalver}"'] + + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath["setup.py" ] == ["{pycalver}", "{pep440_pycalver}"] + assert raw_patterns_by_filepath["setup.cfg"] == ['current_version = "{pycalver}"'] def test_parse_v2_cfg(): @@ -153,10 +166,12 @@ def test_parse_v2_cfg(): assert "setup.py" in cfg.file_patterns assert "setup.cfg" in cfg.file_patterns + # TODO (mb 2020-09-18): - # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] - # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] - # assert cfg.file_patterns["src/project/*.py"] == ['Copyright (c) 2018-YYYY"'] + # raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + # assert raw_patterns_by_filepath["setup.py" ] == ["vYYYY0M.BUILD[-RELEASE]", "YYYY0M.BLD[PYTAGNUM]"] + # assert raw_patterns_by_filepath["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-RELEASE]"'] + # assert raw_patterns_by_filepath["src/project/*.py"] == ['Copyright (c) 2018-YYYY"'] def test_parse_default_toml(): @@ -186,8 +201,9 @@ def test_parse_default_cfg(): def test_parse_project_toml(): - project_path = util.FIXTURES_DIR / "project_a" - config_path = util.FIXTURES_DIR / "project_a" / "pycalver.toml" + project_path = util.FIXTURES_DIR / "project_a" + config_path = util.FIXTURES_DIR / "project_a" / "pycalver.toml" + config_rel_path = "pycalver.toml" with config_path.open() as fobj: config_data = fobj.read() @@ -195,7 +211,7 @@ def test_parse_project_toml(): assert "v201710.0123-alpha" in config_data ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, config_path, "toml", None) + assert ctx == config.ProjectContext(project_path, config_path, config_rel_path, "toml", None) cfg = config.parse(ctx) @@ -210,8 +226,9 @@ def test_parse_project_toml(): def test_parse_project_cfg(): - project_path = util.FIXTURES_DIR / "project_b" - config_path = util.FIXTURES_DIR / "project_b" / "setup.cfg" + project_path = util.FIXTURES_DIR / "project_b" + config_path = util.FIXTURES_DIR / "project_b" / "setup.cfg" + config_rel_path = "setup.cfg" with config_path.open() as fobj: config_data = fobj.read() @@ -219,7 +236,7 @@ def test_parse_project_cfg(): assert "v201307.0456-beta" in config_data ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, config_path, 'cfg', None) + assert ctx == config.ProjectContext(project_path, config_path, config_rel_path, 'cfg', None) cfg = config.parse(ctx) @@ -241,9 +258,10 @@ def test_parse_toml_file(tmpdir): project_path = tmpdir.mkdir("minimal") setup_cfg = project_path.join("pycalver.toml") setup_cfg.write(PYCALVER_TOML_FIXTURE_1) + setup_cfg_rel_path = "pycalver.toml" ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, setup_cfg, 'toml', None) + assert ctx == config.ProjectContext(project_path, setup_cfg, setup_cfg_rel_path, 'toml', None) cfg = config.parse(ctx) @@ -253,19 +271,21 @@ def test_parse_toml_file(tmpdir): assert cfg.commit is True assert cfg.push is True - assert cfg.file_patterns == { + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath == { "README.md" : ["{pycalver}", "{pep440_pycalver}"], "pycalver.toml": ['current_version = "{pycalver}"'], } def test_parse_default_pattern(): - project_path = util.FIXTURES_DIR / "project_c" - config_path = util.FIXTURES_DIR / "project_c" / "pyproject.toml" + project_path = util.FIXTURES_DIR / "project_c" + config_path = util.FIXTURES_DIR / "project_c" / "pyproject.toml" + config_rel_path = "pyproject.toml" ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, config_path, "toml", None) + assert ctx == config.ProjectContext(project_path, config_path, config_rel_path, "toml", None) cfg = config.parse(ctx) @@ -276,7 +296,8 @@ def test_parse_default_pattern(): assert cfg.tag is True assert cfg.push is True - assert cfg.file_patterns == { + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath == { "pyproject.toml": [r'current_version = "v{year}q{quarter}.{build_no}"'] } @@ -285,9 +306,10 @@ def test_parse_cfg_file(tmpdir): project_path = tmpdir.mkdir("minimal") setup_cfg = project_path.join("setup.cfg") setup_cfg.write(SETUP_CFG_FIXTURE) + setup_cfg_rel_path = "setup.cfg" ctx = config.init_project_ctx(project_path) - assert ctx == config.ProjectContext(project_path, setup_cfg, 'cfg', None) + assert ctx == config.ProjectContext(project_path, setup_cfg, setup_cfg_rel_path, 'cfg', None) cfg = config.parse(ctx) @@ -297,7 +319,8 @@ def test_parse_cfg_file(tmpdir): assert cfg.commit is True assert cfg.push is True - assert cfg.file_patterns == { + raw_patterns_by_filepath = _parse_raw_patterns_by_filepath(cfg) + assert raw_patterns_by_filepath == { "setup.py" : ["{pycalver}", "{pep440_pycalver}"], "setup.cfg": ['current_version = "{pycalver}"'], } diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 92cd854..9d50961 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -4,6 +4,7 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import unicode_literals +import re import copy from test import util @@ -13,35 +14,77 @@ from pycalver import v1rewrite from pycalver import v1version from pycalver import v2rewrite from pycalver import v2version +from pycalver import v1patterns +from pycalver import v2patterns # pylint:disable=protected-access ; allowed for test code +# Fix for Python<3.7 +# https://stackoverflow.com/a/56935186/62997 +copy._deepcopy_dispatch[type(re.compile(''))] = lambda r, _: r + + REWRITE_FIXTURE = """ # SPDX-License-Identifier: MIT __version__ = "v201809.0002-beta" """ -def test_rewrite_lines(): - old_lines = REWRITE_FIXTURE.splitlines() - patterns = ['__version__ = "{pycalver}"'] +def test_v1_rewrite_lines_basic(): + pattern = v1patterns.compile_pattern("{pycalver}", '__version__ = "{pycalver}"') new_vinfo = v1version.parse_version_info("v201911.0003") - new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + + old_lines = REWRITE_FIXTURE.splitlines() + new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "v201911.0003" not in "\n".join(old_lines) assert "v201911.0003" in "\n".join(new_lines) -def test_rewrite_final(): +def test_v1_rewrite_lines(): + version_pattern = "{pycalver}" + new_vinfo = v1version.parse_version_info("v201811.0123-beta", version_pattern) + patterns = [v1patterns.compile_pattern(version_pattern, '__version__ = "{pycalver}"')] + lines = v1rewrite.rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-beta"']) + assert lines == ['__version__ = "v201811.0123-beta"'] + + patterns = [v1patterns.compile_pattern(version_pattern, '__version__ = "{pep440_version}"')] + lines = v1rewrite.rewrite_lines(patterns, new_vinfo, ['__version__ = "201809.2b0"']) + assert lines == ['__version__ = "201811.123b0"'] + + +def test_v2_rewrite_lines(): + version_pattern = "vYYYY0M.BUILD[-RELEASE]" + new_vinfo = v2version.parse_version_info("v201811.0123-beta", version_pattern) + patterns = [v2patterns.compile_pattern(version_pattern, '__version__ = "{version}"')] + lines = v2rewrite.rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" ']) + assert lines == ['__version__ = "v201811.0123-beta" '] + + lines = v2rewrite.rewrite_lines( + patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" # comment'] + ) + assert lines == ['__version__ = "v201811.0123-beta" # comment'] + + patterns = [v2patterns.compile_pattern(version_pattern, '__version__ = "YYYY0M.BLD[PYTAGNUM]"')] + old_lines = ['__version__ = "201809.2a0"'] + lines = v2rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + assert lines == ['__version__ = "201811.123b0"'] + + +def test_v1_rewrite_final(): # Patterns written with {release_tag} placeholder preserve # the release tag even if the new version is -final - old_lines = REWRITE_FIXTURE.splitlines() - patterns = ['__version__ = "v{year}{month}.{build_no}-{release_tag}"'] + pattern = v1patterns.compile_pattern( + "v{year}{month}.{build_no}-{release_tag}", + '__version__ = "v{year}{month}.{build_no}-{release_tag}"', + ) new_vinfo = v1version.parse_version_info("v201911.0003") - new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + + old_lines = REWRITE_FIXTURE.splitlines() + new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "v201911.0003" not in "\n".join(old_lines) @@ -93,14 +136,19 @@ def test_error_bad_path(): assert "setup.py" in str(ex) -def test_error_bad_pattern(): +def test_v1_error_bad_pattern(): with util.Project(project="b") as project: ctx = config.init_project_ctx(project.dir) cfg = config.parse(ctx) assert cfg - patterns = copy.deepcopy(cfg.file_patterns) - patterns["setup.py"] = patterns["setup.py"][0] + "invalid" + patterns = copy.deepcopy(cfg.file_patterns) + original_pattern = patterns["setup.py"][0] + invalid_pattern = v1patterns.compile_pattern( + original_pattern.version_pattern, + original_pattern.raw_pattern + ".invalid", + ) + patterns["setup.py"] = [invalid_pattern] try: old_vinfo = v1version.parse_version_info("v201808.0233") @@ -118,45 +166,163 @@ __version__ = "2018.0002-beta" def test_v1_optional_release(): - old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() - pattern = "{year}.{build_no}{release}" - patterns = ['__version__ = "{year}.{build_no}{release}"'] + version_pattern = "{year}.{build_no}{release}" + new_vinfo = v1version.parse_version_info("2019.0003", version_pattern) - new_vinfo = v1version.parse_version_info("2019.0003", pattern) - new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + raw_pattern = '__version__ = "{year}.{build_no}{release}"' + pattern = v1patterns.compile_pattern(version_pattern, raw_pattern) + + old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() + new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "2019.0003" not in "\n".join(old_lines) - new_text = "\n".join(new_lines) - assert "2019.0003" in new_text + assert "2019.0003" in "\n".join(new_lines) + assert '__version__ = "2019.0003"' in "\n".join(new_lines) - new_vinfo = v1version.parse_version_info("2019.0004-beta", pattern) - new_lines = v1rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + new_vinfo = v1version.parse_version_info("2019.0004-beta", version_pattern) + new_lines = v1rewrite.rewrite_lines([pattern], new_vinfo, old_lines) # make sure optional release tag is added back on assert len(new_lines) == len(old_lines) assert "2019.0004-beta" not in "\n".join(old_lines) assert "2019.0004-beta" in "\n".join(new_lines) + assert '__version__ = "2019.0004-beta"' in "\n".join(new_lines) def test_v2_optional_release(): - old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() - pattern = "YYYY.BUILD[-RELEASE]" - patterns = ['__version__ = "YYYY.BUILD[-RELEASE]"'] + version_pattern = "YYYY.BUILD[-RELEASE]" + new_vinfo = v2version.parse_version_info("2019.0003", version_pattern) - new_vinfo = v2version.parse_version_info("2019.0003", pattern) - new_lines = v2rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + raw_pattern = '__version__ = "YYYY.BUILD[-RELEASE]"' + pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) + + old_lines = OPTIONAL_RELEASE_FIXTURE.splitlines() + new_lines = v2rewrite.rewrite_lines([pattern], new_vinfo, old_lines) assert len(new_lines) == len(old_lines) assert "2019.0003" not in "\n".join(old_lines) - new_text = "\n".join(new_lines) - assert "2019.0003" in new_text - assert '__version__ = "2019.0003"' in new_text + assert "2019.0003" in "\n".join(new_lines) + assert '__version__ = "2019.0003"' in "\n".join(new_lines) - new_vinfo = v2version.parse_version_info("2019.0004-beta", pattern) - new_lines = v2rewrite.rewrite_lines(patterns, new_vinfo, old_lines) + new_vinfo = v2version.parse_version_info("2019.0004-beta", version_pattern) + new_lines = v2rewrite.rewrite_lines([pattern], new_vinfo, old_lines) # make sure optional release tag is added back on assert len(new_lines) == len(old_lines) assert "2019.0004-beta" not in "\n".join(old_lines) assert "2019.0004-beta" in "\n".join(new_lines) + assert '__version__ = "2019.0004-beta"' in "\n".join(new_lines) + + +def test_v1_iter_rewritten(): + version_pattern = "{pycalver}" + new_vinfo = v1version.parse_version_info("v201809.0123") + + file_patterns = { + "src/pycalver/__init__.py": [ + v1patterns.compile_pattern(version_pattern, '__version__ = "{pycalver}"'), + ] + } + rewritten_datas = v1rewrite.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 + + +def test_v2_iter_rewritten(): + version_pattern = "vYYYY0M.BUILD[-RELEASE]" + new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + + file_patterns = { + "src/pycalver/__init__.py": [ + v2patterns.compile_pattern(version_pattern, '__version__ = "vYYYY0M.BUILD[-RELEASE]"'), + ] + } + + rewritten_datas = v2rewrite.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 + + +def test_v1_diff(): + version_pattern = "{pycalver}" + raw_pattern = '__version__ = "{pycalver}"' + pattern = v1patterns.compile_pattern(version_pattern, raw_pattern) + file_patterns = {"src/pycalver/__init__.py": [pattern]} + + old_vinfo = v1version.parse_version_info("v201809.0123") + new_vinfo = v1version.parse_version_info("v201910.1124") + + diff_str = v1rewrite.diff(old_vinfo, new_vinfo, file_patterns) + lines = diff_str.split("\n") + + assert lines[:2] == ["--- src/pycalver/__init__.py", "+++ src/pycalver/__init__.py"] + + assert lines[6].startswith('-__version__ = "v20') + assert lines[7].startswith('+__version__ = "v20') + + assert not lines[6].startswith('-__version__ = "v201809.0123"') + + assert lines[7] == '+__version__ = "v201910.1124"' + + raw_pattern = "Copyright (c) 2018-{year}" + pattern = v1patterns.compile_pattern(version_pattern, raw_pattern) + file_patterns = {'LICENSE': [pattern]} + diff_str = v1rewrite.diff(old_vinfo, new_vinfo, file_patterns) + + lines = diff_str.split("\n") + assert lines[3].startswith("-MIT License Copyright (c) 2018-20") + assert lines[4].startswith("+MIT License Copyright (c) 2018-2019") + + +def test_v2_diff(): + version_pattern = "vYYYY0M.BUILD[-RELEASE]" + raw_pattern = '__version__ = "vYYYY0M.BUILD[-RELEASE]"' + pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) + file_patterns = {"src/pycalver/__init__.py": [pattern]} + + old_vinfo = v2version.parse_version_info("v201809.0123", version_pattern) + new_vinfo = v2version.parse_version_info("v201910.1124", version_pattern) + + diff_str = v2rewrite.diff(old_vinfo, new_vinfo, file_patterns) + lines = diff_str.split("\n") + + assert lines[:2] == ["--- src/pycalver/__init__.py", "+++ src/pycalver/__init__.py"] + + assert lines[6].startswith('-__version__ = "v20') + assert lines[7].startswith('+__version__ = "v20') + + assert not lines[6].startswith('-__version__ = "v201809.0123"') + + assert lines[7] == '+__version__ = "v201910.1124"' + + raw_pattern = "Copyright (c) 2018-YYYY" + pattern = v2patterns.compile_pattern(version_pattern, raw_pattern) + file_patterns = {'LICENSE': [pattern]} + diff_str = v2rewrite.diff(old_vinfo, new_vinfo, file_patterns) + + lines = diff_str.split("\n") + assert lines[3].startswith("-MIT License Copyright (c) 2018-20") + assert lines[4].startswith("+MIT License Copyright (c) 2018-2019")