formatting with segment tree

This commit is contained in:
Manuel Barkhau 2020-09-24 11:16:02 +00:00
parent 8af5047244
commit 5a64983b8e
14 changed files with 325 additions and 154 deletions

View file

@ -24,6 +24,7 @@ from . import rewrite
from . import version
from . import v1version
from . import v2version
from . import v1patterns
_VERBOSE = 0
@ -104,13 +105,14 @@ def test(
) -> None:
"""Increment a version number for demo purposes."""
_configure_logging(verbose=max(_VERBOSE, verbose))
raw_pattern = pattern
if release:
_validate_release_tag(release)
new_version = _incr(
old_version,
raw_pattern=pattern,
raw_pattern=raw_pattern,
release=release,
major=major,
minor=minor,
@ -118,7 +120,7 @@ def test(
pin_date=pin_date,
)
if new_version is None:
logger.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.")
logger.error(f"Invalid version '{old_version}' and/or pattern '{raw_pattern}'.")
sys.exit(1)
pep440_version = version.to_pep440(new_version)
@ -185,7 +187,7 @@ def _print_diff(cfg: config.Config, new_version: str) -> None:
def _incr(
old_version: str,
raw_pattern: str = "{pycalver}",
raw_pattern: str,
*,
release : str = None,
major : bool = False,
@ -193,9 +195,10 @@ def _incr(
patch : bool = False,
pin_date: bool = False,
) -> typ.Optional[str]:
is_new_pattern = "{" in raw_pattern and "}" in raw_pattern
if is_new_pattern:
return v2version.incr(
v1_parts = list(v1patterns.PART_PATTERNS) + list(v1patterns.FULL_PART_FORMATS)
has_v1_part = any("{" + part + "}" in raw_pattern for part in v1_parts)
if has_v1_part:
return v1version.incr(
old_version,
raw_pattern=raw_pattern,
release=release,
@ -205,7 +208,7 @@ def _incr(
pin_date=pin_date,
)
else:
return v1version.incr(
return v2version.incr(
old_version,
raw_pattern=raw_pattern,
release=release,

View file

@ -1,10 +1,11 @@
# This file is part of the pycalver project
# https://gitlab.com/mbarkhau/pycalver
#
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Parse setup.cfg or pycalver.cfg files."""
import re
import glob
import typing as typ
import logging
@ -125,7 +126,7 @@ def _debug_str(cfg: Config) -> str:
"\n file_patterns={",
]
for filepath, patterns in cfg.file_patterns.items():
for filepath, patterns in sorted(cfg.file_patterns.items()):
for pattern in patterns:
cfg_str_parts.append(f"\n '{filepath}': '{pattern.raw_pattern}',")
@ -261,6 +262,14 @@ def _parse_config(raw_cfg: RawConfig) -> Config:
is_new_pattern = "{" not in version_pattern and "}" not in version_pattern
if is_new_pattern:
invalid_chars = re.search(r"([\s]+)", raw_cfg['version_pattern'])
if invalid_chars:
raise ValueError(
f"Invalid character(s) '{invalid_chars.group(1)}'"
f" in pycalver.version_pattern = {raw_cfg['version_pattern']}"
)
# TODO (mb 2020-09-18): Validate Pattern
# detect YY with WW or UU -> suggest GG with VV
# detect YYMM -> suggest YY0M
@ -372,17 +381,17 @@ def _parse_raw_config(ctx: ProjectContext) -> RawConfig:
def parse(ctx: ProjectContext) -> MaybeConfig:
"""Parse config file if available."""
if not ctx.config_filepath.exists():
if ctx.config_filepath.exists():
try:
raw_cfg = _parse_raw_config(ctx)
return _parse_config(raw_cfg)
except (TypeError, ValueError) as ex:
logger.warning(f"Couldn't parse {ctx.config_rel_path}: {str(ex)}")
return None
else:
logger.warning(f"File not found: {ctx.config_rel_path}")
return None
try:
raw_cfg = _parse_raw_config(ctx)
return _parse_config(raw_cfg)
except (TypeError, ValueError) as ex:
logger.warning(f"Couldn't parse {ctx.config_rel_path}: {str(ex)}")
return None
DEFAULT_CONFIGPARSER_BASE_TMPL = """
[pycalver]

View file

@ -1,3 +1,8 @@
# 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
import typing as typ

View file

@ -1,3 +1,8 @@
# 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
import typing as typ
import difflib

View file

@ -27,7 +27,8 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C
return cfg
version_tags.sort(reverse=True)
logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
_debug_tags = ", ".join(version_tags[:3])
logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)")
latest_version_tag = version_tags[0]
latest_version_pep440 = version.to_pep440(latest_version_tag)
if latest_version_tag <= cfg.current_version:

View file

@ -34,6 +34,7 @@ import re
import typing as typ
import logging
from . import utils
from .patterns import RE_PATTERN_ESCAPES
from .patterns import Pattern
@ -80,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"(?:a|b|dev|rc|post)?\d*",
'tag' : r"(?:alpha|beta|dev|rc|post|final)",
'pep440_tag' : r"(?:post|dev|rc|a|b)?\d*",
'tag' : r"(?:preview|final|alpha|beta|post|pre|dev|rc|a|b|c|r)",
'yy' : r"\d{2}",
'yyyy' : r"\d{4}",
'quarter' : r"[1-4]",
@ -183,6 +184,7 @@ def _replace_pattern_parts(pattern: str) -> str:
named_part_pattern = f"(?P<{part_name}>{part_pattern})"
placeholder = "\u005c{" + part_name + "\u005c}"
pattern = pattern.replace(placeholder, named_part_pattern)
return pattern
@ -214,6 +216,7 @@ def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[s
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)

View file

@ -37,11 +37,11 @@ def rewrite_lines(
>>> rewrite_lines(patterns, new_vinfo, ['__version__ = "201809.2b0"'])
['__version__ = "201811.123b0"']
"""
new_lines = old_lines[:]
found_patterns = set()
found_patterns: typ.Set[Pattern] = set()
new_lines = old_lines[:]
for match in parse.iter_matches(old_lines, patterns):
found_patterns.add(match.pattern.raw_pattern)
found_patterns.add(match.pattern)
replacement = v1version.format_version(new_vinfo, match.pattern.raw_pattern)
span_l, span_r = match.span
new_line = match.line[:span_l] + replacement + match.line[span_r:]

View file

@ -9,6 +9,8 @@ import typing as typ
import logging
import datetime as dt
import lexid
from . import version
from . import v1patterns
@ -18,15 +20,19 @@ logger = logging.getLogger("pycalver.v1version")
CalInfo = typ.Union[version.V1CalendarInfo, version.V1VersionInfo]
def _is_later_than(old: CalInfo, new: CalInfo) -> bool:
"""Is old > new based on non None fields."""
def _is_cal_gt(left: CalInfo, right: CalInfo) -> bool:
"""Is left > right for non-None fields."""
lvals = []
rvals = []
for field in version.V1CalendarInfo._fields:
aval = getattr(old, field)
bval = getattr(new, field)
if not (aval is None or bval is None):
if aval > bval:
return True
return False
lval = getattr(left , field)
rval = getattr(right, field)
if not (lval is None or rval is None):
lvals.append(lval)
rvals.append(rval)
return lvals > rvals
def _ver_to_cal_info(vnfo: version.V1VersionInfo) -> version.V1CalendarInfo:
@ -235,7 +241,7 @@ def parse_version_info(version_str: str, raw_pattern: str = "{pycalver}") -> ver
if match is None:
err_msg = (
f"Invalid version string '{version_str}' "
f"for pattern '{raw_pattern}'/'{pattern.regexp}'"
f"for pattern '{raw_pattern}'/'{pattern.regexp.pattern}'"
)
raise version.PatternError(err_msg)
else:
@ -386,19 +392,23 @@ def incr(
cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info()
if _is_later_than(old_vinfo, cur_cinfo):
cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict())
else:
logger.warning(f"Version appears to be from the future '{old_version}'")
if _is_cal_gt(old_vinfo, cur_cinfo):
logger.warning(f"Old version appears to be from the future '{old_version}'")
cur_vinfo = old_vinfo
else:
cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict())
cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid))
if release:
cur_vinfo = cur_vinfo._replace(tag=release)
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)
cur_vinfo = version.incr_non_cal_parts(
cur_vinfo,
release,
major,
minor,
patch,
)
new_version = format_version(cur_vinfo, raw_pattern)
if new_version == old_version:
logger.error("Invalid arguments or pattern, version did not change.")

View file

@ -27,7 +27,8 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C
return cfg
version_tags.sort(reverse=True)
logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
_debug_tags = ", ".join(version_tags[:3])
logger.debug(f"found tags: {_debug_tags} ... ({len(version_tags)} in total)")
latest_version_tag = version_tags[0]
latest_version_pep440 = version.to_pep440(latest_version_tag)
if latest_version_tag <= cfg.current_version:

View file

@ -35,6 +35,7 @@ import re
import typing as typ
import logging
from . import utils
from .patterns import RE_PATTERN_ESCAPES
from .patterns import Pattern
@ -84,8 +85,8 @@ PART_PATTERNS = {
'PATCH': r"[0-9]+",
'BUILD': r"[0-9]+",
'BLD' : r"[1-9][0-9]*",
'TAG' : r"(?:alpha|beta|dev|pre|rc|post|final)",
'PYTAG': r"(?:a|b|dev|rc|post)",
'TAG' : r"(?:preview|final|alpha|beta|post|pre|dev|rc|a|b|c|r)",
'PYTAG': r"(?:post|dev|rc|a|b)",
'NUM' : r"[0-9]+",
}
@ -203,24 +204,56 @@ PART_FORMATS: typ.Dict[str, typ.Callable[[FieldValue], str]] = {
}
def _convert_to_pep440(version_pattern: str) -> str:
# NOTE (mb 2020-09-20): This does not support some
# corner cases as specified in PEP440, in particular
# related to post and dev releases.
version_pattern = version_pattern.lstrip("v")
part_names = list(PATTERN_PART_FIELDS.keys())
part_names.sort(key=len, reverse=True)
if version_pattern == "vYYYY0M.BUILD[-TAG]":
return "YYYY0M.BLD[PYTAGNUM]"
# TODO (mb 2020-09-20)
raise NotImplementedError
def normalize_pattern(version_pattern: str, raw_pattern: str) -> str:
normalized_pattern = raw_pattern
if "{version}" in raw_pattern:
normalized_pattern = normalized_pattern.replace("{version}", version_pattern)
if "{pep440_version}" in normalized_pattern:
pep440_version_pattern = _convert_to_pep440(version_pattern)
normalized_pattern = normalized_pattern.replace("{pep440_version}", pep440_version_pattern)
return normalized_pattern
def _replace_pattern_parts(pattern: str) -> str:
# The pattern is escaped, so that everything besides the format
# string variables is treated literally.
if "[" in pattern and "]" in pattern:
pattern = pattern.replace("[", "(?:")
pattern = pattern.replace("]", ")?")
while True:
new_pattern, n = re.subn(r"([^\\]|^)\[", r"\1(?:", pattern)
new_pattern, m = re.subn(r"([^\\]|^)\]", r"\1)?" , new_pattern)
pattern = new_pattern
if n + m == 0:
break
SortKey = typ.Tuple[int, int]
PostitionedPart = typ.Tuple[int, int, str]
part_patterns_by_index: typ.Dict[SortKey, PostitionedPart] = {}
part_patterns_by_index: typ.Dict[typ.Tuple[int, int], typ.Tuple[int, int, str]] = {}
for part_name, part_pattern in PART_PATTERNS.items():
start_idx = pattern.find(part_name)
if start_idx < 0:
continue
field = PATTERN_PART_FIELDS[part_name]
named_part_pattern = f"(?P<{field}>{part_pattern})"
end_idx = start_idx + len(part_name)
sort_key = (-end_idx, -len(part_name))
part_patterns_by_index[sort_key] = (start_idx, end_idx, named_part_pattern)
if start_idx >= 0:
field = PATTERN_PART_FIELDS[part_name]
named_part_pattern = f"(?P<{field}>{part_pattern})"
end_idx = start_idx + len(part_name)
sort_key = (-end_idx, -len(part_name))
part_patterns_by_index[sort_key] = (start_idx, end_idx, named_part_pattern)
# NOTE (mb 2020-09-17): The sorting is done so that we process items:
# - right before left
@ -238,26 +271,21 @@ def _replace_pattern_parts(pattern: str) -> str:
def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]:
escaped_pattern = raw_pattern
normalized_pattern = normalize_pattern(version_pattern, raw_pattern)
escaped_pattern = normalized_pattern
for char, escaped in RE_PATTERN_ESCAPES:
# [] braces are used for optional parts, such as [-TAG]/[-beta]
is_semantic_char = char in "[]"
# and need to be escaped manually.
is_semantic_char = char in "[]\\"
if not is_semantic_char:
# escape it so it is a literal in the re pattern
escaped_pattern = escaped_pattern.replace(char, escaped)
escaped_pattern = raw_pattern.replace("[", "\u005c[").replace("]", "\u005c]")
normalized_pattern = escaped_pattern.replace("{version}", version_pattern)
print(">>>>", (raw_pattern ,))
print("....", (escaped_pattern ,))
print("....", (normalized_pattern,))
print("<<<<", (normalized_pattern,))
# 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)

View file

@ -14,6 +14,7 @@ from . import config
from . import rewrite
from . import version
from . import v2version
from . import v2patterns
from .patterns import Pattern
logger = logging.getLogger("pycalver.v2rewrite")
@ -41,12 +42,15 @@ def rewrite_lines(
>>> rewrite_lines(patterns, new_vinfo, old_lines)
['__version__ = "201811.123b0"']
"""
new_lines = old_lines[:]
found_patterns = set()
found_patterns: typ.Set[Pattern] = set()
new_lines = old_lines[:]
for match in parse.iter_matches(old_lines, patterns):
found_patterns.add(match.pattern.raw_pattern)
replacement = v2version.format_version(new_vinfo, match.pattern.raw_pattern)
found_patterns.add(match.pattern)
normalized_pattern = v2patterns.normalize_pattern(
match.pattern.version_pattern, match.pattern.raw_pattern
)
replacement = v2version.format_version(new_vinfo, normalized_pattern)
span_l, span_r = match.span
new_line = match.line[:span_l] + replacement + match.line[span_r:]
new_lines[match.lineno] = new_line
@ -93,6 +97,18 @@ def rfd_from_content(
return rewrite.RewrittenFileData(path, line_sep, old_lines, new_lines)
def _patterns_with_change(
old_vinfo: version.V2VersionInfo, new_vinfo: version.V2VersionInfo, patterns: typ.List[Pattern]
) -> int:
patterns_with_change = 0
for pattern in patterns:
old_str = v2version.format_version(old_vinfo, pattern.raw_pattern)
new_str = v2version.format_version(new_vinfo, pattern.raw_pattern)
if old_str != new_str:
patterns_with_change += 1
return patterns_with_change
def iter_rewritten(
file_patterns: config.PatternsByFile,
new_vinfo : version.V2VersionInfo,
@ -159,13 +175,6 @@ def diff(
with file_path.open(mode="rt", encoding="utf-8") as fobj:
content = fobj.read()
patterns_with_change = 0
for pattern in patterns:
old_str = v2version.format_version(old_vinfo, pattern.raw_pattern)
new_str = v2version.format_version(new_vinfo, pattern.raw_pattern)
if old_str != new_str:
patterns_with_change += 1
try:
rfd = rfd_from_content(patterns, new_vinfo, content)
except rewrite.NoPatternMatch:
@ -175,6 +184,8 @@ def diff(
rfd = rfd._replace(path=str(file_path))
lines = rewrite.diff_lines(rfd)
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}'"
raise rewrite.NoPatternMatch(errmsg)

View file

@ -9,6 +9,8 @@ import typing as typ
import logging
import datetime as dt
import lexid
from . import version
from . import v2patterns
@ -18,15 +20,19 @@ logger = logging.getLogger("pycalver.v2version")
CalInfo = typ.Union[version.V2CalendarInfo, version.V2VersionInfo]
def _is_later_than(old: CalInfo, new: CalInfo) -> bool:
"""Is old > new based on non None fields."""
for field in version.V1CalendarInfo._fields:
aval = getattr(old, field)
bval = getattr(new, field)
if not (aval is None or bval is None):
if aval > bval:
return True
return False
def _is_cal_gt(left: CalInfo, right: CalInfo) -> bool:
"""Is left > right for non-None fields."""
lvals = []
rvals = []
for field in version.V2CalendarInfo._fields:
lval = getattr(left , field)
rval = getattr(right, field)
if not (lval is None or rval is None):
lvals.append(lval)
rvals.append(rval)
return lvals > rvals
def _ver_to_cal_info(vinfo: version.V2VersionInfo) -> version.V2CalendarInfo:
@ -268,9 +274,10 @@ def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool
TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]]
PartValues = typ.List[typ.Tuple[str, str]]
def _format_part_values(vinfo: version.V2VersionInfo) -> typ.Dict[str, str]:
def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues:
"""Generate kwargs for template from minimal V2VersionInfo.
The V2VersionInfo Tuple only has the minimal representation
@ -279,14 +286,14 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> typ.Dict[str, str]:
representation '09' for '0M'.
>>> vinfo = parse_version_info("v200709.1033-beta", pattern="vYYYY0M.BUILD[-TAG]")
>>> kwargs = _format_part_values(vinfo)
>>> kwargs = dict(_format_part_values(vinfo))
>>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['TAG'])
('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]")
>>> kwargs = _format_part_values(vinfo)
>>> kwargs = dict(_format_part_values(vinfo))
>>> (kwargs['YYYY'], kwargs['0M'], kwargs['BUILD'], kwargs['PYTAG'], kwargs['NUM'])
('2007', '09', '1033', 'b', '1')
"""
@ -299,7 +306,7 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> typ.Dict[str, str]:
format_fn = v2patterns.PART_FORMATS[part]
kwargs[part] = format_fn(field_val)
return kwargs
return sorted(kwargs.items(), key=lambda item: -len(item[0]))
def _make_segments(raw_pattern: str) -> typ.List[str]:
@ -345,12 +352,63 @@ def _clear_zero_segments(
return non_zero_segs
Segment = str
# mypy limitation wrt. cyclic definition
# SegmentTree = typ.List[typ.Union[Segment, "SegmentTree"]]
SegmentTree = typ.Any
def _parse_segment_tree(raw_pattern: str) -> SegmentTree:
"""Generate segment tree from pattern string.
>>> tree = _parse_segment_tree("aa[bb[cc]]")
>>> assert tree == ["aa", ["bb", ["cc"]]]
>>> tree = _parse_segment_tree("aa[bb[cc]dd[ee]ff]gg")
>>> assert tree == ["aa", ["bb", ["cc"], "dd", ["ee"], "ff"], "gg"]
"""
internal_root: SegmentTree = []
branch_stack : typ.List[SegmentTree] = [internal_root]
segment_start_index = -1
raw_pattern = "[" + raw_pattern + "]"
for i, char in enumerate(raw_pattern):
is_escaped = i > 0 and raw_pattern[i - 1] == "\\"
if char in "[]" and not is_escaped:
start = segment_start_index + 1
end = i
if start < end:
branch_stack[-1].append(raw_pattern[start:end])
if char == "[":
new_branch: SegmentTree = []
branch_stack[-1].append(new_branch)
branch_stack.append(new_branch)
segment_start_index = i
elif char == "]":
if len(branch_stack) == 1:
err = f"Unbalanced brace(s) in '{raw_pattern}'"
raise ValueError(err)
branch_stack.pop()
segment_start_index = i
else:
raise NotImplementedError("Unreachable")
if len(branch_stack) > 1:
err = f"Unclosed brace in '{raw_pattern}'"
raise ValueError(err)
return internal_root[0]
def _format_segments(
vinfo : version.V2VersionInfo,
pattern_segs: typ.List[str],
part_values : PartValues,
) -> typ.List[str]:
kwargs = _format_part_values(vinfo)
part_values = sorted(kwargs.items(), key=lambda item: -len(item[0]))
# NOTE (mb 2020-09-21): Old implementaion that doesn't cover corner
# cases relating to escaped braces.
is_zero_segment = [True] * len(pattern_segs)
@ -361,23 +419,25 @@ def _format_segments(
idx_r = len(pattern_segs) - 1
while idx_l <= idx_r:
# NOTE (mb 2020-09-18): All segments are optional,
# except the most left and the most right,
# i.e the ones NOT surrounded by braces.
# Empty string is a valid segment.
is_optional = idx_l > 0
# except the most left and the most right.
# In other words the ones NOT surrounded by braces are
# required. Empty string is a valid segment.
is_required_seg = idx_l == 0
seg_l = pattern_segs[idx_l]
seg_r = pattern_segs[idx_r]
for part, part_value in part_values:
if part in seg_l:
seg_l = seg_l.replace(part, part_value)
if not (is_optional and str(part_value) == version.ZERO_VALUES.get(part)):
seg_l = seg_l.replace(part, part_value)
is_zero_seg = str(part_value) == version.ZERO_VALUES.get(part)
if is_required_seg or not is_zero_seg:
is_zero_segment[idx_l] = False
if part in seg_r:
seg_r = seg_r.replace(part, part_value)
if not (is_optional and str(part_value) == version.ZERO_VALUES[part]):
seg_r = seg_r.replace(part, part_value)
is_zero_seg = str(part_value) == version.ZERO_VALUES.get(part)
if is_required_seg or not is_zero_seg:
is_zero_segment[idx_r] = False
formatted_segs_l.append(seg_l)
@ -391,6 +451,43 @@ def _format_segments(
return _clear_zero_segments(formatted_segs, is_zero_segment)
FormattedSegmentParts = typ.List[str]
def _format_segment_tree(
seg_tree: SegmentTree,
part_values : PartValues,
) -> FormattedSegmentParts:
result_parts = []
for seg in seg_tree:
if isinstance(seg, list):
result_parts.extend(_format_segment_tree(seg, part_values))
else:
# NOTE (mb 2020-09-24): If a segment has any zero parts,
# the whole segment is skipped.
is_zero_seg = False
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:
is_zero_seg = True
else:
formatted_seg = formatted_seg.replace(part, part_value)
if not is_zero_seg:
result_parts.append(formatted_seg)
return result_parts
def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str:
"""Generate version string.
@ -477,10 +574,15 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str:
>>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]"')
'__version__ = "v1.0.0-rc2"'
"""
pattern_segs = _make_segments(raw_pattern)
formatted_segs = _format_segments(vinfo, pattern_segs)
part_values = _format_part_values(vinfo)
return "".join(formatted_segs)
# pattern_segs = _make_segments(raw_pattern)
# formatted_segs = _format_segments(pattern_segs, part_values)
# version_str = "".join(formatted_segs)
seg_tree = _parse_segment_tree(raw_pattern)
version_str_parts = _format_segment_tree(seg_tree, part_values)
return "".join(version_str_parts)
def incr(
@ -505,19 +607,30 @@ def incr(
cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info()
if _is_later_than(old_vinfo, cur_cinfo):
logger.warning(f"Version appears to be from the future '{old_version}'")
if _is_cal_gt(old_vinfo, cur_cinfo):
logger.warning(f"Old version appears to be from the future '{old_version}'")
cur_vinfo = old_vinfo
else:
cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict())
cur_vinfo = version.incr_non_cal_parts(
cur_vinfo,
release,
major,
minor,
patch,
)
# 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 release:
cur_vinfo = cur_vinfo._replace(tag=release)
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)
# TODO (mb 2020-09-20): New Rollover Behaviour:
# Reset major, minor, patch to zero if any part to the left of it is incremented
new_version = format_version(cur_vinfo, raw_pattern)
if new_version == old_version:
logger.error("Invalid arguments or pattern, version did not change.")

View file

@ -1,7 +1,11 @@
# 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
import typing as typ
import datetime as dt
import lexid
import pkg_resources
MaybeInt = typ.Optional[int]
@ -71,9 +75,6 @@ class V2VersionInfo(typ.NamedTuple):
pytag : str
VersionInfoType = typ.TypeVar('VersionInfoType', V1VersionInfo, V2VersionInfo)
# The test suite may replace this.
TODAY = dt.datetime.utcnow().date()
@ -81,7 +82,7 @@ TODAY = dt.datetime.utcnow().date()
TAG_BY_PEP440_TAG = {
'a' : 'alpha',
'b' : 'beta',
"" : 'final',
'' : 'final',
'rc' : 'rc',
'dev' : 'dev',
'post': 'post',
@ -89,13 +90,19 @@ TAG_BY_PEP440_TAG = {
PEP440_TAG_BY_TAG = {
'alpha': "a",
'beta' : "b",
'final': "",
'pre' : "rc",
'rc' : "rc",
'dev' : "dev",
'post' : "post",
'a' : 'a',
'b' : 'b',
'dev' : 'dev',
'alpha' : 'a',
'beta' : 'b',
'preview': 'rc',
'pre' : 'rc',
'rc' : 'rc',
'c' : 'rc',
'final' : '',
'post' : 'post',
'r' : 'post',
'rev' : 'post',
}
assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values())
@ -148,28 +155,3 @@ def to_pep440(version: str) -> str:
'201811.7b0'
"""
return str(pkg_resources.parse_version(version))
def incr_non_cal_parts(
vinfo : VersionInfoType,
release: typ.Optional[str],
major : bool,
minor : bool,
patch : bool,
) -> VersionInfoType:
_bid = vinfo.bid
if int(_bid) < 1000:
# prevent truncation of leading zeros
_bid = str(int(_bid) + 1000)
vinfo = vinfo._replace(bid=lexid.next_id(_bid))
if release:
vinfo = vinfo._replace(tag=release)
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)
return vinfo