mirror of
https://github.com/TECHNOFAB11/bumpver.git
synced 2025-12-12 06:20:08 +01:00
wip: implement v2 rollover behavior
This commit is contained in:
parent
37777ee133
commit
879ff4a945
5 changed files with 148 additions and 84 deletions
|
|
@ -7,6 +7,7 @@
|
|||
- Better support for week numbering.
|
||||
- Better support for optional parts.
|
||||
- New: Start `BUILD` parts at `1000` to avoid leading zero truncation.
|
||||
- New: `MAJOR`/`MINOR`/`PATCH`/`INC` will roll over when a date part changes to their left.
|
||||
- New gitlab #2: Added `grep` subcommand to find and debug patterns.
|
||||
- New: Added better error messages to debug regular expressions.
|
||||
- New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts.
|
||||
|
|
|
|||
|
|
@ -63,56 +63,39 @@ The recommended approach to using `pylint-ignore` is:
|
|||
```
|
||||
|
||||
|
||||
## File src/pycalver/__main__.py - Line 419 - W0511 (fixme)
|
||||
## File src/pycalver/__main__.py - Line 421 - W0511 (fixme)
|
||||
|
||||
- `message: TODO (mb 2020-09-18): Investigate error messages`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-09-19T16:24:10`
|
||||
|
||||
```
|
||||
389: def _bump(
|
||||
391: def _bump(
|
||||
...
|
||||
417: sys.exit(1)
|
||||
418: except Exception as ex:
|
||||
> 419: # TODO (mb 2020-09-18): Investigate error messages
|
||||
420: logger.error(str(ex))
|
||||
421: sys.exit(1)
|
||||
```
|
||||
|
||||
|
||||
## File src/pycalver/v2version.py - Line 641 - W0511 (fixme)
|
||||
|
||||
- `message: TODO (mb 2020-09-20): New Rollover Behaviour:`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-10-03T19:31:28`
|
||||
|
||||
```
|
||||
599: def incr(
|
||||
...
|
||||
639: )
|
||||
640:
|
||||
> 641: # TODO (mb 2020-09-20): New Rollover Behaviour:
|
||||
642: # Reset major, minor, patch to zero if any part to the left of it is incremented
|
||||
643:
|
||||
419: sys.exit(1)
|
||||
420: except Exception as ex:
|
||||
> 421: # TODO (mb 2020-09-18): Investigate error messages
|
||||
422: logger.error(str(ex))
|
||||
423: sys.exit(1)
|
||||
```
|
||||
|
||||
|
||||
# W0703: broad-except
|
||||
|
||||
## File src/pycalver/__main__.py - Line 418 - W0703 (broad-except)
|
||||
## File src/pycalver/__main__.py - Line 420 - W0703 (broad-except)
|
||||
|
||||
- `message: Catching too general exception Exception`
|
||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||
- `date : 2020-09-05T14:30:17`
|
||||
|
||||
```
|
||||
389: def _bump(
|
||||
391: def _bump(
|
||||
...
|
||||
416: logger.error(str(ex))
|
||||
417: sys.exit(1)
|
||||
> 418: except Exception as ex:
|
||||
419: # TODO (mb 2020-09-18): Investigate error messages
|
||||
420: logger.error(str(ex))
|
||||
418: logger.error(str(ex))
|
||||
419: sys.exit(1)
|
||||
> 420: except Exception as ex:
|
||||
421: # TODO (mb 2020-09-18): Investigate error messages
|
||||
422: logger.error(str(ex))
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -318,36 +318,19 @@ def _format_part_values(vinfo: version.V2VersionInfo) -> PartValues:
|
|||
return sorted(kwargs.items(), key=lambda item: -len(item[0]))
|
||||
|
||||
|
||||
def _clear_zero_segments(
|
||||
formatted_segs: typ.List[str], is_zero_segment: typ.List[bool]
|
||||
) -> typ.List[str]:
|
||||
non_zero_segs = list(formatted_segs)
|
||||
|
||||
has_val_to_right = False
|
||||
for idx, is_zero in reversed(list(enumerate(is_zero_segment))):
|
||||
is_optional = 0 < idx < len(formatted_segs) - 1
|
||||
if is_optional:
|
||||
if is_zero and not has_val_to_right:
|
||||
non_zero_segs[idx] = ""
|
||||
else:
|
||||
has_val_to_right = True
|
||||
|
||||
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:
|
||||
def _parse_segtree(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"]
|
||||
>>> _parse_segtree('aa[bb[cc]]')
|
||||
['aa', ['bb', ['cc']]]
|
||||
>>> _parse_segtree('aa[bb[cc]dd[ee]ff]gg')
|
||||
['aa', ['bb', ['cc'], 'dd', ['ee'], 'ff'], 'gg']
|
||||
"""
|
||||
|
||||
internal_root: SegmentTree = []
|
||||
|
|
@ -428,7 +411,7 @@ def _format_segment(seg: Segment, part_values: PartValues) -> FormatedSeg:
|
|||
|
||||
|
||||
def _format_segment_tree(
|
||||
seg_tree : SegmentTree,
|
||||
segtree : SegmentTree,
|
||||
part_values: PartValues,
|
||||
) -> FormatedSeg:
|
||||
# print("??>>>", seg_tree)
|
||||
|
|
@ -437,7 +420,7 @@ def _format_segment_tree(
|
|||
# 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:
|
||||
for seg in segtree:
|
||||
if isinstance(seg, list):
|
||||
formatted_seg = _format_segment_tree(seg, part_values)
|
||||
else:
|
||||
|
|
@ -541,38 +524,95 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str:
|
|||
'__version__ = "v1.0.0-rc2"'
|
||||
"""
|
||||
part_values = _format_part_values(vinfo)
|
||||
seg_tree = _parse_segment_tree(raw_pattern)
|
||||
formatted_seg = _format_segment_tree(seg_tree, part_values)
|
||||
segtree = _parse_segtree(raw_pattern)
|
||||
formatted_seg = _format_segment_tree(segtree, part_values)
|
||||
return formatted_seg.result
|
||||
|
||||
|
||||
def _iter_flat_segtree(segtree: SegmentTree) -> typ.Iterable[Segment]:
|
||||
"""Flatten a SegmentTree (mixed nested list of lists or str).
|
||||
|
||||
>>> list(_iter_flat_segtree(['aa', ['bb', ['cc'], 'dd', ['ee'], 'ff'], 'gg']))
|
||||
['aa', 'bb', 'cc', 'dd', 'ee', 'ff', 'gg']
|
||||
"""
|
||||
for subtree in segtree:
|
||||
if isinstance(subtree, list):
|
||||
for seg in _iter_flat_segtree(subtree):
|
||||
yield seg
|
||||
else:
|
||||
yield subtree
|
||||
|
||||
|
||||
def _parse_pattern_fields(raw_pattern: str) -> typ.List[str]:
|
||||
parts = list(v2patterns.PATTERN_PART_FIELDS.keys())
|
||||
parts.sort(key=len, reverse=True)
|
||||
|
||||
segtree = _parse_segtree(raw_pattern)
|
||||
segments = _iter_flat_segtree(segtree)
|
||||
|
||||
fields_by_index = {}
|
||||
for segment_index, segment in enumerate(segments):
|
||||
for part in parts:
|
||||
part_index = segment.find(part)
|
||||
if part_index >= 0:
|
||||
field = v2patterns.PATTERN_PART_FIELDS[part]
|
||||
fields_by_index[segment_index, part_index] = field
|
||||
|
||||
return [field for _, field in sorted(fields_by_index.items())]
|
||||
|
||||
|
||||
def _iter_reset_field_items(
|
||||
fields : typ.List[str],
|
||||
old_vinfo: version.V2VersionInfo,
|
||||
cur_vinfo: version.V2VersionInfo,
|
||||
) -> typ.Iterable[typ.Tuple[str, str]]:
|
||||
# Any field to the left of another can reset all to the right
|
||||
has_reset = False
|
||||
for field in fields:
|
||||
zero_val = version.V2_FIELD_ZERO_VALUES.get(field)
|
||||
if has_reset and zero_val is not None:
|
||||
yield field, zero_val
|
||||
elif getattr(old_vinfo, field) != getattr(cur_vinfo, field):
|
||||
has_reset = True
|
||||
|
||||
|
||||
def _incr_numeric(
|
||||
vinfo : version.V2VersionInfo,
|
||||
raw_pattern: str,
|
||||
old_vinfo : version.V2VersionInfo,
|
||||
cur_vinfo : version.V2VersionInfo,
|
||||
major : bool,
|
||||
minor : bool,
|
||||
patch : bool,
|
||||
release : typ.Optional[str],
|
||||
tag : typ.Optional[str],
|
||||
release_num: bool,
|
||||
) -> version.V2VersionInfo:
|
||||
# Reset major/minor/patch/num/inc to zero if any part to the left of it is incremented
|
||||
fields = _parse_pattern_fields(raw_pattern)
|
||||
reset_fields = dict(_iter_reset_field_items(fields, old_vinfo, cur_vinfo))
|
||||
|
||||
cur_kwargs = cur_vinfo._asdict()
|
||||
cur_kwargs.update(reset_fields)
|
||||
cur_vinfo = version.V2VersionInfo(**cur_kwargs)
|
||||
|
||||
# prevent truncation of leading zeros
|
||||
if int(vinfo.bid) < 1000:
|
||||
vinfo = vinfo._replace(bid=str(int(vinfo.bid) + 1000))
|
||||
if int(cur_vinfo.bid) < 1000:
|
||||
cur_vinfo = cur_vinfo._replace(bid=str(int(cur_vinfo.bid) + 1000))
|
||||
|
||||
vinfo = vinfo._replace(bid=lexid.next_id(vinfo.bid))
|
||||
cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_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
|
||||
if major and 'major' not in reset_fields:
|
||||
cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0)
|
||||
if minor and 'minor' not in reset_fields:
|
||||
cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0)
|
||||
if patch and 'patch' not in reset_fields:
|
||||
cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1)
|
||||
if release_num and 'release_num' not in reset_fields:
|
||||
cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1)
|
||||
if tag and 'tag' not in reset_fields:
|
||||
if tag != cur_vinfo.tag:
|
||||
cur_vinfo = cur_vinfo._replace(num=0)
|
||||
cur_vinfo = cur_vinfo._replace(tag=tag)
|
||||
return cur_vinfo
|
||||
|
||||
|
||||
def is_valid_week_pattern(raw_pattern) -> bool:
|
||||
|
|
@ -580,9 +620,7 @@ def is_valid_week_pattern(raw_pattern) -> bool:
|
|||
has_ww_part = any(part in raw_pattern for part in ["WW" , "0W", "UU", "0U"])
|
||||
has_gg_part = any(part in raw_pattern for part in ["GGGG", "GG", "0G"])
|
||||
has_vv_part = any(part in raw_pattern for part in ["VV" , "0V"])
|
||||
if not ((has_yy_part or has_gg_part) and (has_ww_part or has_vv_part)):
|
||||
return True
|
||||
elif has_yy_part and has_vv_part:
|
||||
if has_yy_part and has_vv_part:
|
||||
alt1 = raw_pattern.replace("V", "W")
|
||||
alt2 = raw_pattern.replace("Y", "G")
|
||||
logger.error(f"Invalid pattern: '{raw_pattern}'. Maybe try {alt1} or {alt2}")
|
||||
|
|
@ -630,17 +668,16 @@ def incr(
|
|||
cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict())
|
||||
|
||||
cur_vinfo = _incr_numeric(
|
||||
raw_pattern,
|
||||
old_vinfo,
|
||||
cur_vinfo,
|
||||
major=major,
|
||||
minor=minor,
|
||||
patch=patch,
|
||||
release=release,
|
||||
tag=tag,
|
||||
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
|
||||
|
||||
new_version = format_version(cur_vinfo, raw_pattern)
|
||||
if new_version == old_version:
|
||||
logger.error("Invalid arguments or pattern, version did not change.")
|
||||
|
|
|
|||
|
|
@ -109,18 +109,30 @@ assert set(RELEASE_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_RELEASE.values())
|
|||
assert set(RELEASE_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_RELEASE.keys())
|
||||
|
||||
|
||||
ZERO_VALUES = {
|
||||
PART_ZERO_VALUES = {
|
||||
'MAJOR' : "0",
|
||||
'MINOR' : "0",
|
||||
'PATCH' : "0",
|
||||
'RELEASE': "final",
|
||||
'PYTAG' : "",
|
||||
'NUM' : "0",
|
||||
'INC' : "0",
|
||||
}
|
||||
|
||||
|
||||
V2_FIELD_ZERO_VALUES = {
|
||||
'major': "0",
|
||||
'minor': "0",
|
||||
'patch': "0",
|
||||
'tag' : "final",
|
||||
'pytag': "",
|
||||
'num' : "0",
|
||||
'inc' : "0",
|
||||
}
|
||||
|
||||
|
||||
def is_zero_val(part: str, part_value: str) -> bool:
|
||||
return part in ZERO_VALUES and part_value == ZERO_VALUES[part]
|
||||
return part in PART_ZERO_VALUES and part_value == PART_ZERO_VALUES[part]
|
||||
|
||||
|
||||
class PatternError(Exception):
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import re
|
|||
import time
|
||||
import shlex
|
||||
import shutil
|
||||
import datetime as dt
|
||||
import subprocess as sp
|
||||
|
||||
import pytest
|
||||
|
|
@ -21,6 +22,7 @@ from pycalver import v2cli
|
|||
from pycalver import config
|
||||
from pycalver import v1patterns
|
||||
from pycalver.__main__ import cli
|
||||
from pycalver.__main__ import incr_dispatch
|
||||
|
||||
# pylint:disable=redefined-outer-name ; pytest fixtures
|
||||
# pylint:disable=protected-access ; allowed for test code
|
||||
|
|
@ -764,3 +766,32 @@ def test_multimatch_file_patterns(runner):
|
|||
|
||||
assert "Hello World v202010.1003-beta !" in readme_text
|
||||
assert "[aka. 202010.1003b0 !]" in readme_text
|
||||
|
||||
|
||||
def _kwargs(month, minor):
|
||||
return {'date': dt.date(2020, month, 1), 'minor': minor}
|
||||
|
||||
|
||||
ROLLOVER_TEST_CASES = [
|
||||
# v1 cases
|
||||
["{year}.{month}.{MINOR}", "2020.10.3", "2020.10.4", _kwargs(10, True)],
|
||||
["{year}.{month}.{MINOR}", "2020.10.3", None, _kwargs(10, False)],
|
||||
["{year}.{month}.{MINOR}", "2020.10.3", "2020.11.4", _kwargs(11, True)],
|
||||
["{year}.{month}.{MINOR}", "2020.10.3", "2020.11.3", _kwargs(11, False)],
|
||||
# v2 cases
|
||||
["YYYY.MM.MINOR" , "2020.10.3", "2020.10.4", _kwargs(10, True)],
|
||||
["YYYY.MM.MINOR" , "2020.10.3", None, _kwargs(10, False)],
|
||||
["YYYY.MM.MINOR" , "2020.10.3", "2020.11.0", _kwargs(11, True)],
|
||||
["YYYY.MM.MINOR" , "2020.10.3", "2020.11.0", _kwargs(11, False)],
|
||||
["YYYY.MM[.MINOR]", "2020.10.3", "2020.10.4", _kwargs(10, True)],
|
||||
["YYYY.MM[.MINOR]", "2020.10.3", "2020.11", _kwargs(11, False)],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version_pattern, old_version, expected, kwargs", ROLLOVER_TEST_CASES)
|
||||
def test_rollover(version_pattern, old_version, expected, kwargs):
|
||||
new_version = incr_dispatch(old_version, raw_pattern=version_pattern, **kwargs)
|
||||
if new_version is None:
|
||||
assert expected is None
|
||||
else:
|
||||
assert new_version == expected
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue