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
38 changes: 36 additions & 2 deletions src/univers/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,18 +217,52 @@ def build_value(cls, string):
"""
Return a packaging.version.LegacyVersion or packaging.version.Version
"""
return packaging_version.Version(string)
return cls._coerce_pep440(string)

@classmethod
def is_valid(cls, string):
try:
# Note: we consider only modern pep440 versions as valid. legacy
# will fail validation for now.
cls.build_value(string)
cls._coerce_pep440(string)
return True
except packaging_version.InvalidVersion:
return False

@classmethod
def _coerce_pep440(cls, string):
"""
Return a packaging.version.Version, coercing a limited set of
legacy PyPI version forms that use '-' for a local version segment.
"""
try:
return packaging_version.Version(string)
except packaging_version.InvalidVersion:
normalized = cls._normalize_legacy_local(string)
if normalized:
return packaging_version.Version(normalized)
raise

@classmethod
def _normalize_legacy_local(cls, string):
"""
Normalize legacy local versions like "2.0.1rc2-git" to
PEP 440-compatible "2.0.1rc2+git" when safe.
"""
if not string or "+" in string or "-" not in string:
return None

base, local = string.rsplit("-", 1)
if not local or not local[0].isalpha():
return None

candidate = f"{base}+{local}"
try:
packaging_version.Version(candidate)
except packaging_version.InvalidVersion:
return None
return candidate


class EnhancedSemanticVersion(semantic_version.Version):
@property
Expand Down
4 changes: 4 additions & 0 deletions tests/test_pypi_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def test_constructor(self):
assert pypi_version.value == packaging_version.Version("2.4.5")
self.assertRaises(InvalidVersion, PypiVersion, "2.//////")

def test_constructor_accepts_legacy_local(self):
pypi_version = PypiVersion("2.0.1rc2-git")
assert str(pypi_version) == "2.0.1rc2+git"

def test_compare(self):
pypi_version = PypiVersion("2.4.5")
assert pypi_version == PypiVersion("2.4.5")
Expand Down
1 change: 1 addition & 0 deletions tests/test_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def test_pypi_version():
assert PypiVersion("1.2.3") != PypiVersion("1.2.4")
assert PypiVersion("1") == PypiVersion("1.0")
assert PypiVersion.is_valid("1.2.3")
assert PypiVersion.is_valid("2.0.1rc2-git")
assert not PypiVersion.is_valid("1.2.3a-1-a")
assert PypiVersion.normalize("v1.2.3") == "1.2.3"
assert PypiVersion("1").satisfies(
Expand Down