Visitar URL original
Preserve version order when legacy prerelease counters roll over (#500) · python-semver/python-semver@9ef4131 · GitHub
Skip to content

Commit 9ef4131

Browse files
authored
Preserve version order when legacy prerelease counters roll over (#500)
Advance the patch version when incrementing an old-style prerelease counter would otherwise produce a lower version, while preserving the token and incremented counter. Keep the existing patch for increments that already move forward. Add regression coverage for counter rollovers, build metadata, custom tokens, and repeated bumps; document the behavior in the changelog.
1 parent bcd151f commit 9ef4131

4 files changed

Lines changed: 67 additions & 3 deletions

File tree

‎changelog.d/499.bugfix.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``Version.next_version("prerelease")`` now advances the patch version when
2+
incrementing an old-style prerelease counter would produce an older version.
3+
For example, ``1.2.3-rc9`` becomes ``1.2.4-rc10``. The legacy token and
4+
incremented counter are preserved; increments that already move forward keep
5+
the current patch version.

‎src/semver/version.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,12 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version":
524524
>>> str(semver.Version.parse("1.2.3-rc.1").next_version("prerelease", "alpha"))
525525
'1.2.4-alpha.1'
526526
527+
Old-style counters also advance the patch version if incrementing
528+
the counter would move backwards under SemVer's lexical ordering:
529+
530+
>>> str(semver.Version.parse("1.2.3-rc9").next_version("prerelease"))
531+
'1.2.4-rc10'
532+
527533
:param part: One of "major", "minor", "patch", or "prerelease"
528534
:param prerelease_token: prefix string of prerelease, defaults to 'rc'
529535
:return: new object with the appropriate part raised
@@ -560,9 +566,14 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version":
560566
)
561567
if match:
562568
num = int(match.group(1))
563-
return version.replace(
569+
candidate = version.replace(
564570
prerelease=f"{prerelease_token}{num + 1}", build=None
565571
)
572+
if candidate > version:
573+
return candidate
574+
return candidate.bump_patch().replace(
575+
prerelease=candidate.prerelease
576+
)
566577

567578
if not (
568579
version.prerelease == prerelease_token

‎tests/test_issue484.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ def test_issue_484_rc3_to_rc4():
3131

3232

3333
def test_issue_484_rc9_to_rc10():
34-
"""Ensure numeric bumping works across digit boundaries."""
34+
"""Keep the legacy counter while ensuring digit rollover moves forward."""
3535
v = Version.parse("1.2.3-rc9")
36-
assert str(v.next_version("prerelease")) == "1.2.3-rc10"
36+
result = v.next_version("prerelease")
37+
assert str(result) == "1.2.4-rc10"
38+
assert result > v
3739

3840

3941
def test_issue_484_dot_style_unchanged():
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Legacy prerelease counters must not make next_version move backwards."""
2+
3+
import pytest
4+
5+
from semver import Version
6+
7+
8+
@pytest.mark.parametrize(
9+
"current,token,expected",
10+
[
11+
("1.2.3-rc9", "rc", "1.2.4-rc10"),
12+
("1.2.3-rc99", "rc", "1.2.4-rc100"),
13+
("1.2.3-rc999+build.7", "rc", "1.2.4-rc1000"),
14+
("1.2.3-beta9", "beta", "1.2.4-beta10"),
15+
("1.2.3-alpha.beta9", "alpha.beta", "1.2.4-alpha.beta10"),
16+
("1.2.3-0rc9", "0rc", "1.2.4-0rc10"),
17+
("0.0.0-rc9", "rc", "0.0.1-rc10"),
18+
("1.2.3-rc8", "rc", "1.2.3-rc9"),
19+
("1.2.3-rc98", "rc", "1.2.3-rc99"),
20+
("1.2.3-rc099", "rc", "1.2.3-rc100"),
21+
("1.2.3-rc09", "rc", "1.2.3-rc10"),
22+
("1.2.3-rc.9", "rc", "1.2.3-rc.10"),
23+
],
24+
)
25+
def test_next_legacy_counter_preserves_order(current, token, expected):
26+
original = Version.parse(current)
27+
result = original.next_version("prerelease", token)
28+
assert str(result) == expected
29+
assert result > original
30+
assert result.build is None
31+
assert Version.parse(str(result)).to_tuple() == result.to_tuple()
32+
assert str(original) == current
33+
34+
35+
def test_repeated_legacy_bumps_preserve_order_and_subclass():
36+
class Release(Version):
37+
pass
38+
39+
current = Release.parse("1.2.3-rc8+build.7")
40+
for expected in ("1.2.3-rc9", "1.2.4-rc10", "1.2.4-rc11"):
41+
result = current.next_version("prerelease")
42+
assert isinstance(result, Release)
43+
assert str(result) == expected
44+
assert result > current
45+
assert result.build is None
46+
current = result

0 commit comments

Comments
 (0)