Skip to content

Commit bcd151f

Browse files
Apeksha9389tomschr
andauthored
Preserve version order when switching prerelease tokens (#498)
Fixes #497 When calling `Version.parse().next_version("prerelease", token)`, transitioning to a prerelease token that sorts earlier (e.g., from `rc` to `alpha`) previously returned a version older than the input, violating ordering guarantees. Fix this by advancing the patch version and resetting the requested prerelease token on the new patch (e.g., `1.2.3-rc.1` -> `1.2.4-alpha.1`). * Forward token transitions (e.g., `alpha` to `rc`) retain the current patch. * Same-token increments maintain existing behavior. * Build metadata is cleared on increment; original version instances remain immutable. * Update outdated test expectation and add regression test suite covering token transitions. --------- Co-authored-by: Tom Schraitle <tomschr@users.noreply.github.com>
1 parent 94debf9 commit bcd151f

5 files changed

Lines changed: 54 additions & 2 deletions

File tree

‎changelog.d/497.bugfix.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
``Version.next_version("prerelease", prerelease_token=...)`` now advances the
2+
patch version when switching tokens would otherwise produce a lower version.
3+
For example, switching from ``1.2.3-rc.1`` to ``alpha`` returns
4+
``1.2.4-alpha.1``. Forward token changes retain the current patch version.

‎docs/usage/increase-parts-of-a-version_prereleases.rst‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,18 @@ would perhaps a better fit.
2222
'3.4.6'
2323
>>> str(Version.parse("0.1.4").next_version("prerelease"))
2424
'0.1.5-rc.1'
25+
26+
Switching Prerelease Tokens
27+
---------------------------
28+
29+
When switching between prerelease tokens, :meth:`~semver.version.Version.next_version`
30+
preserves natural ordering. Advancing to a higher token retains the patch version,
31+
while switching to an earlier token increments the patch version to prevent a downgrade:
32+
33+
.. code-block:: python
34+
35+
>>> v = Version.parse("1.2.3-alpha.1")
36+
>>> str(v.next_version("prerelease", prerelease_token="beta"))
37+
'1.2.3-beta.1'
38+
>>> str(Version.parse("1.2.3-rc.1").next_version("prerelease", prerelease_token="alpha"))
39+
'1.2.4-alpha.1'

‎src/semver/version.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,12 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version":
518518
>>> str(semver.Version.parse("0.1.4").next_version("prerelease"))
519519
'0.1.5-rc.1'
520520
521+
Switching to an earlier prerelease token advances the patch version
522+
so the result remains greater than the original version:
523+
524+
>>> str(semver.Version.parse("1.2.3-rc.1").next_version("prerelease", "alpha"))
525+
'1.2.4-alpha.1'
526+
521527
:param part: One of "major", "minor", "patch", or "prerelease"
522528
:param prerelease_token: prefix string of prerelease, defaults to 'rc'
523529
:return: new object with the appropriate part raised
@@ -562,7 +568,12 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version":
562568
version.prerelease == prerelease_token
563569
or version.prerelease.startswith(prerelease_token + ".")
564570
):
565-
return version.replace(prerelease=f"{prerelease_token}.1", build=None)
571+
candidate = version.replace(
572+
prerelease=f"{prerelease_token}.1", build=None
573+
)
574+
if candidate > version:
575+
return candidate
576+
return version.bump_patch().bump_prerelease(prerelease_token)
566577
return version.bump_prerelease(prerelease_token, bump_when_empty=True)
567578

568579
@_comparator

‎tests/test_bump.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def test_should_ignore_build_on_prerelease_bump():
160160
("1.2.3-alpha.beta.gamma.1", "alpha", "1.2.3-alpha.beta.gamma.2"),
161161
("1.2.3-rc3", "rc", "1.2.3-rc4"),
162162
("1.2.3-rc.3", "rc", "1.2.3-rc.4"),
163-
("1.2.3-alphaXbeta9", "alpha.beta", "1.2.3-alpha.beta.1"),
163+
("1.2.3-alphaXbeta9", "alpha.beta", "1.2.4-alpha.beta.1"),
164164
],
165165
)
166166
def test_next_version_matches_literal_prerelease_token_family(
@@ -170,6 +170,7 @@ def test_next_version_matches_literal_prerelease_token_family(
170170
result = original.next_version("prerelease", token)
171171
assert str(result) == expected
172172
assert Version.parse(str(result)).to_tuple() == result.to_tuple()
173+
assert result > original
173174
assert str(original) == version
174175

175176

‎tests/test_issue484.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
"""Regression test for issue #484: next_version with old-style prereleases."""
22

3+
import pytest
4+
35
from semver import Version
46

57

8+
@pytest.mark.parametrize(
9+
"current, token, expected",
10+
[
11+
("1.2.3-rc.1", "alpha", "1.2.4-alpha.1"),
12+
("1.2.3-beta.3", "alpha", "1.2.4-alpha.1"),
13+
("1.2.3-rc.1+build.7", "dev", "1.2.4-dev.1"),
14+
("1.2.3-rc3", "alpha", "1.2.4-alpha.1"),
15+
("1.2.3-rc.1", "2", "1.2.4-2.1"),
16+
("1.2.3-alpha.4", "rc", "1.2.3-rc.1"),
17+
],
18+
)
19+
def test_token_switch_preserves_version_order(current, token, expected):
20+
original = Version.parse(current)
21+
result = original.next_version("prerelease", token)
22+
assert str(result) == expected
23+
assert result > original
24+
assert str(original) == current
25+
26+
627
def test_issue_484_rc3_to_rc4():
728
"""Old-style prerelease 'rc3' should bump to 'rc4', not reset to 'rc.1'."""
829
v = Version.parse("0.1.2-rc3")

0 commit comments

Comments
 (0)