Skip to content
Merged
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
2 changes: 2 additions & 0 deletions changelog.d/487.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Comparing versions with long numeric prerelease identifiers no longer raises
``ValueError`` because of Python's integer string conversion limit.
26 changes: 14 additions & 12 deletions src/semver/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,21 +185,23 @@ def _validate_identifiers(

@classmethod
def _nat_cmp(cls, a: Optional[str], b: Optional[str]) -> int:
def cmp_prerelease_tag(a, b):
if isinstance(a, int) and isinstance(b, int):
return _cmp(a, b)
elif isinstance(a, int):
return -1
elif isinstance(b, int):
return 1
else:
return _cmp(a, b)
def prerelease_key(identifier: str) -> tuple[int, int, str]:
if not identifier.isdigit():
return (1, 0, identifier)

if not identifier.isascii():
identifier = str(int(identifier))

# Compare decimal values without converting arbitrarily long
# ASCII identifiers to integers. Keep constructor leading zeros.
normalized = identifier.lstrip("0") or "0"
return (0, len(normalized), normalized)

a_parts = [int(x) if x.isdigit() else x for x in (a or "").split(".")]
b_parts = [int(x) if x.isdigit() else x for x in (b or "").split(".")]
a_parts = [prerelease_key(x) for x in (a or "").split(".")]
b_parts = [prerelease_key(x) for x in (b or "").split(".")]

for sub_a, sub_b in zip(a_parts, b_parts):
cmp_result = cmp_prerelease_tag(sub_a, sub_b)
cmp_result = _cmp(sub_a, sub_b)
if cmp_result != 0:
return cmp_result

Expand Down
80 changes: 80 additions & 0 deletions tests/test_compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,86 @@
from semver import Version, compare


def test_should_compare_long_numeric_prerelease():
value = "0.0.0-" + "1" * 5000
assert Version.is_valid(value)
version = Version.parse(value)
assert version.compare(value) == 0
assert compare(value, "0.0.0-2") == 1
assert compare("0.0.0-2", value) == -1


@pytest.mark.parametrize(
"lower,higher",
[
pytest.param("9" * 4999, "1" + "0" * 4999, id="digit-count"),
pytest.param(
"1" + "0" * 4998 + "1",
"1" + "0" * 4998 + "2",
id="equal-length",
),
pytest.param("1" * 5000, "alpha", id="numeric-before-text"),
pytest.param("alpha." + "1" * 5000, "alpha." + "1" * 5000 + ".1", id="prefix"),
],
)
def test_should_order_long_numeric_prereleases(lower, higher):
low = Version.parse("0.0.0-" + lower)
high = Version.parse("0.0.0-" + higher)
assert low.compare(high) == -1
assert high.compare(low) == 1
assert sorted([high, low]) == [low, high]


def test_should_ignore_build_and_preserve_release_order_with_long_prerelease():
version = Version.parse("0.0.0-" + "1" * 5000)
assert version == version.replace(build="other")
assert version < Version(0, 0, 0)


@pytest.mark.parametrize(
"left,right,expected",
[
("0001", "1", 0),
("000", "0", 0),
("02", "10", -1),
("01.2", "1.2", 0),
("2a", "10a", 1),
("01a", "1a", -1),
("-1", "0", 1),
("", None, 0),
("\u0661\u0662", "12", 0),
("\uff11\uff12", "13", -1),
("1\u0662", "12", 0),
("\u0660\u0660\u0661", "1", 0),
],
)
def test_should_preserve_constructor_prerelease_comparison(left, right, expected):
first = Version(0, 0, 0, prerelease=left)
second = Version(0, 0, 0, prerelease=right)
assert first.compare(second) == expected
assert second.compare(first) == -expected
assert first.prerelease == left
assert second.prerelease == right


@pytest.mark.parametrize("prerelease", ["01", "01.2", "\u0661\u0662", "\uff11\uff12"])
def test_should_reject_noncanonical_prerelease_when_parsing(prerelease):
with pytest.raises(ValueError):
Version.parse("0.0.0-" + prerelease)


@pytest.mark.parametrize(
"left,right", [("\u00b2", "2"), ("a.\u00b2", "b.0"), ("a.0", "b.\u00b2")]
)
def test_should_preserve_eager_invalid_unicode_digit_error(left, right):
with pytest.raises(ValueError):
Version(0, 0, 0, prerelease=left).compare(Version(0, 0, 0, prerelease=right))


def test_should_compare_core_before_invalid_prerelease():
assert Version(0, 0, 0, prerelease="\u00b2") < Version(1, 0, 0, prerelease="\u00b2")


@pytest.mark.parametrize(
"left,right",
[
Expand Down
Loading