WIP: refactoring and testing

This commit is contained in:
Manuel Barkhau 2018-09-03 22:23:51 +02:00
parent 8189075385
commit 5108837f45
11 changed files with 203 additions and 151 deletions

View file

@ -52,8 +52,8 @@ which is compatible with python packaging software
The PyCalVer package provides the ``pycalver`` command and The PyCalVer package provides the ``pycalver`` command and
module to generate version strings which follow the format: module to generate version strings which follow the following
``v{calendar_version}.{build_number}[-{release_tag}]`` format: ``v{calendar_version}.{build_number}[-{release_tag}]``
Some examples: Some examples:
@ -69,9 +69,9 @@ Some examples:
v202207.18134 v202207.18134
The ``pycalver bump`` command will parse specified/configured The ``pycalver bump`` command will parse the files you configure
files for such strings and rewrite them with an updated version in ``setup.cfg`` for such strings and rewrite them with an
string. updated version string.
The format accepted by PyCalVer can be parsed with this regular The format accepted by PyCalVer can be parsed with this regular
expression: expression:

View file

@ -8,4 +8,4 @@ import os
__version__ = "v201809.0002-beta" __version__ = "v201809.0002-beta"
DEBUG = os.environ.get("PYCALVER_DEBUG", "0") == "1" DEBUG = os.environ.get("PYCALVER_DEBUG", "0") == "1"

View file

@ -10,8 +10,6 @@ import os
import sys import sys
import click import click
import logging import logging
import difflib
import typing as typ
from . import DEBUG from . import DEBUG
from . import vcs from . import vcs
@ -56,13 +54,13 @@ def cli():
def show() -> None: def show() -> None:
_init_loggers(verbose=False) _init_loggers(verbose=False)
cfg = config.parse() cfg: config.MaybeConfig = config.parse()
if cfg is None: if cfg is None:
log.error("Could not parse configuration from setup.cfg") log.error("Could not parse configuration from setup.cfg")
sys.exit(1) sys.exit(1)
print(f"Current Version: {cfg['current_version']}") print(f"Current Version: {cfg.current_version}")
print(f"PEP440 Version: {cfg['pep440_version']}") print(f"PEP440 Version: {cfg.pep440_version}")
@cli.command() @cli.command()
@ -99,7 +97,7 @@ def init(dry: bool) -> None:
"""Initialize [pycalver] configuration in setup.cfg""" """Initialize [pycalver] configuration in setup.cfg"""
_init_loggers(verbose=False) _init_loggers(verbose=False)
cfg = config.parse() cfg: config.MaybeConfig = config.parse()
if cfg: if cfg:
log.error("Configuration already initialized in setup.cfg") log.error("Configuration already initialized in setup.cfg")
sys.exit(1) sys.exit(1)
@ -179,16 +177,14 @@ def bump(
log.error(f"Valid arguments are: {', '.join(parse.VALID_RELESE_VALUES)}") log.error(f"Valid arguments are: {', '.join(parse.VALID_RELESE_VALUES)}")
sys.exit(1) sys.exit(1)
cfg = config.parse() cfg: config.MaybeConfig = config.parse()
if cfg is None: if cfg is None:
log.error("Could not parse configuration from setup.cfg") log.error("Could not parse configuration from setup.cfg")
sys.exit(1) sys.exit(1)
old_version = cfg["current_version"] old_version = cfg.current_version
new_version = version.bump(old_version, release=release) 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"Old Version: {old_version}")
log.info(f"New Version: {new_version}") log.info(f"New Version: {new_version}")
@ -196,53 +192,12 @@ def bump(
if dry: if dry:
log.info("Running with '--dry', showing diffs instead of updating files.") log.info("Running with '--dry', showing diffs instead of updating files.")
file_patterns = cfg.file_patterns
filepaths = set(file_patterns.keys())
_vcs = vcs.get_vcs() _vcs = vcs.get_vcs()
dirty_files = _vcs.dirty_files() _vcs.assert_not_dirty(filepaths, allow_dirty)
rewrite.rewrite(new_version, file_patterns, dry, verbose)
if dirty_files:
log.warn(f"{_vcs.__name__} working directory is not clean:")
for file in dirty_files:
log.warn(" " + file)
if not allow_dirty and dirty_files:
sys.exit(1)
pattern_files = cfg["file_patterns"].keys()
dirty_pattern_files = set(dirty_files) & set(pattern_files)
if dirty_pattern_files:
log.error("Not commiting when pattern files are dirty:")
for file in dirty_pattern_files:
log.warn(" " + file)
sys.exit(1)
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()
old_lines = content.splitlines()
new_lines = old_lines.copy()
matches = parse.parse_patterns(old_lines, patterns)
for m in matches:
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: if dry:
return return

View file

@ -18,38 +18,48 @@ from .parse import PYCALVER_RE
log = logging.getLogger("pycalver.config") log = logging.getLogger("pycalver.config")
def parse(config_file="setup.cfg") -> typ.Optional[typ.Dict[str, typ.Any]]: class Config(typ.NamedTuple):
if not os.path.exists(config_file):
log.error("File not found: setup.cfg")
return None
current_version : str
pep440_version : str
tag : bool
commit : bool
file_patterns : typ.Dict[str, typ.List[str]]
MaybeConfig = typ.Optional[Config]
def _parse_buffer(cfg_buffer: io.StringIO) -> MaybeConfig:
cfg_parser = configparser.RawConfigParser("") cfg_parser = configparser.RawConfigParser("")
with io.open(config_file, mode="rt", encoding="utf-8") as fh: cfg_parser.readfp(cfg_buffer)
cfg_parser.readfp(fh)
if "pycalver" not in cfg_parser: if "pycalver" not in cfg_parser:
log.error("setup.cfg does not contain a [pycalver] section.") log.error("setup.cfg does not contain a [pycalver] section.")
return None return None
cfg = dict(cfg_parser.items("pycalver")) base_cfg = dict(cfg_parser.items("pycalver"))
if "current_version" not in cfg: if "current_version" not in base_cfg:
log.error("setup.cfg does not have 'pycalver.current_version'") log.error("setup.cfg does not have 'pycalver.current_version'")
return None return None
current_version = cfg["current_version"] current_version = base_cfg["current_version"]
if PYCALVER_RE.match(current_version) is None: if PYCALVER_RE.match(current_version) is None:
log.error(f"setup.cfg 'pycalver.current_version is invalid") log.error(f"setup.cfg 'pycalver.current_version is invalid")
log.error(f"current_version = {current_version}") log.error(f"current_version = {current_version}")
return None return None
cfg["pep440_version"] = str(pkg_resources.parse_version(current_version)) pep440_version = str(pkg_resources.parse_version(current_version))
cfg["tag"] = cfg.get("tag", "").lower() in ("yes", "true", "1", "on") tag = base_cfg.get("tag", "").lower() in ("yes", "true", "1", "on")
cfg["commit"] = cfg.get("commit", "").lower() in ("yes", "true", "1", "on") commit = base_cfg.get("commit", "").lower() in ("yes", "true", "1", "on")
cfg["file_patterns"] = {} file_patterns: typ.Dict[str, typ.List[str]] = {}
section_name: str
for section_name in cfg_parser.sections(): for section_name in cfg_parser.sections():
if not section_name.startswith("pycalver:file:"): if not section_name.startswith("pycalver:file:"):
continue continue
@ -59,25 +69,39 @@ def parse(config_file="setup.cfg") -> typ.Optional[typ.Dict[str, typ.Any]]:
log.error(f"No such file: {filepath} from {section_name} in setup.cfg") log.error(f"No such file: {filepath} from {section_name} in setup.cfg")
return None return None
section = dict(cfg_parser.items(section_name)) section: typ.Dict[str, str] = dict(cfg_parser.items(section_name))
patterns = section.get("patterns")
if "patterns" in section: if patterns is None:
cfg["file_patterns"][filepath] = [ file_patterns[filepath] = ["{version}", "{pep440_version}"]
else:
file_patterns[filepath] = [
line.strip() line.strip()
for line in section["patterns"].splitlines() for line in patterns.splitlines()
if line.strip() if line.strip()
] ]
else:
cfg["file_patterns"][filepath] = ["{version}", "{pep440_version}"]
if not cfg["file_patterns"]: if not file_patterns:
cfg["file_patterns"]["setup.cfg"] = ["{version}", "{pep440_version}"] file_patterns["setup.cfg"] = ["{version}", "{pep440_version}"]
cfg = Config(current_version, pep440_version, tag, commit, file_patterns)
log.debug(f"Config Parsed: {cfg}") log.debug(f"Config Parsed: {cfg}")
return cfg return cfg
def parse(config_file="setup.cfg") -> MaybeConfig:
if not os.path.exists(config_file):
log.error("File not found: setup.cfg")
return None
cfg_buffer = io.StringIO()
with io.open(config_file, mode="rt", encoding="utf-8") as fh:
cfg_buffer.write(fh.read())
cfg_buffer.seek(0)
return _parse_buffer(cfg_buffer)
def default_config_lines() -> typ.List[str]: def default_config_lines() -> typ.List[str]:
initial_version = dt.datetime.now().strftime("v%Y%m.0001-dev") initial_version = dt.datetime.now().strftime("v%Y%m.0001-dev")

View file

@ -85,6 +85,9 @@ MINIMUM_ID = "0"
def next_id(prev_id: str) -> str: def next_id(prev_id: str) -> str:
num_digits = len(prev_id) 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) _prev_id = int(prev_id, 10)
_next_id = int(_prev_id) + 1 _next_id = int(_prev_id) + 1
next_id = f"{_next_id:0{num_digits}}" next_id = f"{_next_id:0{num_digits}}"

View file

@ -5,17 +5,52 @@
# (C) 2018 Manuel Barkhau (@mbarkhau) # (C) 2018 Manuel Barkhau (@mbarkhau)
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import logging import io
import difflib import difflib
import logging
import typing as typ
from . import parse
log = logging.getLogger("pycalver.rewrite") log = logging.getLogger("pycalver.rewrite")
def rewrite_file(file: str, pattern: str, dry=False) -> None: def rewrite(
difflib.unified_diff( new_version: str,
file_content_before.splitlines(), file_patterns: typ.Dict[str, str],
file_content_after.splitlines(), dry=False,
lineterm="", verbose=False,
fromfile="a/" + file, ) -> None:
tofile="b/" + file, new_version_nfo = parse.parse_version_info(new_version)
) new_version_fmt_kwargs = new_version_nfo._asdict()
matches: typ.List[parse.PatternMatch]
for filepath, patterns in file_patterns.items():
with io.open(filepath, mode="rt", encoding="utf-8") as fh:
content = fh.read()
old_lines = content.splitlines()
new_lines = old_lines.copy()
matches = parse.parse_patterns(old_lines, patterns)
for m in matches:
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 dry:
continue
new_content = "\n".join(new_lines)
with io.open(filepath, mode="wt", encoding="utf-8") as fh:
fh.write(new_content)

View file

@ -9,8 +9,10 @@
# MIT License - (C) 2013-2014 Filip Noetzel # MIT License - (C) 2013-2014 Filip Noetzel
import os import os
import sys
import logging import logging
import tempfile import tempfile
import typing as typ
import subprocess as sp import subprocess as sp
@ -52,6 +54,25 @@ class BaseVCS:
if not line.strip().startswith(b"??") if not line.strip().startswith(b"??")
] ]
@classmethod
def assert_not_dirty(cls, filepaths: typ.Set[str], allow_dirty=False) -> None:
dirty_files = cls.dirty_files()
if dirty_files:
log.warn(f"{cls.__name__} working directory is not clean:")
for file in dirty_files:
log.warn(" " + file)
if not allow_dirty and dirty_files:
sys.exit(1)
dirty_pattern_files = set(dirty_files) & filepaths
if dirty_pattern_files:
log.error("Not commiting when pattern files are dirty:")
for file in dirty_pattern_files:
log.warn(" " + file)
sys.exit(1)
class Git(BaseVCS): class Git(BaseVCS):

View file

@ -14,15 +14,25 @@ log = logging.getLogger("pycalver.version")
def current_calver() -> str: def current_calver() -> str:
return dt.datetime.utcnow().strftime("v%Y%m") return dt.date.today().strftime("v%Y%m")
def bump(old_version: str, release: str=None) -> str: def bump(old_version: str, *, release: str=None) -> str:
# old_version is assumed to be a valid calver string, # old_version is assumed to be a valid calver string,
# validated in pycalver.config.parse. # validated in pycalver.config.parse.
old_ver = parse.parse_version_info(old_version) old_ver = parse.parse_version_info(old_version)
new_calver = current_calver() new_calver = current_calver()
if old_ver.calver > new_calver:
log.warning(
f"'version.bump' called with '{old_version}', " +
f"which is from the future, " +
f"maybe your system clock is out of sync."
)
# leave calver as is (don't go back in time)
new_calver = old_ver.calver
new_build = lex_id.next_id(old_ver.build[1:]) new_build = lex_id.next_id(old_ver.build[1:])
if release is None: if release is None:
if old_ver.release: if old_ver.release:
@ -30,6 +40,8 @@ def bump(old_version: str, release: str=None) -> str:
new_release = old_ver.release[1:] new_release = old_ver.release[1:]
else: else:
new_release = None new_release = None
elif release == "final":
new_release = None
else: else:
new_release = release new_release = release
@ -37,38 +49,3 @@ def bump(old_version: str, release: str=None) -> str:
if new_release: if new_release:
new_version += "-" + new_release new_version += "-" + new_release
return new_version return new_version
def incr_version(old_version: str, *, tag: str="__sentinel__") -> str:
maybe_match: MaybeMatch = VERSION_RE.search(old_version)
if maybe_match is None:
raise ValueError(f"Invalid version string: {old_version}")
prev_version_info: PyCalVerInfo = maybe_match.groupdict()
prev_calver: str = prev_version_info["calver"]
next_calver: str = current_calver()
prev_build: str = prev_version_info["build"]
if prev_calver > next_calver:
log.warning(
f"'incr_version' called with '{old_version}', " +
f"which is from the future, " +
f"maybe your system clock is out of sync."
)
next_calver = prev_calver # leave calver as is
next_build = lex_id.next_id(prev_build)
new_version = f"{next_calver}.{next_build}"
if tag != "__sentinel__":
if tag is None:
pass # tag explicitly ignored/removed
else:
new_version += "-" + tag
elif "tag" in prev_version_info:
# preserve previous tag
new_version += "-" + prev_version_info["tag"]
assert old_version < new_version, f"{old_version} {new_version}"
return new_version

View file

@ -0,0 +1,5 @@
from pycalver import config
def test_parse():
pass

View file

@ -7,11 +7,23 @@ def test_next_id_basic():
assert lex_id.next_id("09") == "110" assert lex_id.next_id("09") == "110"
def test_next_id_overflow():
try:
prev_id = "9999"
next_id = lex_id.next_id(prev_id)
assert False, (prev_id, "->", next_id)
except OverflowError:
pass
def test_next_id_random(): def test_next_id_random():
for i in range(1000): for i in range(1000):
prev_id = str(random.randint(1, 100000)) prev_id = str(random.randint(1, 100000))
next_id = lex_id.next_id(prev_id) try:
assert prev_id < next_id next_id = lex_id.next_id(prev_id)
assert prev_id < next_id
except OverflowError:
assert len(prev_id) == prev_id.count("9")
def test_ord_val(): def test_ord_val():

View file

@ -1,4 +1,6 @@
import random
import datetime as dt import datetime as dt
from pycalver import version from pycalver import version
@ -9,29 +11,47 @@ def test_current_calver():
assert v[1:].isdigit() assert v[1:].isdigit()
# def test_calver(): def test_bump_beta():
# import random calver = version.current_calver()
cur_version = calver + ".0001-beta"
assert cur_version < version.bump(cur_version)
assert version.bump(cur_version).endswith("-beta")
assert version.bump(cur_version, release="alpha").endswith("-alpha")
assert version.bump(cur_version, release="final").endswith("0002")
# first_version_str = "v201808.0001-dev"
# padding = len(first_version_str) + 3
# version_str = first_version_str
# def _current_calver() -> str: def test_bump_final():
# _current_calver.delta += dt.timedelta(days=int(random.random() * 5)) calver = version.current_calver()
cur_version = calver + ".0001"
assert cur_version < version.bump(cur_version)
assert version.bump(cur_version, release="alpha").endswith("-alpha")
assert version.bump(cur_version, release="final").endswith("0002")
assert version.bump(cur_version).endswith("0002")
# return (dt.datetime.utcnow() + _current_calver.delta).strftime("v%Y%m")
# _current_calver.delta = dt.timedelta(days=1) def test_bump_future():
future_date = dt.datetime.today() + dt.timedelta(days=300)
future_calver = future_date.strftime("v%Y%m")
cur_version = future_calver + ".0001"
assert cur_version < version.bump(cur_version)
# global current_calver
# current_calver = _current_calver
# for i in range(1050): def test_bump_random():
# version_str = incr_version(version_str, tag=random.choice([ cur_date = dt.date.today()
# None, "alpha", "beta", "rc" cur_version = cur_date.strftime("v%Y%m") + ".0001-dev"
# ]))
# print(f"{version_str:<{padding}}", end=" ")
# if (i + 1) % 8 == 0:
# print()
# print() def _mock_current_calver():
return cur_date.strftime("v%Y%m")
_orig_current_calver = version.current_calver
version.current_calver = _mock_current_calver
try:
for i in range(1000):
cur_date += dt.timedelta(days=int((1 + random.random()) ** 10))
new_version = version.bump(cur_version, release=random.choice([
None, "alpha", "beta", "rc", "final"
]))
assert cur_version < new_version
cur_version = new_version
finally:
version.current_calver = _orig_current_calver