much bugfixing

This commit is contained in:
Manuel Barkhau 2020-10-02 20:52:54 +00:00
parent 56c9f9b36c
commit 49e19fbf89
18 changed files with 687 additions and 451 deletions

View file

@ -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'.")

View file

@ -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}"

View file

@ -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)

View file

@ -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)

View file

@ -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]

View file

@ -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"

View file

@ -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)

View file

@ -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)

View file

@ -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]

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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