From ce9083ef853b55636b2a30ded4178cd35ba92cec Mon Sep 17 00:00:00 2001 From: Manuel Barkhau Date: Sun, 2 Sep 2018 23:36:57 +0200 Subject: [PATCH] WIP: diffs and stuff --- README.rst | 145 ++++++++++++++++++++++++++++++++------- setup.cfg | 4 +- setup.py | 4 +- src/pycalver/__init__.py | 4 +- src/pycalver/__main__.py | 100 +++++++++++++++++++++++---- src/pycalver/parse.py | 38 ++++++---- src/pycalver/version.py | 15 ++-- 7 files changed, 246 insertions(+), 64 deletions(-) diff --git a/README.rst b/README.rst index 7775999..5236df9 100644 --- a/README.rst +++ b/README.rst @@ -38,9 +38,9 @@ which is compatible with python packaging software :target: https://pypi.python.org/pypi/pycalver :alt: PyPI Version -.. |version| image:: https://img.shields.io/badge/CalVer-v201809.0001--beta-blue.svg +.. |version| image:: https://img.shields.io/badge/CalVer-v201809.0002--beta-blue.svg :target: https://calver.org/ - :alt: CalVer v201809.0001-beta + :alt: CalVer v201809.0002-beta .. |wheel| image:: https://img.shields.io/pypi/wheel/pycalver.svg :target: https://pypi.org/project/pycalver/#files @@ -53,7 +53,7 @@ which is compatible with python packaging software The PyCalVer package provides the ``pycalver`` command and module to generate version strings which follow the format: -``v{calendar_version}.{build_number}[-{tag}]`` +``v{calendar_version}.{build_number}[-{release_tag}]`` Some examples: @@ -81,8 +81,8 @@ expression: import re + # https://regex101.com/r/fnj60p/10 pycalver_re = re.compile(r""" - # https://regex101.com/r/fnj60p/9 \b (?P (?P @@ -90,15 +90,13 @@ expression: (?P\d{4}) (?P\d{2}) ) - (?: + (?P \. # "." build nr prefix - (?P\d{4,}) + \d{4,} ) - (?: + (?P \- # "-" release prefix - (?P - alpha|beta|dev|rc|post - ) + (?:alpha|beta|dev|rc|post) )? )(?:\s|$) """, flags=re.VERBOSE) @@ -111,8 +109,8 @@ expression: "calver" : "v201712", "year" : "2017", "month" : "12", - "build" : "0001", - "release" : "alpha", + "build" : ".0001", + "release" : "-alpha", } version_str = "v201712.0033" @@ -123,23 +121,33 @@ expression: "calver" : "v201712", "year" : "2017", "month" : "12", - "build" : "0033", + "build" : ".0033", "release" : None, } -Usage ------ +Installation +------------ Before we look at project setup, we can simply install and test -by passing a version string to ``pycalver bump``. +by passing a version string to ``pycalver incr``. .. code-block:: bash $ pip install pycalver - $ pycalver bump v201801.0033-beta - v201809.0034-beta + + $ pycalver incr v201801.0033-beta + PyCalVer Version: v201809.0034-beta + PEP440 Version: 201809.34b0 + + $ pycalver incr v201801.0033-beta --release=final + PyCalVer Version: v201809.0034 + PEP440 Version: 201809.34 + + $ pycalver incr v201809.1999 + PyCalVer Version: v201809.22000 + PEP440 Version: 201809.22000 The CalVer component is set to the current year and month, the @@ -147,11 +155,55 @@ build number is incremented by one and the optional release tag is preserved as is, unless specified otherwise via the ``--release=`` parameter. -To setup a project, add the following lines to your ``setup.cfg`` + +Configuration +------------- + +The fastest way to setup a project is to invoke +``pycalver init``. + + +.. code-block:: bash + + $ cd my-project + ~/my-project$ pycalver init + Updated setup.cfg .. code-block:: ini + # setup.cfg + [bdist_wheel] + universal = 1 + + [pycalver] + current_version = v201809.0001-dev + commit = True + tag = True + + [pycalver:file:setup.cfg] + patterns = + current_version = {version} + + [pycalver:file:setup.py] + patterns = + "{version}", + "{pep440_version}", + + [pycalver:file:README.rst] + patterns = + {version} + {pep440_version} + + +Depending on your project, the above will probably cover all +version numbers across your repository. Something like the +following may illustrate additional changes you'll need to make. + + +.. code-block:: ini + + # setup.cfg [pycalver] current_version = v201809.0001-beta commit = True @@ -163,7 +215,7 @@ To setup a project, add the following lines to your ``setup.cfg`` [pycalver:file:setup.py] patterns = - version="{pep440_version}", + version="{pep440_version}" [pycalver:file:src/myproject.py] patterns = @@ -175,7 +227,20 @@ To setup a project, add the following lines to your ``setup.cfg`` :alt: CalVer {version} -The above setup.cfg file is very explicit, and can be shortened quite a bit. +If ``patterns`` is not specified for a ``pycalver:file:`` +section, the default patterns are used: + + +.. code-block:: ini + + [pycalver:file:src/myproject.py] + patterns = + {version} + {pep440_version} + + +This allows us to less explicit but shorter configuration, like +this: .. code-block:: ini @@ -194,9 +259,38 @@ The above setup.cfg file is very explicit, and can be shortened quite a bit. :alt: CalVer {version} -This makes use of the default ``patterns = {version}``, which -will replace all occurrences of a PyCalVer version string with -the updated ``current_version``. +Pattern Search and Replacement +------------------------------ + +``patterns`` is used both to search for version strings and to +generate the replacement strings. The following placeholders are +available for use, everything else in a pattern is treated as +literal text. + +.. table:: Patterns Placeholders + + ================== ====================== + placeholder example + ================== ====================== + ``pep440_version`` 201809.1b0 + ``version`` v201809.0001-alpha + ``calver`` v201809 + ``year`` 2018 + ``month`` 09 + ``build`` .0001 + ``release`` -alpha + ================== ====================== + +Note that the separator/prefix characters are part of what is +matched and generated for a given placeholder, and they should +not be included in your patterns. + +A further restriction is, that a version string cannot span +multiple lines in your source file. + + +Pattern Search and Replacement +------------------------------ Now we can call ``pycalver bump`` to bump all occurrences of version strings in these files. Normally this will change local @@ -543,4 +637,5 @@ express such information assumes 1. that the author of a package is aware of how a given change needs to be reflected in a version number and 2. that users and packaging softare correctly parse that meaning. When I used semantic versioning, I realized that -the major version number of my packages would never change, because I don't think breaking changes should ever be One of the biggest offenses expres +the major version number of my packages would never change, +because I don't think breaking changes should ever be \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 8d593ad..6dac5bf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ strict_optional = True universal = 1 [pycalver] -current_version = v201808.0001-beta +current_version = v201809.0002-beta commit = True tag = True @@ -23,7 +23,7 @@ patterns = [pycalver:file:setup.py] patterns = - __version__ = "{version}" + version="{pep440_version}" [pycalver:file:src/pycalver/__init__.py] patterns = diff --git a/setup.py b/setup.py index e74cdad..4afe356 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setuptools.setup( author="Manuel Barkhau", author_email="mbarkhau@gmail.com", url="https://github.com/mbarkhau/pycalver", - version="201809.1a0", + version="201809.2b0", description="CalVer versioning for python projects", long_description=long_description, @@ -77,4 +77,4 @@ setuptools.setup( "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", ], -) +) \ No newline at end of file diff --git a/src/pycalver/__init__.py b/src/pycalver/__init__.py index f042c95..ba36471 100644 --- a/src/pycalver/__init__.py +++ b/src/pycalver/__init__.py @@ -6,6 +6,6 @@ import os -__version__ = "v201809.0001-beta" +__version__ = "v201809.0002-beta" -DEBUG = os.environ.get("PYCALVER_DEBUG", "0") == "1" +DEBUG = os.environ.get("PYCALVER_DEBUG", "0") == "1" \ No newline at end of file diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 9aa0f78..a196558 100644 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -10,6 +10,7 @@ import os import sys import click import logging +import difflib import typing as typ from . import DEBUG @@ -57,12 +58,36 @@ def show() -> None: cfg = config.parse() if cfg is None: - return + log.error("Could not parse configuration from setup.cfg") + sys.exit(1) print(f"Current Version: {cfg['current_version']}") print(f"PEP440 Version: {cfg['pep440_version']}") +@cli.command() +@click.argument("old_version") +@click.option( + "--release", + default=None, + metavar="", + help="Override release name of current_version", +) +def incr(old_version: str, release: str = None) -> None: + _init_loggers(verbose=False) + + if release and release not in parse.VALID_RELESE_VALUES: + log.error(f"Invalid argument --release={release}") + log.error(f"Valid arguments are: {', '.join(parse.VALID_RELESE_VALUES)}") + sys.exit(1) + + new_version = version.bump(old_version, release=release) + new_version_nfo = parse.parse_version_info(new_version) + + print("PyCalVer Version:", new_version) + print("PEP440 Version:", new_version_nfo["pep440_version"]) + + @cli.command() @click.option( "--dry", @@ -77,7 +102,7 @@ def init(dry: bool) -> None: cfg = config.parse() if cfg: log.error("Configuration already initialized in setup.cfg") - return + sys.exit(1) cfg_lines = config.default_config_lines() @@ -99,6 +124,12 @@ def init(dry: bool) -> None: @cli.command() +@click.option( + "--release", + default=None, + metavar="", + help="Override release name of current_version", +) @click.option( "--verbose", default=False, @@ -112,35 +143,78 @@ def init(dry: bool) -> None: help="Display diff of changes, don't rewrite files.", ) @click.option( - "--release", - default=None, - metavar="", - help="Override release name of current_version", + "--commit", + default=True, + is_flag=True, + help="Tag the commit.", ) -def bump(verbose: bool, dry: bool, release: typ.Optional[str] = None) -> None: +@click.option( + "--tag", + default=True, + is_flag=True, + help="Tag the commit.", +) +def bump(release: str, verbose: bool, dry: bool, commit: bool, tag: bool) -> None: _init_loggers(verbose) + if release and release not in parse.VALID_RELESE_VALUES: log.error(f"Invalid argument --release={release}") log.error(f"Valid arguments are: {', '.join(parse.VALID_RELESE_VALUES)}") - return + sys.exit(1) cfg = config.parse() if cfg is None: - log.error("Unable to parse pycalver configuration from setup.cfg") - return + log.error("Could not parse configuration from setup.cfg") + sys.exit(1) old_version = cfg["current_version"] new_version = version.bump(old_version, release=release) + new_version_nfo = parse.parse_version_info(new_version) + new_version_fmt_kwargs = new_version_nfo._asdict() log.info(f"Old Version: {old_version}") log.info(f"New Version: {new_version}") + if dry: + log.info("Running with '--dry', showing diffs instead of updating files.") + matches: typ.List[parse.PatternMatch] for filepath, patterns in cfg["file_patterns"].items(): with io.open(filepath, mode="rt", encoding="utf-8") as fh: content = fh.read() - lines = content.splitlines() - matches = parse.parse_patterns(lines, patterns) + + old_lines = content.splitlines() + new_lines = old_lines.copy() + + matches = parse.parse_patterns(old_lines, patterns) for m in matches: - print(m) + 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:] + new_lines[m.lineno] = new_line + + if dry or verbose: + print("\n".join(difflib.unified_diff( + old_lines, + new_lines, + lineterm="", + fromfile="a/" + filepath, + tofile="b/" + filepath, + ))) + + # if not dry: + # new_content = "\n".join(new_lines) + # with io.open(filepath, mode="wt", encoding="utf-8") as fh: + # fh.write(new_content) + + if dry: + return + + if not commit: + return + + v = vcs.get_vcs() + if v is None: + log.warn("Version Control System not found, aborting commit.") + return diff --git a/src/pycalver/parse.py b/src/pycalver/parse.py index 8568bf2..b8970c8 100644 --- a/src/pycalver/parse.py +++ b/src/pycalver/parse.py @@ -11,8 +11,7 @@ import sys import logging import typing as typ import datetime as dt - -from pkg_resources import parse_version +import pkg_resources from . import lex_id @@ -22,6 +21,7 @@ log = logging.getLogger("pycalver.parse") VALID_RELESE_VALUES = ("alpha", "beta", "dev", "rc", "post") +# https://regex101.com/r/fnj60p/10 PYCALVER_RE: typ.re.Pattern[str] = re.compile(r""" \b (?P @@ -30,22 +30,20 @@ PYCALVER_RE: typ.re.Pattern[str] = re.compile(r""" (?P\d{4}) (?P\d{2}) ) - (?: + (?P \. # "." build nr prefix - (?P\d{4,}) + \d{4,} ) - (?: + (?P \- # "-" release prefix - (?P - alpha|beta|dev|rc|post - ) + (?:alpha|beta|dev|rc|post) )? )(?:\s|$) """, flags=re.VERBOSE) RE_PATTERN_PARTS = { - "pep440_version" : r"\d{6}\.\d+(a|b|dev|rc|post)?\d*", + "pep440_version" : r"\b\d{6}\.[1-9]\d*(a|b|dev|rc|post)?\d*(?:\s|$)", "version" : r"v\d{6}\.\d{4,}\-(?:alpha|beta|dev|rc|post)", "calver" : r"v\d{6}", "build" : r"\.\d{4,}", @@ -55,15 +53,28 @@ RE_PATTERN_PARTS = { class PatternMatch(typ.NamedTuple): - lineno : int + lineno : int # zero based line : str pattern : str span : typ.Tuple[int, int] match : str -MaybeMatch = typ.Optional[typ.re.Match[str]] -PyCalVerInfo = typ.Dict[str, str] +class VersionInfo(typ.NamedTuple): + + pep440_version : str + version : str + calver : str + year : str + month : str + build : str + release : typ.Optional[str] + + +def parse_version_info(version: str) -> VersionInfo: + match = PYCALVER_RE.match(version) + pep440_version = pkg_resources.parse_version(version) + return VersionInfo(pep440_version=pep440_version, **match.groupdict()) def iter_pattern_matches(lines: typ.List[str], pattern: str) -> typ.Iterable[PatternMatch]: @@ -80,10 +91,9 @@ def iter_pattern_matches(lines: typ.List[str], pattern: str) -> typ.Iterable[Pat .replace("(", "\\(") .format(**RE_PATTERN_PARTS) ) - for i, line in enumerate(lines): + for lineno, line in enumerate(lines): match = pattern_re.search(line) if match: - lineno = i + 1 yield PatternMatch(lineno, line, pattern, match.span(), match.group(0)) diff --git a/src/pycalver/version.py b/src/pycalver/version.py index 53e53b7..80a517c 100644 --- a/src/pycalver/version.py +++ b/src/pycalver/version.py @@ -8,6 +8,8 @@ import logging import datetime as dt from . import lex_id +from . import parse + log = logging.getLogger("pycalver.version") @@ -18,15 +20,16 @@ def current_calver() -> str: def bump(old_version: str, release: str=None) -> str: # old_version is assumed to be a valid calver string, # validated in pycalver.config.parse. - - old_calver, rest = old_version.split(".") - old_build, old_release = rest.split("-") + old_ver = parse.parse_version_info(old_version) new_calver = current_calver() - new_build = lex_id.next_id(old_build) + new_build = lex_id.next_id(old_ver.build[1:]) if release is None: - # preserve existing release - new_release = old_release + if old_ver.release: + # preserve existing release + new_release = old_ver.release[1:] + else: + new_release = None else: new_release = release