From d7e14fd1e8d2cc3a170c2d76d310b3282bff4935 Mon Sep 17 00:00:00 2001 From: Muneeb Sheikh Date: Sun, 11 Oct 2026 11:37:21 -0400 Subject: [PATCH 1/2] Bump large numeric identifiers without integer conversion --- changelog.d/large-identifiers.bugfix.rst | 2 ++ src/semver/version.py | 22 +++++++++--- tests/test_bump_large_identifiers.py | 46 ++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 changelog.d/large-identifiers.bugfix.rst create mode 100644 tests/test_bump_large_identifiers.py diff --git a/changelog.d/large-identifiers.bugfix.rst b/changelog.d/large-identifiers.bugfix.rst new file mode 100644 index 00000000..153f8042 --- /dev/null +++ b/changelog.d/large-identifiers.bugfix.rst @@ -0,0 +1,2 @@ +Bumping numeric prerelease and build identifiers now works when an identifier +exceeds Python's integer string conversion limit. diff --git a/src/semver/version.py b/src/semver/version.py index 7ead382e..c99542a6 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -293,6 +293,20 @@ def __iter__(self) -> VersionIterator: """Return iter(self).""" yield from self.to_tuple() + @staticmethod + def _increment_number(number: str) -> str: + """Increment an ASCII decimal string without a size limit.""" + if not number.isascii(): + return str(int(number) + 1) + + number = number.lstrip("0") + prefix = number.rstrip("9") + if prefix: + return prefix[:-1] + str(int(prefix[-1]) + 1) + "0" * ( + len(number) - len(prefix) + ) + return "1" + "0" * len(number) + @staticmethod def _increment_prerelease(string: str) -> str: """ @@ -304,7 +318,7 @@ def _increment_prerelease(string: str) -> str: """ match = Version._LAST_PRERELEASE.search(string) if match: - next_ = str(int(match.group(2)) + 1) + next_ = Version._increment_number(match.group(2)) string = match.group(1) + next_ if match.group(1) else next_ else: string += ".0" @@ -323,7 +337,7 @@ def _increment_string(string: str) -> str: """ match = Version._LAST_NUMBER.search(string) if match: - next_ = str(int(match.group(1)) + 1) + next_ = Version._increment_number(match.group(1)) start, end = match.span(1) string = string[: max(end - len(next_), start)] + next_ + string[end:] return string @@ -553,9 +567,9 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": rf"{re.escape(prerelease_token)}(\d+)", version.prerelease ) if match: - num = int(match.group(1)) + num = cls._increment_number(match.group(1)) return version.replace( - prerelease=f"{prerelease_token}{num + 1}", build=None + prerelease=f"{prerelease_token}{num}", build=None ) if not ( diff --git a/tests/test_bump_large_identifiers.py b/tests/test_bump_large_identifiers.py new file mode 100644 index 00000000..d2f0d3c3 --- /dev/null +++ b/tests/test_bump_large_identifiers.py @@ -0,0 +1,46 @@ +import pytest + +from semver import Version + + +@pytest.mark.parametrize( + "number,expected", + [ + ("1" * 5000, "1" * 4999 + "2"), + ("9" * 5000, "1" + "0" * 5000), + ("1" + "9" * 4999, "2" + "0" * 4999), + ], + ids=["no-carry", "new-digit", "carry"], +) +@pytest.mark.parametrize("part", ["prerelease", "build", "old-style-prerelease"]) +def test_bump_large_numeric_identifier(number, expected, part): + if part == "build": + original = "1.2.3-rc.1+build." + number + ".tail" + version = Version.parse(original) + result = version.bump_build() + expected_version = "1.2.3-rc.1+build." + expected + ".tail" + elif part == "old-style-prerelease": + original = "1.2.3-rc" + number + "+build.1" + version = Version.parse(original) + result = version.next_version("prerelease", "rc") + expected_version = "1.2.3-rc" + expected + else: + original = "1.2.3-rc." + number + "+build.1" + version = Version.parse(original) + result = version.bump_prerelease() + expected_version = "1.2.3-rc." + expected + + assert str(result) == expected_version + assert Version.is_valid(str(result)) + assert str(version) == original + + +def test_bump_preserves_constructor_unicode_digit_behavior(): + version = Version(1, 2, 3, prerelease="rc.\u0660\u0660\u0669") + assert version.bump_prerelease().prerelease == "rc.10" + + version = Version(1, 2, 3, build="build.\u0660\u0660\u0669") + assert version.bump_build().build == "build.\u066010" + + version = Version(1, 2, 3, prerelease="rc\u0660\u0660\u0669") + assert version.next_version("prerelease", "rc").prerelease == "rc10" From dd0eaaf9fbac56ab460a78ea0e8f5daec96c7871 Mon Sep 17 00:00:00 2001 From: Muneeb Sheikh Date: Sun, 11 Oct 2026 11:37:50 -0400 Subject: [PATCH 2/2] Reference the numeric identifier fix in the changelog --- changelog.d/{large-identifiers.bugfix.rst => pr494.bugfix.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{large-identifiers.bugfix.rst => pr494.bugfix.rst} (100%) diff --git a/changelog.d/large-identifiers.bugfix.rst b/changelog.d/pr494.bugfix.rst similarity index 100% rename from changelog.d/large-identifiers.bugfix.rst rename to changelog.d/pr494.bugfix.rst