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

View file

@ -10,8 +10,6 @@ import os
import sys
import click
import logging
import difflib
import typing as typ
from . import DEBUG
from . import vcs
@ -56,13 +54,13 @@ def cli():
def show() -> None:
_init_loggers(verbose=False)
cfg = config.parse()
cfg: config.MaybeConfig = config.parse()
if cfg is None:
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']}")
print(f"Current Version: {cfg.current_version}")
print(f"PEP440 Version: {cfg.pep440_version}")
@cli.command()
@ -99,7 +97,7 @@ def init(dry: bool) -> None:
"""Initialize [pycalver] configuration in setup.cfg"""
_init_loggers(verbose=False)
cfg = config.parse()
cfg: config.MaybeConfig = config.parse()
if cfg:
log.error("Configuration already initialized in setup.cfg")
sys.exit(1)
@ -179,16 +177,14 @@ def bump(
log.error(f"Valid arguments are: {', '.join(parse.VALID_RELESE_VALUES)}")
sys.exit(1)
cfg = config.parse()
cfg: config.MaybeConfig = config.parse()
if cfg is None:
log.error("Could not parse configuration from setup.cfg")
sys.exit(1)
old_version = cfg["current_version"]
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}")
@ -196,53 +192,12 @@ def bump(
if dry:
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()
dirty_files = _vcs.dirty_files()
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)
_vcs.assert_not_dirty(filepaths, allow_dirty)
rewrite.rewrite(new_version, file_patterns, dry, verbose)
if dry:
return

View file

@ -18,38 +18,48 @@ from .parse import PYCALVER_RE
log = logging.getLogger("pycalver.config")
def parse(config_file="setup.cfg") -> typ.Optional[typ.Dict[str, typ.Any]]:
if not os.path.exists(config_file):
log.error("File not found: setup.cfg")
return None
class Config(typ.NamedTuple):
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("")
with io.open(config_file, mode="rt", encoding="utf-8") as fh:
cfg_parser.readfp(fh)
cfg_parser.readfp(cfg_buffer)
if "pycalver" not in cfg_parser:
log.error("setup.cfg does not contain a [pycalver] section.")
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'")
return None
current_version = cfg["current_version"]
current_version = base_cfg["current_version"]
if PYCALVER_RE.match(current_version) is None:
log.error(f"setup.cfg 'pycalver.current_version is invalid")
log.error(f"current_version = {current_version}")
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")
cfg["commit"] = cfg.get("commit", "").lower() in ("yes", "true", "1", "on")
tag = base_cfg.get("tag", "").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():
if not section_name.startswith("pycalver:file:"):
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")
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:
cfg["file_patterns"][filepath] = [
if patterns is None:
file_patterns[filepath] = ["{version}", "{pep440_version}"]
else:
file_patterns[filepath] = [
line.strip()
for line in section["patterns"].splitlines()
for line in patterns.splitlines()
if line.strip()
]
else:
cfg["file_patterns"][filepath] = ["{version}", "{pep440_version}"]
if not cfg["file_patterns"]:
cfg["file_patterns"]["setup.cfg"] = ["{version}", "{pep440_version}"]
if not file_patterns:
file_patterns["setup.cfg"] = ["{version}", "{pep440_version}"]
cfg = Config(current_version, pep440_version, tag, commit, file_patterns)
log.debug(f"Config Parsed: {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]:
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:
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}}"

View file

@ -5,17 +5,52 @@
# (C) 2018 Manuel Barkhau (@mbarkhau)
# SPDX-License-Identifier: MIT
import logging
import io
import difflib
import logging
import typing as typ
from . import parse
log = logging.getLogger("pycalver.rewrite")
def rewrite_file(file: str, pattern: str, dry=False) -> None:
difflib.unified_diff(
file_content_before.splitlines(),
file_content_after.splitlines(),
lineterm="",
fromfile="a/" + file,
tofile="b/" + file,
)
def rewrite(
new_version: str,
file_patterns: typ.Dict[str, str],
dry=False,
verbose=False,
) -> None:
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
import os
import sys
import logging
import tempfile
import typing as typ
import subprocess as sp
@ -52,6 +54,25 @@ class BaseVCS:
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):

View file

@ -14,15 +14,25 @@ log = logging.getLogger("pycalver.version")
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,
# validated in pycalver.config.parse.
old_ver = parse.parse_version_info(old_version)
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:])
if release is None:
if old_ver.release:
@ -30,6 +40,8 @@ def bump(old_version: str, release: str=None) -> str:
new_release = old_ver.release[1:]
else:
new_release = None
elif release == "final":
new_release = None
else:
new_release = release
@ -37,38 +49,3 @@ def bump(old_version: str, release: str=None) -> str:
if new_release:
new_version += "-" + new_release
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"
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():
for i in range(1000):
prev_id = str(random.randint(1, 100000))
next_id = lex_id.next_id(prev_id)
assert prev_id < next_id
try:
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():

View file

@ -1,4 +1,6 @@
import random
import datetime as dt
from pycalver import version
@ -9,29 +11,47 @@ def test_current_calver():
assert v[1:].isdigit()
# def test_calver():
# import random
def test_bump_beta():
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:
# _current_calver.delta += dt.timedelta(days=int(random.random() * 5))
def test_bump_final():
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):
# version_str = incr_version(version_str, tag=random.choice([
# None, "alpha", "beta", "rc"
# ]))
# print(f"{version_str:<{padding}}", end=" ")
# if (i + 1) % 8 == 0:
# print()
def test_bump_random():
cur_date = dt.date.today()
cur_version = cur_date.strftime("v%Y%m") + ".0001-dev"
# 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