Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog.d/499.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
``Version.next_version("prerelease")`` now advances the patch version when
incrementing an old-style prerelease counter would produce an older version.
For example, ``1.2.3-rc9`` becomes ``1.2.4-rc10``. The legacy token and
incremented counter are preserved; increments that already move forward keep
the current patch version.
13 changes: 12 additions & 1 deletion src/semver/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,12 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version":
>>> str(semver.Version.parse("1.2.3-rc.1").next_version("prerelease", "alpha"))
'1.2.4-alpha.1'

Old-style counters also advance the patch version if incrementing
the counter would move backwards under SemVer's lexical ordering:

>>> str(semver.Version.parse("1.2.3-rc9").next_version("prerelease"))
'1.2.4-rc10'

:param part: One of "major", "minor", "patch", or "prerelease"
:param prerelease_token: prefix string of prerelease, defaults to 'rc'
:return: new object with the appropriate part raised
Expand Down Expand Up @@ -560,9 +566,14 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version":
)
if match:
num = int(match.group(1))
return version.replace(
candidate = version.replace(
prerelease=f"{prerelease_token}{num + 1}", build=None
)
if candidate > version:
return candidate
return candidate.bump_patch().replace(
prerelease=candidate.prerelease
)

if not (
version.prerelease == prerelease_token
Expand Down
6 changes: 4 additions & 2 deletions tests/test_issue484.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ def test_issue_484_rc3_to_rc4():


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


def test_issue_484_dot_style_unchanged():
Expand Down
46 changes: 46 additions & 0 deletions tests/test_next_version_legacy_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Legacy prerelease counters must not make next_version move backwards."""

import pytest

from semver import Version


@pytest.mark.parametrize(
"current,token,expected",
[
("1.2.3-rc9", "rc", "1.2.4-rc10"),
("1.2.3-rc99", "rc", "1.2.4-rc100"),
("1.2.3-rc999+build.7", "rc", "1.2.4-rc1000"),
("1.2.3-beta9", "beta", "1.2.4-beta10"),
("1.2.3-alpha.beta9", "alpha.beta", "1.2.4-alpha.beta10"),
("1.2.3-0rc9", "0rc", "1.2.4-0rc10"),
("0.0.0-rc9", "rc", "0.0.1-rc10"),
("1.2.3-rc8", "rc", "1.2.3-rc9"),
("1.2.3-rc98", "rc", "1.2.3-rc99"),
("1.2.3-rc099", "rc", "1.2.3-rc100"),
("1.2.3-rc09", "rc", "1.2.3-rc10"),
("1.2.3-rc.9", "rc", "1.2.3-rc.10"),
],
)
def test_next_legacy_counter_preserves_order(current, token, expected):
original = Version.parse(current)
result = original.next_version("prerelease", token)
assert str(result) == expected
assert result > original
assert result.build is None
assert Version.parse(str(result)).to_tuple() == result.to_tuple()
assert str(original) == current


def test_repeated_legacy_bumps_preserve_order_and_subclass():
class Release(Version):
pass

current = Release.parse("1.2.3-rc8+build.7")
for expected in ("1.2.3-rc9", "1.2.4-rc10", "1.2.4-rc11"):
result = current.next_version("prerelease")
assert isinstance(result, Release)
assert str(result) == expected
assert result > current
assert result.build is None
current = result
Loading