diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2c3a5163..417dc0df 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog ========= +Version v30.9.1 +---------------- + +- Add invert function to VersionRange. + + Version v30.9.0 ---------------- diff --git a/src/univers/version_constraint.py b/src/univers/version_constraint.py index 1b239b3a..b8f4c9b0 100644 --- a/src/univers/version_constraint.py +++ b/src/univers/version_constraint.py @@ -132,6 +132,33 @@ def __lt__(self, other): # we compare tuples, version first return (self.version, self.comparator).__lt__((other.version, other.comparator)) + def is_star(self): + return self.comparator == "*" + + def invert(self): + """ + Return a new VersionConstraint instance with the comparator inverted. + For example:: + >>> assert str(VersionConstraint(comparator=">=", version=Version("2.3")).invert()) == "<2.3" + """ + INVERTED_COMPARATORS = { + ">=": "<", + "<=": ">", + "!=": "=", + "<": ">=", + ">": "<=", + "=": "!=", + } + + if self.is_star(): + return None + + inverted_comparator = INVERTED_COMPARATORS[self.comparator] + return self.__class__( + comparator=inverted_comparator, + version=self.version, + ) + @classmethod def from_string(cls, string, version_class): """ diff --git a/src/univers/version_range.py b/src/univers/version_range.py index ce2fc962..6068b325 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -25,6 +25,16 @@ class InvalidVersionRange(Exception): """ +INVERTED_COMPARATORS = { + ">=": "<", + "<=": ">", + "!=": "=", + "<": ">=", + ">": "<=", + "=": "!=", +} + + @attr.s(frozen=True, order=False, eq=True, hash=True) class VersionRange: """ @@ -164,6 +174,27 @@ def from_versions(cls, sequence): constraints.append(constraint) return cls(constraints=constraints) + def is_star(self): + return len(self.constraints) == 1 and self.constraints[0].is_star() + + def invert(self): + """ + Return the inverse or complement of this VersionRange. For example, if this range is + ">=1.0.0", the inverse is "<1.0.0". + >>> str(VersionRange.from_string("vers:npm/>=1.0.0").invert()) + 'vers:npm/<1.0.0' + """ + inverted_constraints = [] + + if self.is_star(): + # The inverse of "*" is an empty range. + return None + + for constraint in self.constraints: + inverted_constraints.append(constraint.invert()) + + return self.__class__(constraints=inverted_constraints) + def __str__(self): constraints = "|".join(str(c) for c in sorted(self.constraints)) return f"vers:{self.scheme}/{constraints}" diff --git a/tests/test_version_constraint.py b/tests/test_version_constraint.py index ca4ecf60..b4b5df9b 100644 --- a/tests/test_version_constraint.py +++ b/tests/test_version_constraint.py @@ -60,3 +60,30 @@ def test_semver_comparison(version, spec, expected): version_class=versions.SemverVersion, ) assert (version in constraint) is expected + + +@pytest.mark.parametrize( + "original, inverted", + [ + (">2.7.1", "<=2.7.1"), + ("!=1.1.0", "=1.1.0"), + ("=2.0.0", "!=2.0.0"), + ("<=0.9999.9999", ">0.9999.9999"), + (">=0.2.9", "<0.2.9"), + ("<1.9999.9999", ">=1.9999.9999"), + ("*", None), + ], +) +def test_invert_opertaion(original, inverted): + constraint = VersionConstraint.from_string( + string=original, + version_class=versions.SemverVersion, + ) + if inverted: + inverted_constraint = VersionConstraint.from_string( + string=inverted, + version_class=versions.SemverVersion, + ) + assert constraint.invert() == inverted_constraint + else: + assert constraint.invert() is None diff --git a/tests/test_version_range.py b/tests/test_version_range.py index e2a02f37..a7ee2779 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -393,3 +393,23 @@ def test_npm_advisory_version_range_parse(test_case): string=test_case["npm_native"], ) assert str(result) == test_case["expected_vers"] + + +def test_invert(): + vers_with_equal_operator = VersionRange.from_string("vers:gem/1.0") + assert str(vers_with_equal_operator.invert()) == "vers:gem/!=1.0" + assert VersionRange.from_string("vers:gem/!=1.0").invert() == vers_with_equal_operator + + vers_with_less_than_operator = VersionRange.from_string("vers:gem/<1.0") + assert str(vers_with_less_than_operator.invert()) == "vers:gem/>=1.0" + assert VersionRange.from_string("vers:gem/>=1.0").invert() == vers_with_less_than_operator + + vers_with_greater_than_operator = VersionRange.from_string("vers:gem/>1.0") + assert str(vers_with_greater_than_operator.invert()) == "vers:gem/<=1.0" + assert VersionRange.from_string("vers:gem/<=1.0").invert() == vers_with_greater_than_operator + + vers_with_complex_constraints = VersionRange.from_string("vers:gem/<=1.0|>=3.0|<4.0|!=5.0") + assert str(vers_with_complex_constraints.invert()) == "vers:gem/>1.0|<3.0|>=4.0|5.0" + + vers_with_star_operator = VersionRange.from_string("vers:gem/*") + assert vers_with_star_operator.invert() == None