From 54a681bf3472d04326deffd79517fa606a15da44 Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Thu, 15 Nov 2018 22:16:16 +0100 Subject: [PATCH] Code quality updates --- README.md | 9 +-- docker_base.Dockerfile | 3 +- makefile | 1 + setup.cfg | 2 + src/pycalver/__main__.py | 74 +++++++++++--------- src/pycalver/config.py | 31 ++++++--- src/pycalver/lex_id.py | 51 +++++++++++--- src/pycalver/parse.py | 131 +++++++++++++++++++++++++---------- src/pycalver/rewrite.py | 145 ++++++++++++++++++++++++++++++++------- src/pycalver/vcs.py | 48 +++++++++---- src/pycalver/version.py | 2 +- test/test_lex_id.py | 2 +- test/test_parse.py | 75 ++++++++++---------- test/test_rewrite.py | 26 +++---- 14 files changed, 413 insertions(+), 187 deletions(-) diff --git a/README.md b/README.md index 560b371..286be15 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ regular expression: import re # https://regex101.com/r/fnj60p/10 -pycalver_re = re.compile(r""" +PYCALVER_PATTERN = r""" \b (?P (?P @@ -101,10 +101,11 @@ pycalver_re = re.compile(r""" (?:alpha|beta|dev|rc|post) )? )(?:\s|$) -""", flags=re.VERBOSE) +""" +PYCALVER_RE = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE) version_str = "v201712.0001-alpha" -version_info = pycalver_re.match(version_str).groupdict() +version_info = PYCALVER_RE.match(version_str).groupdict() assert version_info == { "version" : "v201712.0001-alpha", @@ -116,7 +117,7 @@ assert version_info == { } version_str = "v201712.0033" -version_info = pycalver_re.match(version_str).groupdict() +version_info = PYCALVER_RE.match(version_str).groupdict() assert version_info == { "version" : "v201712.0033", diff --git a/docker_base.Dockerfile b/docker_base.Dockerfile index c996f13..bf00033 100644 --- a/docker_base.Dockerfile +++ b/docker_base.Dockerfile @@ -17,7 +17,8 @@ ENV CONDA_DIR /opt/conda ENV PATH $CONDA_DIR/bin:$PATH ENV SHELL /bin/bash -RUN apk add --no-cache bash make sed grep gawk curl git bzip2 unzip +RUN apk add --no-cache bash make sed grep gawk curl bzip2 unzip +RUN apk add --no-cache git mercurial CMD [ "/bin/bash" ] diff --git a/makefile b/makefile index d27664b..a55caac 100644 --- a/makefile +++ b/makefile @@ -320,6 +320,7 @@ check: fmt lint mypy test env: @bash --init-file <(echo '\ source $$HOME/.bashrc; \ + source $(CONDA_ROOT)/etc/profile.d/conda.sh \ export ENV=${ENV-dev}; \ export PYTHONPATH="src/:vendor/:$$PYTHONPATH"; \ conda activate $(DEV_ENV_NAME) \ diff --git a/setup.cfg b/setup.cfg index 0bc6fc3..329b2a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,8 @@ ignore = # D101 # Missing docstring on __init__ D107 + # No blank lines allowed after function docstring + D202 # First line should be in imperative mood D401 select = A,AAA,D,C,E,F,W,H,B,D212,D404,D405,D406,B901,B950 diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 7ccfd42..72abe55 100644 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -30,7 +30,7 @@ _VERBOSE = 0 log = logging.getLogger("pycalver.cli") -def _init_loggers(verbose: int = 0) -> None: +def _init_logging(verbose: int = 0) -> None: if verbose >= 2: log_format = "%(asctime)s.%(msecs)03d %(levelname)-7s %(name)-15s - %(message)s" log_level = logging.DEBUG @@ -42,7 +42,7 @@ def _init_loggers(verbose: int = 0) -> None: log_level = logging.WARNING logging.basicConfig(level=log_level, format=log_format, datefmt="%Y-%m-%dT%H:%M:%S") - log.debug("Loggers initialized.") + log.debug("Logging initialized.") @click.group() @@ -84,7 +84,7 @@ def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config: def show(verbose: int = 0, fetch: bool = True) -> None: """Show current version.""" verbose = max(_VERBOSE, verbose) - _init_loggers(verbose=verbose) + _init_logging(verbose=verbose) cfg: config.MaybeConfig = config.parse() if cfg is None: @@ -106,7 +106,7 @@ def show(verbose: int = 0, fetch: bool = True) -> None: def incr(old_version: str, verbose: int = 0, release: str = None) -> None: """Increment a version number for demo purposes.""" verbose = max(_VERBOSE, verbose) - _init_loggers(verbose) + _init_logging(verbose) if release and release not in parse.VALID_RELESE_VALUES: log.error(f"Invalid argument --release={release}") @@ -114,7 +114,7 @@ def incr(old_version: str, verbose: int = 0, release: str = None) -> None: sys.exit(1) new_version = version.incr(old_version, release=release) - new_version_nfo = parse.parse_version_info(new_version) + new_version_nfo = parse.VersionInfo.parse(new_version) print("PyCalVer Version:", new_version) print("PEP440 Version:" , new_version_nfo.pep440_version) @@ -128,7 +128,7 @@ def incr(old_version: str, verbose: int = 0, release: str = None) -> None: def init(verbose: int = 0, dry: bool = False) -> None: """Initialize [pycalver] configuration.""" verbose = max(_VERBOSE, verbose) - _init_loggers(verbose) + _init_logging(verbose) cfg : config.MaybeConfig = config.parse() if cfg: @@ -174,6 +174,35 @@ def _assert_not_dirty(vcs, filepaths: typ.Set[str], allow_dirty: bool): sys.exit(1) +def _bump(cfg: config.Config, new_version: str, allow_dirty: bool = False) -> None: + _vcs: typ.Optional[vcs.VCS] + + try: + _vcs = vcs.get_vcs() + except OSError: + log.warn("Version Control System not found, aborting commit.") + _vcs = None + + filepaths = set(cfg.file_patterns.keys()) + + if _vcs: + _assert_not_dirty(_vcs, filepaths, allow_dirty) + + rewrite.rewrite(new_version, cfg.file_patterns) + + if _vcs is None or not cfg.commit: + return + + for filepath in filepaths: + _vcs.add(filepath) + + _vcs.commit(f"bump version to {new_version}") + + if cfg.tag: + _vcs.tag(new_version) + _vcs.push(new_version) + + @cli.command() @click.option("-v", "--verbose" , count=True , help="Control log level. -vv for debug level.") @click.option('-f', "--fetch/--no-fetch", is_flag=True, default=True) @@ -202,7 +231,7 @@ def bump( ) -> None: """Increment the current version string and update project files.""" verbose = max(_VERBOSE, verbose) - _init_loggers(verbose) + _init_logging(verbose) if release and release not in parse.VALID_RELESE_VALUES: log.error(f"Invalid argument --release={release}") @@ -223,33 +252,10 @@ def bump( log.info(f"Old Version: {old_version}") log.info(f"New Version: {new_version}") + if dry or verbose: + print(rewrite.diff(new_version, cfg.file_patterns)) + if dry: - log.info("Running with '--dry', showing diffs instead of updating files.") - - file_patterns = cfg.file_patterns - filepaths = set(file_patterns.keys()) - - _vcs: typ.Optional[vcs.VCS] - - try: - _vcs = vcs.get_vcs() - except OSError: - log.warn("Version Control System not found, aborting commit.") - _vcs = None - - # if _vcs: - # _assert_not_dirty(_vcs, filepaths, allow_dirty) - - rewrite.rewrite(new_version, file_patterns, dry=dry, verbose=verbose) - - if dry or _vcs is None or not cfg.commit: return - for filepath in filepaths: - _vcs.add(filepath) - - _vcs.commit(f"bump version to {new_version}") - - if cfg.tag: - _vcs.tag(new_version) - _vcs.push(new_version) + _bump(cfg, new_version, allow_dirty) diff --git a/src/pycalver/config.py b/src/pycalver/config.py index d08a886..94468ef 100644 --- a/src/pycalver/config.py +++ b/src/pycalver/config.py @@ -3,7 +3,7 @@ # # Copyright (c) 2018 Manuel Barkhau (@mbarkhau) - MIT License # SPDX-License-Identifier: MIT -"""Parsing code for setup.cfg or pycalver.cfg""" +"""Parse setup.cfg or pycalver.cfg files.""" import io import os @@ -18,15 +18,18 @@ from .parse import PYCALVER_RE log = logging.getLogger("pycalver.config") +PatternsByFilePath = typ.Dict[str, typ.List[str]] + class Config(typ.NamedTuple): + """Represents a parsed config.""" current_version: str tag : bool commit: bool - file_patterns: typ.Dict[str, typ.List[str]] + file_patterns: PatternsByFilePath def _debug_str(self) -> str: cfg_str_parts = [ @@ -46,6 +49,12 @@ class Config(typ.NamedTuple): @property def pep440_version(self) -> str: + """Derive pep440 compliant version string from PyCalVer version string. + + >>> cfg = Config("v201811.0007-beta", True, True, []) + >>> cfg.pep440_version + '201811.7b0' + """ return str(pkg_resources.parse_version(self.current_version)) @@ -131,26 +140,27 @@ def _parse_buffer(cfg_buffer: io.StringIO, config_filename: str = " MaybeConfig: - if config_filename is None: +def parse(config_filepath: str = None) -> MaybeConfig: + """Parse config file using configparser.""" + if config_filepath is None: if os.path.exists("pycalver.cfg"): - config_filename = "pycalver.cfg" + config_filepath = "pycalver.cfg" elif os.path.exists("setup.cfg"): - config_filename = "setup.cfg" + config_filepath = "setup.cfg" else: log.error("File not found: pycalver.cfg or setup.cfg") return None - if not os.path.exists(config_filename): - log.error(f"File not found: {config_filename}") + if not os.path.exists(config_filepath): + log.error(f"File not found: {config_filepath}") return None cfg_buffer = io.StringIO() - with io.open(config_filename, mode="rt", encoding="utf-8") as fh: + with io.open(config_filepath, mode="rt", encoding="utf-8") as fh: cfg_buffer.write(fh.read()) cfg_buffer.seek(0) - return _parse_buffer(cfg_buffer, config_filename) + return _parse_buffer(cfg_buffer, config_filepath) DEFAULT_CONFIG_BASE_STR = """ @@ -190,6 +200,7 @@ patterns = def default_config_lines() -> typ.List[str]: + """Generate initial default config based on PWD and current date.""" initial_version = dt.datetime.now().strftime("v%Y%m.0001-dev") cfg_str = DEFAULT_CONFIG_BASE_STR.format(initial_version=initial_version) diff --git a/src/pycalver/lex_id.py b/src/pycalver/lex_id.py index cf44704..f63b845 100644 --- a/src/pycalver/lex_id.py +++ b/src/pycalver/lex_id.py @@ -4,9 +4,7 @@ # Copyright (c) 2018 Manuel Barkhau (@mbarkhau) - MIT License # SPDX-License-Identifier: MIT -""" -This is a simple scheme for numerical ids which are ordered both -numerically and lexically. +"""A scheme for lexically ordered numerical ids. Throughout the sequence this expression remains true, whether you are dealing with integers or strings: @@ -84,25 +82,56 @@ MINIMUM_ID = "0" def next_id(prev_id: str) -> str: + """Generate next lexical id. + + Increments by one and adds padding if required. + + >>> next_id("0098") + '0099' + >>> next_id("0099") + '0100' + >>> next_id("0999") + '11000' + >>> next_id("11000") + '11001' + """ + num_digits = len(prev_id) + if prev_id.count("9") == num_digits: raise OverflowError("max lexical version reached: " + prev_id) - _prev_id = int(prev_id, 10) - _next_id = int(_prev_id) + 1 - next_id = f"{_next_id:0{num_digits}}" - if prev_id[0] != next_id[0]: - next_id = str(_next_id * 11) - return next_id + _prev_id_val = int(prev_id, 10) + _next_id_val = int(_prev_id_val) + 1 + _next_id_str = f"{_next_id_val:0{num_digits}}" + if prev_id[0] != _next_id_str[0]: + _next_id_str = str(_next_id_val * 11) + return _next_id_str def ord_val(lex_id: str) -> int: + """Parse the ordinal value of a lexical id. + + The ordinal value is the position in the sequence, + from repeated calls to next_id. + + >>> ord_val("0098") + 98 + >>> ord_val("0099") + 99 + >>> ord_val("0100") + 100 + >>> ord_val("11000") + 1000 + >>> ord_val("11001") + 1001 + """ if len(lex_id) == 1: return int(lex_id, 10) return int(lex_id[1:], 10) -def main() -> None: +def _main() -> None: _curr_id = "01" print(f"{'lexical':<13} {'numerical':>12}") @@ -130,4 +159,4 @@ def main() -> None: if __name__ == '__main__': - main() + _main() diff --git a/src/pycalver/parse.py b/src/pycalver/parse.py index 6daa1a7..b574275 100644 --- a/src/pycalver/parse.py +++ b/src/pycalver/parse.py @@ -3,6 +3,28 @@ # # Copyright (c) 2018 Manuel Barkhau (@mbarkhau) - MIT License # SPDX-License-Identifier: MIT +"""Parse PyCalVer strings. + +>>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict() +>>> assert version_info == { +... "version" : "v201712.0123-alpha", +... "calver" : "v201712", +... "year" : "2017", +... "month" : "12", +... "build" : ".0123", +... "release" : "-alpha", +... } +>>> +>>> version_info = PYCALVER_RE.match("v201712.0033").groupdict() +>>> assert version_info == { +... "version" : "v201712.0033", +... "calver" : "v201712", +... "year" : "2017", +... "month" : "12", +... "build" : ".0033", +... "release" : None, +... } +""" import re import logging @@ -64,7 +86,49 @@ RE_PATTERN_PARTS = { } +class VersionInfo(typ.NamedTuple): + """Container for parsed version string.""" + + version: str + calver : str + year : str + month : str + build : str + release: typ.Optional[str] + + @property + def pep440_version(self) -> str: + """Generate pep440 compliant version string. + + >>> vnfo = VersionInfo.parse("v201712.0033-beta") + >>> vnfo.pep440_version + '201712.33b0' + """ + return str(pkg_resources.parse_version(self.version)) + + @staticmethod + def parse(version: str) -> 'VersionInfo': + """Parse a PyCalVer string. + + >>> vnfo = VersionInfo.parse("v201712.0033-beta") + >>> assert vnfo == VersionInfo( + ... version="v201712.0033-beta", + ... calver="v201712", + ... year="2017", + ... month="12", + ... build=".0033", + ... release="-beta", + ... ) + """ + match = PYCALVER_RE.match(version) + if match is None: + raise ValueError(f"Invalid pycalver: {version}") + + return VersionInfo(**match.groupdict()) + + class PatternMatch(typ.NamedTuple): + """Container to mark a version string in a file.""" lineno : int # zero based line : str @@ -72,45 +136,38 @@ class PatternMatch(typ.NamedTuple): span : typ.Tuple[int, int] match : str + @staticmethod + def _iter_for_pattern(lines: typ.List[str], pattern: str) -> typ.Iterable['PatternMatch']: + # The pattern is escaped, so that everything besides the format + # string variables is treated literally. -class VersionInfo(typ.NamedTuple): + pattern_tmpl = pattern - pep440_version: str - version : str - calver : str - year : str - month : str - build : str - release : typ.Optional[str] + for char, escaped in PATTERN_ESCAPES: + pattern_tmpl = pattern_tmpl.replace(char, escaped) + pattern_str = pattern_tmpl.format(**RE_PATTERN_PARTS) + pattern_re = re.compile(pattern_str) + for lineno, line in enumerate(lines): + match = pattern_re.search(line) + if match: + yield PatternMatch(lineno, line, pattern, match.span(), match.group(0)) -def parse_version_info(version: str) -> VersionInfo: - match = PYCALVER_RE.match(version) - if match is None: - raise ValueError(f"Invalid pycalver: {version}") - pep440_version = str(pkg_resources.parse_version(version)) - return VersionInfo(pep440_version=pep440_version, **match.groupdict()) + @staticmethod + def iter_matches(lines: typ.List[str], patterns: typ.List[str]) -> typ.Iterable['PatternMatch']: + """Iterate over all matches of any pattern on any line. - -def _iter_pattern_matches(lines: typ.List[str], pattern: str) -> typ.Iterable[PatternMatch]: - # The pattern is escaped, so that everything besides the format - # string variables is treated literally. - - pattern_tmpl = pattern - - for char, escaped in PATTERN_ESCAPES: - pattern_tmpl = pattern_tmpl.replace(char, escaped) - - pattern_str = pattern_tmpl.format(**RE_PATTERN_PARTS) - pattern_re = re.compile(pattern_str) - for lineno, line in enumerate(lines): - match = pattern_re.search(line) - if match: - yield PatternMatch(lineno, line, pattern, match.span(), match.group(0)) - - -def parse_patterns(lines: typ.List[str], patterns: typ.List[str]) -> typ.List[PatternMatch]: - all_matches: typ.List[PatternMatch] = [] - for pattern in patterns: - all_matches.extend(_iter_pattern_matches(lines, pattern)) - return all_matches + >>> lines = ["__version__ = 'v201712.0002-alpha'"] + >>> patterns = ["{version}", "{pep440_version}"] + >>> matches = list(PatternMatch.iter_matches(lines, patterns)) + >>> assert matches[0] == PatternMatch( + ... lineno = 0, + ... line = "__version__ = 'v201712.0002-alpha'", + ... pattern= "{version}", + ... span = (15, 33), + ... match = "v201712.0002-alpha", + ... ) + """ + for pattern in patterns: + for match in PatternMatch._iter_for_pattern(lines, pattern): + yield match diff --git a/src/pycalver/rewrite.py b/src/pycalver/rewrite.py index d531784..ef536e3 100644 --- a/src/pycalver/rewrite.py +++ b/src/pycalver/rewrite.py @@ -3,6 +3,7 @@ # # (C) 2018 Manuel Barkhau (@mbarkhau) # SPDX-License-Identifier: MIT +"""Rewrite files, updating occurences of version strings.""" import io import difflib @@ -10,11 +11,23 @@ import logging import typing as typ from . import parse +from . import config log = logging.getLogger("pycalver.rewrite") -def _detect_line_sep(content: str) -> str: +def detect_line_sep(content: str) -> str: + r"""Parse line separator from content. + + >>> detect_line_sep('\r\n') + '\r\n' + >>> detect_line_sep('\r') + '\r' + >>> detect_line_sep('\n') + '\n' + >>> detect_line_sep('') + '\n' + """ if "\r\n" in content: return "\r\n" elif "\r" in content: @@ -24,16 +37,21 @@ def _detect_line_sep(content: str) -> str: def rewrite_lines( - old_lines: typ.List[str], patterns: typ.List[str], new_version: str + patterns: typ.List[str], new_version: str, old_lines: typ.List[str] ) -> typ.List[str]: - new_version_nfo = parse.parse_version_info(new_version) + """Replace occurances of patterns in old_lines with new_version. + + >>> old_lines = ['__version__ = "v201809.0002-beta"'] + >>> patterns = ['__version__ = "{version}"'] + >>> new_lines = rewrite_lines(patterns, "v201811.0123-beta", old_lines) + >>> assert new_lines == ['__version__ = "v201811.0123-beta"'] + """ + new_version_nfo = parse.VersionInfo.parse(new_version) new_version_fmt_kwargs = new_version_nfo._asdict() new_lines = old_lines.copy() - matches: typ.List[parse.PatternMatch] = parse.parse_patterns(old_lines, patterns) - - for m in matches: + for m in parse.PatternMatch.iter_matches(old_lines, patterns): replacement = m.pattern.format(**new_version_fmt_kwargs) span_l, span_r = m.span new_line = m.line[:span_l] + replacement + m.line[span_r:] @@ -42,26 +60,107 @@ def rewrite_lines( return new_lines -def rewrite( - new_version: str, file_patterns: typ.Dict[str, typ.List[str]], dry=False, verbose: int = 0 -) -> None: - for filepath, patterns in file_patterns.items(): - with io.open(filepath, mode="rt", encoding="utf-8") as fh: - content = fh.read() +class RewrittenFileData(typ.NamedTuple): + """Container for line-wise content of rewritten files.""" - line_sep = _detect_line_sep(content) - old_lines = content.split(line_sep) - new_lines = rewrite_lines(old_lines, patterns, new_version) + path : str + line_sep : str + old_lines: typ.List[str] + new_lines: typ.List[str] - if dry or verbose: - diff_lines = difflib.unified_diff( - old_lines, new_lines, lineterm="", fromfile="a/" + filepath, tofile="b/" + filepath + @property + def diff_lines(self) -> typ.List[str]: + r"""Generate unified diff. + + >>> rwd = RewrittenFileData( + ... path = "", + ... line_sep = "\n", + ... old_lines = ["foo"], + ... new_lines = ["bar"], + ... ) + >>> rwd.diff_lines + ['--- ', '+++ ', '@@ -1 +1 @@', '-foo', '+bar'] + """ + return list( + difflib.unified_diff( + a=self.old_lines, + b=self.new_lines, + lineterm="", + fromfile=self.path, + tofile=self.path, ) - print("\n".join(diff_lines)) + ) - if dry: - continue + @staticmethod + def from_content( + patterns: typ.List[str], new_version: str, content: str + ) -> 'RewrittenFileData': + r"""Rewrite pattern occurrences with version string. - new_content = line_sep.join(new_lines) - with io.open(filepath, mode="wt", encoding="utf-8") as fh: + >>> patterns = ['__version__ = "{version}"'] + >>> content = '__version__ = "v201809.0001-alpha"' + >>> rwd = RewrittenFileData.from_content(patterns, "v201809.0123", content) + >>> assert rwd.new_lines == ['__version__ = "v201809.0123"'] + """ + line_sep = detect_line_sep(content) + old_lines = content.split(line_sep) + new_lines = rewrite_lines(patterns, new_version, old_lines) + return RewrittenFileData("", line_sep, old_lines, new_lines) + + @staticmethod + def iter_rewritten( + file_patterns: config.PatternsByFilePath, new_version: str + ) -> typ.Iterable['RewrittenFileData']: + r'''Iterate over files with version string replaced. + + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{version}"']} + >>> rewritten_datas = RewrittenFileData.iter_rewritten(file_patterns, "v201809.0123") + >>> rwd = list(rewritten_datas)[0] + >>> assert rwd.new_lines == [ + ... '# This file is part of the pycalver project', + ... '# https://gitlab.com/mbarkhau/pycalver', + ... '#', + ... '# Copyright (c) 2018 Manuel Barkhau (@mbarkhau) - MIT License', + ... '# SPDX-License-Identifier: MIT', + ... '"""PyCalVer: Automatic CalVer Versioning for Python Packages."""', + ... '', + ... '__version__ = "v201809.0123"', + ... '', + ... ] + >>> + ''' + for filepath, patterns in file_patterns.items(): + with io.open(filepath, mode="rt", encoding="utf-8") as fh: + content = fh.read() + + rfd = RewrittenFileData.from_content(patterns, new_version, content) + yield rfd._replace(path=filepath) + + +def diff(new_version: str, file_patterns: config.PatternsByFilePath) -> str: + r"""Generate diffs of rewritten files. + + >>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{version}"']} + >>> diff_lines = diff("v201809.0123", file_patterns).split("\n") + >>> diff_lines[:2] + ['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py'] + >>> assert diff_lines[6].startswith('-__version__ = "v2') + >>> assert not diff_lines[6].startswith('-__version__ = "v201809.0123"') + >>> diff_lines[7] + '+__version__ = "v201809.0123"' + """ + diff_lines: typ.List[str] = [] + + for rwd in RewrittenFileData.iter_rewritten(file_patterns, new_version): + diff_lines += rwd.diff_lines + + return "\n".join(diff_lines) + + +def rewrite(new_version: str, file_patterns: config.PatternsByFilePath) -> None: + """Rewrite project files, updating each with the new version.""" + + for file_data in RewrittenFileData.iter_rewritten(file_patterns, new_version): + new_content = file_data.line_sep.join(file_data.new_lines) + with io.open(file_data.path, mode="wt", encoding="utf-8") as fh: fh.write(new_content) diff --git a/src/pycalver/vcs.py b/src/pycalver/vcs.py index f1afc37..ffa0054 100644 --- a/src/pycalver/vcs.py +++ b/src/pycalver/vcs.py @@ -7,6 +7,12 @@ # pycalver/vcs.py (this file) is based on code from the # bumpversion project: https://github.com/peritus/bumpversion # Copyright (c) 2013-2014 Filip Noetzel - MIT License +"""Minimal Git and Mercirial API. + +If terminology for similar concepts differs between git and +mercurial, then the git terms are used. For example "fetch" +(git) instead of "pull" (hg) . +""" import os import logging @@ -42,7 +48,7 @@ VCS_SUBCOMMANDS_BY_NAME = { class VCS: - """Version Control System absraction for git and mercurial""" + """VCS absraction for git and mercurial.""" def __init__(self, name: str, subcommands: typ.Dict[str, str] = None): self.name = name @@ -51,13 +57,19 @@ class VCS: else: self.subcommands = subcommands - def __call__(self, cmd_name: str, env=None, **kwargs: str) -> bytes: - cmd_str = self.subcommands[cmd_name] - cmd_parts = cmd_str.format(**kwargs).split() - return sp.check_output(cmd_parts, env=env) + def __call__(self, cmd_name: str, env=None, **kwargs: str) -> str: + """Invoke subcommand and return output.""" + cmd_str = self.subcommands[cmd_name] + cmd_parts = cmd_str.format(**kwargs).split() + output_data = sp.check_output(cmd_parts, env=env) + + # TODO (mb 2018-11-15): Detect encoding of output? + _encoding = "utf-8" + return output_data.decode(_encoding) @property def is_usable(self) -> bool: + """Detect availability of subcommand.""" cmd = self.subcommands['is_usable'].split() try: @@ -70,28 +82,31 @@ class VCS: raise def fetch(self) -> None: + """Fetch updates from remote origin.""" self('fetch') def status(self) -> typ.List[str]: + """Get status lines.""" status_output = self('status') return [ - line.decode("utf-8")[2:].strip() + line[2:].strip() for line in status_output.splitlines() - if not line.strip().startswith(b"??") + if not line.strip().startswith("??") ] def ls_tags(self) -> typ.List[str]: + """List vcs tags on all branches.""" ls_tag_lines = self('ls_tags').splitlines() log.debug(f"ls_tags output {ls_tag_lines}") - return [ - line.decode("utf-8").strip() for line in ls_tag_lines if line.strip().startswith(b"v") - ] + return [line.strip() for line in ls_tag_lines if line.strip().startswith("v")] - def add(self, path) -> None: + def add(self, path: str) -> None: + """Add updates to be included in next commit.""" log.info(f"{self.name} add {path}") self('add_path', path=path) def commit(self, message: str) -> None: + """Commit added files.""" log.info(f"{self.name} commit -m '{message}'") message_data = message.encode("utf-8") @@ -108,17 +123,24 @@ class VCS: self('commit', env=env, path=tmp_file.name) os.unlink(tmp_file.name) - def tag(self, tag_name) -> None: + def tag(self, tag_name: str) -> None: + """Create an annotated tag.""" self('tag', tag=tag_name) - def push(self, tag_name) -> None: + def push(self, tag_name: str) -> None: + """Push changes to origin.""" self('push_tag', tag=tag_name) def __repr__(self) -> str: + """Generate string representation.""" return f"VCS(name='{self.name}')" def get_vcs() -> VCS: + """Detect the appropriate VCS for a repository. + + raises OSError if the directory doesn't use a supported VCS. + """ for vcs_name in VCS_SUBCOMMANDS_BY_NAME.keys(): vcs = VCS(name=vcs_name) if vcs.is_usable: diff --git a/src/pycalver/version.py b/src/pycalver/version.py index 566e420..13e34b8 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -29,7 +29,7 @@ def incr(old_version: str, *, release: str = None) -> str: Old_version is assumed to be a valid calver string, already validated in pycalver.config.parse. """ - old_ver = parse.parse_version_info(old_version) + old_ver = parse.VersionInfo.parse(old_version) new_calver = current_calver() diff --git a/test/test_lex_id.py b/test/test_lex_id.py index be01804..68c2444 100644 --- a/test/test_lex_id.py +++ b/test/test_lex_id.py @@ -35,7 +35,7 @@ def test_ord_val(): def test_main(capsys): - lex_id.main() + lex_id._main() captured = capsys.readouterr() assert len(captured.err) == 0 diff --git a/test/test_parse.py b/test/test_parse.py index 8bfc62a..db00bb6 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -75,7 +75,7 @@ def test_re_pattern_parts(): def test_parse_version_info(): version_str = "v201712.0001-alpha" - version_nfo = parse.parse_version_info(version_str) + version_nfo = parse.VersionInfo.parse(version_str) assert version_nfo.pep440_version == "201712.1a0" assert version_nfo.version == "v201712.0001-alpha" @@ -86,7 +86,7 @@ def test_parse_version_info(): assert version_nfo.release == "-alpha" version_str = "v201712.0001" - version_nfo = parse.parse_version_info(version_str) + version_nfo = parse.VersionInfo.parse(version_str) assert version_nfo.pep440_version == "201712.1" assert version_nfo.version == "v201712.0001" @@ -97,23 +97,25 @@ def test_parse_version_info(): assert version_nfo.release is None -def test_default_parse_patterns(): - lines = [ - "# setup.py", - "import setuptools", - "__version__ = 'v201712.0002-alpha'", - "setuptools.setup(", - "...", - " version='201712.2a0',", - ] +SETUP_PY_FIXTURE = """ +# setup.py +import setuptools +__version__ = 'v201712.0002-alpha' +setuptools.setup( +... + version='201712.2a0', +""" + +def test_default_parse_patterns(): + lines = SETUP_PY_FIXTURE.splitlines() patterns = ["{version}", "{pep440_version}"] - matches = parse.parse_patterns(lines, patterns) + matches = list(parse.PatternMatch.iter_matches(lines, patterns)) assert len(matches) == 2 - assert matches[0].lineno == 2 - assert matches[1].lineno == 5 + assert matches[0].lineno == 3 + assert matches[1].lineno == 6 assert matches[0].pattern == patterns[0] assert matches[1].pattern == patterns[1] @@ -123,22 +125,15 @@ def test_default_parse_patterns(): def test_explicit_parse_patterns(): - lines = [ - "# setup.py", - "import setuptools", - "__version__ = 'v201712.0002-alpha'", - "setuptools.setup(", - "...", - " version='201712.2a0',", - ] + lines = SETUP_PY_FIXTURE.splitlines() patterns = ["__version__ = '{version}'", "version='{pep440_version}'"] - matches = parse.parse_patterns(lines, patterns) + matches = list(parse.PatternMatch.iter_matches(lines, patterns)) assert len(matches) == 2 - assert matches[0].lineno == 2 - assert matches[1].lineno == 5 + assert matches[0].lineno == 3 + assert matches[1].lineno == 6 assert matches[0].pattern == patterns[0] assert matches[1].pattern == patterns[1] @@ -147,23 +142,25 @@ def test_explicit_parse_patterns(): assert matches[1].match == "version='201712.2a0'" +README_RST_FIXTURE = """ +:alt: PyPI version + +.. |version| image:: https://img.shields.io/badge/CalVer-v201809.0002--beta-blue.svg +:target: https://calver.org/ +:alt: CalVer v201809.0002-beta +""" + + def test_badge_parse_patterns(): - lines = [ - ":alt: PyPI version", - "", - ".. |version| image:: https://img.shields.io/badge/CalVer-v201809.0002--beta-blue.svg", - ":target: https://calver.org/", - ":alt: CalVer v201809.0002-beta", - "", - ] + lines = README_RST_FIXTURE.splitlines() patterns = ["badge/CalVer-{calver}{build}-{release}-blue.svg", ":alt: CalVer {version}"] - matches = parse.parse_patterns(lines, patterns) + matches = list(parse.PatternMatch.iter_matches(lines, patterns)) assert len(matches) == 2 - assert matches[0].lineno == 2 - assert matches[1].lineno == 4 + assert matches[0].lineno == 3 + assert matches[1].lineno == 5 assert matches[0].pattern == patterns[0] assert matches[1].pattern == patterns[1] @@ -174,19 +171,19 @@ def test_badge_parse_patterns(): def test_parse_error(): try: - parse.parse_version_info("") + parse.VersionInfo.parse("") assert False except ValueError as err: pass try: - parse.parse_version_info("201809.0002") + parse.VersionInfo.parse("201809.0002") assert False except ValueError as err: pass try: - parse.parse_version_info("v201809.2b0") + parse.VersionInfo.parse("v201809.2b0") assert False except ValueError as err: pass diff --git a/test/test_rewrite.py b/test/test_rewrite.py index 6fc14fc..973f771 100644 --- a/test/test_rewrite.py +++ b/test/test_rewrite.py @@ -1,22 +1,22 @@ from pycalver import rewrite +REWRITE_FIXTURE = """ +# This file is part of the pycalver project +# https://github.com/mbarkhau/pycalver +# +# (C) 2018 Manuel Barkhau (@mbarkhau) +# SPDX-License-Identifier: MIT + +__version__ = "v201809.0002-beta" +""" + + def test_rewrite_lines(): - old_lines = [ - "# This file is part of the pycalver project", - "# https://github.com/mbarkhau/pycalver", - "#", - "# (C) 2018 Manuel Barkhau (@mbarkhau)", - "# SPDX-License-Identifier: MIT", - '', - "import os", - '', - '__version__ = "v201809.0002-beta"', - 'DEBUG = os.environ.get("PYDEBUG", "0") == "1"', - ] + old_lines = REWRITE_FIXTURE.splitlines() patterns = ['__version__ = "{version}"'] new_version = "v201809.0003" - new_lines = rewrite.rewrite_lines(old_lines, patterns, new_version) + new_lines = rewrite.rewrite_lines(patterns, new_version, old_lines) assert len(new_lines) == len(old_lines) assert new_version not in "\n".join(old_lines)