diff --git a/src/univers/version_range.py b/src/univers/version_range.py index c62d5b21..4a885f0d 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -664,11 +664,6 @@ class PypiVersionRange(VersionRange): ">=": ">=", "<": "<", ">": ">", - # per https://www.python.org/dev/peps/pep-0440/#compatible-release - # For a given release identifier V.N, the compatible release clause is - # approximately equivalent to the pair of comparison clauses: - # >= V.N, == V.* - "~=": None, # 01.01.01 is NOT equal to 1.1.1 using === which is strict string # equality this is a rare and eventually non-suggested approach "===": None, @@ -681,7 +676,7 @@ def from_native(cls, string): Raise an a univers.versions.InvalidVersion """ # TODO: environment markers are yet supported - # TODO: handle .* version, ~= and === operators + # TODO: handle === operators if ";" in string: raise InvalidVersionRange(f"Unsupported PyPI environment marker: {string!r}") @@ -694,6 +689,8 @@ def from_native(cls, string): f"Unsupported character: {unsupported_chars!r} " f"in PyPI version: {string!r}" ) + string = cls.sanitize_constraints(string) + try: specifiers = SpecifierSet(string) except InvalidSpecifier as e: @@ -707,14 +704,10 @@ def from_native(cls, string): operator = spec.operator version = spec.version - if operator == "~=" or operator == "===": + if operator == "===": msg = f"Unsupported PyPI version constraint operator: {spec!r}" unsupported_messages.append(msg) - if str(version).endswith(".*"): - msg = f"Unsupported PyPI version: {spec!r}" - unsupported_messages.append(msg) - try: version = cls.version_class(version) comparator = cls.vers_by_native_comparators[operator] @@ -729,6 +722,35 @@ def from_native(cls, string): return cls(constraints=constraints) + @classmethod + def sanitize_constraints(cls, string): + constraints = [] + try: + specifiers = SpecifierSet(string) + except InvalidSpecifier as e: + raise InvalidVersionRange() from e + for spec in specifiers: + operator = spec.operator + version = spec.version + if "*" in version: + pos = version.find("*") + version = version[: pos - 1] + if "==" in operator: + constraints.extend( + [f">= {version}", f"< {version[:pos - 2]}{str(int(version[pos - 2]) + 1)}"] + ) + elif "!=" in operator: + constraints.extend( + [f"< {version}", f">= {version[:pos - 2]}{str(int(version[pos - 2]) + 1)}"] + ) + elif "~=" in operator: + parts = version.split(".") + parts[-2] = str(int(parts[-2]) + 1) + constraints.extend([f">= {version}", f"< {'.'.join(parts[:-1])}"]) + else: + constraints.append(f"{operator} {version}") + return ", ".join(constraints) + class MavenVersionRange(VersionRange): """ diff --git a/tests/test_version_range.py b/tests/test_version_range.py index c92de0b7..4be2fb12 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -357,13 +357,14 @@ def test_from_native_and_from_string_round_trip(scheme, native_ranges): @pytest.mark.parametrize( "range, will_pass, expected", [ - (" ~= 0.9", False, "Unsupported PyPI version constraint operator"), - ("~= 1.3", False, "Unsupported PyPI version constraint operator"), + (" ~= 0.9", True, "vers:pypi/>=0.9|<1"), + ("~= 1.3", True, "vers:pypi/>=1.3|<2"), (" >= 1.0", True, "vers:pypi/>=1.0"), - (" != 1.3.4.*", False, "Unsupported PyPI version"), + (" != 1.3.4.*", True, "vers:pypi/<1.3.4|>=1.3.5"), ("< 2.0", True, "vers:pypi/<2.0"), ("~= 1.3.4.*", False, ""), - ("==1. *", False, "Unsupported PyPI version"), + ("==1. *", True, "vers:pypi/>=1|<2"), + ("!=1.2.*", True, "vers:pypi/<1.2|>=1.3"), ("==1.3.4 ) (", False, "Unsupported character"), ("===1.0", False, "Unsupported PyPI version"), ], @@ -375,6 +376,7 @@ def test_PypiVersionRange_raises_ivr_for_unsupported_and_invalid_ranges(range, w raise Exception("Exception not raised") except InvalidVersionRange as ivre: assert expected in str(ivre) + else: assert expected == str(PypiVersionRange.from_native(range))