From 6973227c2c84047a59a427f1d2d57340260f9371 Mon Sep 17 00:00:00 2001 From: machines-6657 <294783368+machines-6657@users.noreply.github.com> Date: Sun, 4 Oct 2026 07:19:49 +0800 Subject: [PATCH] Increment long numeric identifiers without integer conversion --- src/semver/version.py | 21 +++++++- tests/test_bump_long_identifiers.py | 75 +++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 tests/test_bump_long_identifiers.py diff --git a/src/semver/version.py b/src/semver/version.py index 6727446..80950b1 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -304,12 +304,29 @@ def _increment_prerelease(string: str) -> str: """ match = Version._LAST_PRERELEASE.search(string) if match: - next_ = str(int(match.group(2)) + 1) + next_ = Version._increment_digits(match.group(2)) string = match.group(1) + next_ if match.group(1) else next_ else: string += ".0" return string + @staticmethod + def _increment_digits(digits: str) -> str: + """Increment an ASCII decimal string without converting it to an int.""" + if not digits.isascii(): + # Preserve the historical handling of non-ASCII digits accepted + # by direct construction; parsed SemVer identifiers are ASCII. + return str(int(digits) + 1) + + # Match int() normalization; the build caller preserves padding. + result = list(digits.lstrip("0") or "0") + for index in range(len(result) - 1, -1, -1): + if result[index] != "9": + result[index] = chr(ord(result[index]) + 1) + return "".join(result) + result[index] = "0" + return "1" + "".join(result) + @staticmethod def _increment_string(string: str) -> str: """ @@ -323,7 +340,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_digits(match.group(1)) start, end = match.span(1) string = string[: max(end - len(next_), start)] + next_ + string[end:] return string diff --git a/tests/test_bump_long_identifiers.py b/tests/test_bump_long_identifiers.py new file mode 100644 index 0000000..b61f37e --- /dev/null +++ b/tests/test_bump_long_identifiers.py @@ -0,0 +1,75 @@ +import sys + +import pytest + +from semver import Version + + +def test_bump_prerelease_with_long_numeric_identifier(): + identifier = "9" * 5000 + version = f"1.2.3-rc.{identifier}" + + assert Version.is_valid(version) + assert str(Version.parse(version).bump_prerelease()) == f"1.2.3-rc.1{'0' * 5000}" + + +def test_bump_build_with_long_numeric_identifier(): + identifier = "9" * 5000 + version = f"1.2.3+build.{identifier}" + + assert Version.is_valid(version) + assert str(Version.parse(version).bump_build()) == f"1.2.3+build.1{'0' * 5000}" + + +def test_bump_build_preserves_leading_zeroes_when_carrying(): + version = Version.parse("1.2.3+build.0099") + + assert str(version.bump_build()) == "1.2.3+build.0100" + + +@pytest.mark.parametrize( + "prerelease, expected", + [("0009", "10"), ("rc.0099", "rc.100"), ("rc.0000", "rc.1")], +) +def test_bump_prerelease_normalizes_constructor_leading_zeroes(prerelease, expected): + version = Version(1, 2, 3, prerelease=prerelease) + + assert version.bump_prerelease().prerelease == expected + assert version.prerelease == prerelease + + +@pytest.mark.parametrize( + "part, identifier, expected", + [ + ("prerelease", "rc.\u0660\u0660\u0669\u0669", "rc.100"), + ("build", "build.\u0660\u0660\u0669\u0669", "build.\u0660100"), + ], +) +def test_bump_preserves_constructor_unicode_digit_behavior(part, identifier, expected): + version = Version(1, 2, 3, **{part: identifier}) + + assert getattr(getattr(version, f"bump_{part}")(), part) == expected + + +@pytest.mark.parametrize("part", ["prerelease", "build"]) +def test_bump_long_identifier_without_carry_does_not_change_int_limit(part): + identifier = "1" + "0" * 4999 + version = Version.parse(f"1.2.3{'-' if part == 'prerelease' else '+'}{identifier}") + original_limit = ( + sys.get_int_max_str_digits() if hasattr(sys, "get_int_max_str_digits") else None + ) + + bumped = getattr(version, f"bump_{part}")() + + assert getattr(bumped, part) == "1" + "0" * 4998 + "1" + assert Version.is_valid(str(bumped)) + assert getattr(version, part) == identifier + if original_limit is not None: + assert sys.get_int_max_str_digits() == original_limit + + +def test_bump_long_build_preserves_all_leading_zeroes(): + identifier = "0" * 5000 + version = Version.parse(f"1.2.3+{identifier}") + + assert version.bump_build().build == "0" * 4999 + "1"