Skip to content

Commit 9cd83e4

Browse files
committed
Accept legacy PyPI local versions like 2.0.1rc2-git
1 parent 8fc9f84 commit 9cd83e4

3 files changed

Lines changed: 41 additions & 2 deletions

File tree

src/univers/versions.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,18 +217,52 @@ def build_value(cls, string):
217217
"""
218218
Return a packaging.version.LegacyVersion or packaging.version.Version
219219
"""
220-
return packaging_version.Version(string)
220+
return cls._coerce_pep440(string)
221221

222222
@classmethod
223223
def is_valid(cls, string):
224224
try:
225225
# Note: we consider only modern pep440 versions as valid. legacy
226226
# will fail validation for now.
227-
cls.build_value(string)
227+
cls._coerce_pep440(string)
228228
return True
229229
except packaging_version.InvalidVersion:
230230
return False
231231

232+
@classmethod
233+
def _coerce_pep440(cls, string):
234+
"""
235+
Return a packaging.version.Version, coercing a limited set of
236+
legacy PyPI version forms that use '-' for a local version segment.
237+
"""
238+
try:
239+
return packaging_version.Version(string)
240+
except packaging_version.InvalidVersion:
241+
normalized = cls._normalize_legacy_local(string)
242+
if normalized:
243+
return packaging_version.Version(normalized)
244+
raise
245+
246+
@classmethod
247+
def _normalize_legacy_local(cls, string):
248+
"""
249+
Normalize legacy local versions like "2.0.1rc2-git" to
250+
PEP 440-compatible "2.0.1rc2+git" when safe.
251+
"""
252+
if not string or "+" in string or "-" not in string:
253+
return None
254+
255+
base, local = string.rsplit("-", 1)
256+
if not local or not local[0].isalpha():
257+
return None
258+
259+
candidate = f"{base}+{local}"
260+
try:
261+
packaging_version.Version(candidate)
262+
except packaging_version.InvalidVersion:
263+
return None
264+
return candidate
265+
232266

233267
class EnhancedSemanticVersion(semantic_version.Version):
234268
@property

tests/test_pypi_version.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ def test_constructor(self):
2121
assert pypi_version.value == packaging_version.Version("2.4.5")
2222
self.assertRaises(InvalidVersion, PypiVersion, "2.//////")
2323

24+
def test_constructor_accepts_legacy_local(self):
25+
pypi_version = PypiVersion("2.0.1rc2-git")
26+
assert str(pypi_version) == "2.0.1rc2+git"
27+
2428
def test_compare(self):
2529
pypi_version = PypiVersion("2.4.5")
2630
assert pypi_version == PypiVersion("2.4.5")

tests/test_versions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def test_pypi_version():
3939
assert PypiVersion("1.2.3") != PypiVersion("1.2.4")
4040
assert PypiVersion("1") == PypiVersion("1.0")
4141
assert PypiVersion.is_valid("1.2.3")
42+
assert PypiVersion.is_valid("2.0.1rc2-git")
4243
assert not PypiVersion.is_valid("1.2.3a-1-a")
4344
assert PypiVersion.normalize("v1.2.3") == "1.2.3"
4445
assert PypiVersion("1").satisfies(

0 commit comments

Comments
 (0)