mirror of
https://github.com/TECHNOFAB11/bumpver.git
synced 2025-12-12 14:30:09 +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 week numbering.
|
||||||
- Better support for optional parts.
|
- Better support for optional parts.
|
||||||
- New: Start `BUILD` parts at `1000` to avoid leading zero truncation.
|
- 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 gitlab #2: Added `grep` subcommand to find and debug patterns.
|
||||||
- New: Added better error messages to debug regular expressions.
|
- 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.
|
- 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`
|
- `message: TODO (mb 2020-09-18): Investigate error messages`
|
||||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||||
- `date : 2020-09-19T16:24:10`
|
- `date : 2020-09-19T16:24:10`
|
||||||
|
|
||||||
```
|
```
|
||||||
389: def _bump(
|
391: def _bump(
|
||||||
...
|
...
|
||||||
417: sys.exit(1)
|
419: sys.exit(1)
|
||||||
418: except Exception as ex:
|
420: except Exception as ex:
|
||||||
> 419: # TODO (mb 2020-09-18): Investigate error messages
|
> 421: # TODO (mb 2020-09-18): Investigate error messages
|
||||||
420: logger.error(str(ex))
|
422: logger.error(str(ex))
|
||||||
421: sys.exit(1)
|
423: 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:
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# W0703: broad-except
|
# 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`
|
- `message: Catching too general exception Exception`
|
||||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||||
- `date : 2020-09-05T14:30:17`
|
- `date : 2020-09-05T14:30:17`
|
||||||
|
|
||||||
```
|
```
|
||||||
389: def _bump(
|
391: def _bump(
|
||||||
...
|
...
|
||||||
416: logger.error(str(ex))
|
418: logger.error(str(ex))
|
||||||
417: sys.exit(1)
|
419: sys.exit(1)
|
||||||
> 418: except Exception as ex:
|
> 420: except Exception as ex:
|
||||||
419: # TODO (mb 2020-09-18): Investigate error messages
|
421: # TODO (mb 2020-09-18): Investigate error messages
|
||||||
420: logger.error(str(ex))
|
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]))
|
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
|
Segment = str
|
||||||
# mypy limitation wrt. cyclic definition
|
# mypy limitation wrt. cyclic definition
|
||||||
# SegmentTree = typ.List[typ.Union[Segment, "SegmentTree"]]
|
# SegmentTree = typ.List[typ.Union[Segment, "SegmentTree"]]
|
||||||
SegmentTree = typ.Any
|
SegmentTree = typ.Any
|
||||||
|
|
||||||
|
|
||||||
def _parse_segment_tree(raw_pattern: str) -> SegmentTree:
|
def _parse_segtree(raw_pattern: str) -> SegmentTree:
|
||||||
"""Generate segment tree from pattern string.
|
"""Generate segment tree from pattern string.
|
||||||
|
|
||||||
>>> tree = _parse_segment_tree("aa[bb[cc]]")
|
>>> _parse_segtree('aa[bb[cc]]')
|
||||||
>>> assert tree == ["aa", ["bb", ["cc"]]]
|
['aa', ['bb', ['cc']]]
|
||||||
>>> tree = _parse_segment_tree("aa[bb[cc]dd[ee]ff]gg")
|
>>> _parse_segtree('aa[bb[cc]dd[ee]ff]gg')
|
||||||
>>> assert tree == ["aa", ["bb", ["cc"], "dd", ["ee"], "ff"], "gg"]
|
['aa', ['bb', ['cc'], 'dd', ['ee'], 'ff'], 'gg']
|
||||||
"""
|
"""
|
||||||
|
|
||||||
internal_root: SegmentTree = []
|
internal_root: SegmentTree = []
|
||||||
|
|
@ -428,7 +411,7 @@ def _format_segment(seg: Segment, part_values: PartValues) -> FormatedSeg:
|
||||||
|
|
||||||
|
|
||||||
def _format_segment_tree(
|
def _format_segment_tree(
|
||||||
seg_tree : SegmentTree,
|
segtree : SegmentTree,
|
||||||
part_values: PartValues,
|
part_values: PartValues,
|
||||||
) -> FormatedSeg:
|
) -> FormatedSeg:
|
||||||
# print("??>>>", seg_tree)
|
# 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.
|
# is only omitted, if all parts to the right of it were also omitted.
|
||||||
result_parts: typ.List[str] = []
|
result_parts: typ.List[str] = []
|
||||||
is_zero = True
|
is_zero = True
|
||||||
for seg in seg_tree:
|
for seg in segtree:
|
||||||
if isinstance(seg, list):
|
if isinstance(seg, list):
|
||||||
formatted_seg = _format_segment_tree(seg, part_values)
|
formatted_seg = _format_segment_tree(seg, part_values)
|
||||||
else:
|
else:
|
||||||
|
|
@ -541,38 +524,95 @@ def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str:
|
||||||
'__version__ = "v1.0.0-rc2"'
|
'__version__ = "v1.0.0-rc2"'
|
||||||
"""
|
"""
|
||||||
part_values = _format_part_values(vinfo)
|
part_values = _format_part_values(vinfo)
|
||||||
seg_tree = _parse_segment_tree(raw_pattern)
|
segtree = _parse_segtree(raw_pattern)
|
||||||
formatted_seg = _format_segment_tree(seg_tree, part_values)
|
formatted_seg = _format_segment_tree(segtree, part_values)
|
||||||
return formatted_seg.result
|
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(
|
def _incr_numeric(
|
||||||
vinfo : version.V2VersionInfo,
|
raw_pattern: str,
|
||||||
|
old_vinfo : version.V2VersionInfo,
|
||||||
|
cur_vinfo : version.V2VersionInfo,
|
||||||
major : bool,
|
major : bool,
|
||||||
minor : bool,
|
minor : bool,
|
||||||
patch : bool,
|
patch : bool,
|
||||||
release : typ.Optional[str],
|
tag : typ.Optional[str],
|
||||||
release_num: bool,
|
release_num: bool,
|
||||||
) -> version.V2VersionInfo:
|
) -> 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
|
# prevent truncation of leading zeros
|
||||||
if int(vinfo.bid) < 1000:
|
if int(cur_vinfo.bid) < 1000:
|
||||||
vinfo = vinfo._replace(bid=str(int(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:
|
if major and 'major' not in reset_fields:
|
||||||
vinfo = vinfo._replace(major=vinfo.major + 1, minor=0, patch=0)
|
cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0)
|
||||||
if minor:
|
if minor and 'minor' not in reset_fields:
|
||||||
vinfo = vinfo._replace(minor=vinfo.minor + 1, patch=0)
|
cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0)
|
||||||
if patch:
|
if patch and 'patch' not in reset_fields:
|
||||||
vinfo = vinfo._replace(patch=vinfo.patch + 1)
|
cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1)
|
||||||
if release_num:
|
if release_num and 'release_num' not in reset_fields:
|
||||||
vinfo = vinfo._replace(num=vinfo.num + 1)
|
cur_vinfo = cur_vinfo._replace(num=cur_vinfo.num + 1)
|
||||||
if release:
|
if tag and 'tag' not in reset_fields:
|
||||||
if release != vinfo.tag:
|
if tag != cur_vinfo.tag:
|
||||||
vinfo = vinfo._replace(num=0)
|
cur_vinfo = cur_vinfo._replace(num=0)
|
||||||
vinfo = vinfo._replace(tag=release)
|
cur_vinfo = cur_vinfo._replace(tag=tag)
|
||||||
return vinfo
|
return cur_vinfo
|
||||||
|
|
||||||
|
|
||||||
def is_valid_week_pattern(raw_pattern) -> bool:
|
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_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_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"])
|
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)):
|
if has_yy_part and has_vv_part:
|
||||||
return True
|
|
||||||
elif has_yy_part and has_vv_part:
|
|
||||||
alt1 = raw_pattern.replace("V", "W")
|
alt1 = raw_pattern.replace("V", "W")
|
||||||
alt2 = raw_pattern.replace("Y", "G")
|
alt2 = raw_pattern.replace("Y", "G")
|
||||||
logger.error(f"Invalid pattern: '{raw_pattern}'. Maybe try {alt1} or {alt2}")
|
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 = old_vinfo._replace(**cur_cinfo._asdict())
|
||||||
|
|
||||||
cur_vinfo = _incr_numeric(
|
cur_vinfo = _incr_numeric(
|
||||||
|
raw_pattern,
|
||||||
|
old_vinfo,
|
||||||
cur_vinfo,
|
cur_vinfo,
|
||||||
major=major,
|
major=major,
|
||||||
minor=minor,
|
minor=minor,
|
||||||
patch=patch,
|
patch=patch,
|
||||||
release=release,
|
tag=tag,
|
||||||
release_num=release_num,
|
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)
|
new_version = format_version(cur_vinfo, raw_pattern)
|
||||||
if new_version == old_version:
|
if new_version == old_version:
|
||||||
logger.error("Invalid arguments or pattern, version did not change.")
|
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())
|
assert set(RELEASE_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_RELEASE.keys())
|
||||||
|
|
||||||
|
|
||||||
ZERO_VALUES = {
|
PART_ZERO_VALUES = {
|
||||||
'MAJOR' : "0",
|
'MAJOR' : "0",
|
||||||
'MINOR' : "0",
|
'MINOR' : "0",
|
||||||
'PATCH' : "0",
|
'PATCH' : "0",
|
||||||
'RELEASE': "final",
|
'RELEASE': "final",
|
||||||
'PYTAG' : "",
|
'PYTAG' : "",
|
||||||
'NUM' : "0",
|
'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:
|
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):
|
class PatternError(Exception):
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import re
|
||||||
import time
|
import time
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
|
import datetime as dt
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -21,6 +22,7 @@ from pycalver import v2cli
|
||||||
from pycalver import config
|
from pycalver import config
|
||||||
from pycalver import v1patterns
|
from pycalver import v1patterns
|
||||||
from pycalver.__main__ import cli
|
from pycalver.__main__ import cli
|
||||||
|
from pycalver.__main__ import incr_dispatch
|
||||||
|
|
||||||
# pylint:disable=redefined-outer-name ; pytest fixtures
|
# pylint:disable=redefined-outer-name ; pytest fixtures
|
||||||
# pylint:disable=protected-access ; allowed for test code
|
# 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 "Hello World v202010.1003-beta !" in readme_text
|
||||||
assert "[aka. 202010.1003b0 !]" 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