Skip to content

Commit 5d0518c

Browse files
authored
Merge pull request #92 from TG1999/inverse
Add function to invert constraints and ranges #91
2 parents 9c5e47a + 637d362 commit 5d0518c

5 files changed

Lines changed: 111 additions & 0 deletions

File tree

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ Changelog
22
=========
33

44

5+
Version v30.9.1
6+
----------------
7+
8+
- Add invert function to VersionRange.
9+
10+
511
Version v30.9.0
612
----------------
713

src/univers/version_constraint.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,33 @@ def __lt__(self, other):
132132
# we compare tuples, version first
133133
return (self.version, self.comparator).__lt__((other.version, other.comparator))
134134

135+
def is_star(self):
136+
return self.comparator == "*"
137+
138+
def invert(self):
139+
"""
140+
Return a new VersionConstraint instance with the comparator inverted.
141+
For example::
142+
>>> assert str(VersionConstraint(comparator=">=", version=Version("2.3")).invert()) == "<2.3"
143+
"""
144+
INVERTED_COMPARATORS = {
145+
">=": "<",
146+
"<=": ">",
147+
"!=": "=",
148+
"<": ">=",
149+
">": "<=",
150+
"=": "!=",
151+
}
152+
153+
if self.is_star():
154+
return None
155+
156+
inverted_comparator = INVERTED_COMPARATORS[self.comparator]
157+
return self.__class__(
158+
comparator=inverted_comparator,
159+
version=self.version,
160+
)
161+
135162
@classmethod
136163
def from_string(cls, string, version_class):
137164
"""

src/univers/version_range.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ class InvalidVersionRange(Exception):
2525
"""
2626

2727

28+
INVERTED_COMPARATORS = {
29+
">=": "<",
30+
"<=": ">",
31+
"!=": "=",
32+
"<": ">=",
33+
">": "<=",
34+
"=": "!=",
35+
}
36+
37+
2838
@attr.s(frozen=True, order=False, eq=True, hash=True)
2939
class VersionRange:
3040
"""
@@ -164,6 +174,27 @@ def from_versions(cls, sequence):
164174
constraints.append(constraint)
165175
return cls(constraints=constraints)
166176

177+
def is_star(self):
178+
return len(self.constraints) == 1 and self.constraints[0].is_star()
179+
180+
def invert(self):
181+
"""
182+
Return the inverse or complement of this VersionRange. For example, if this range is
183+
">=1.0.0", the inverse is "<1.0.0".
184+
>>> str(VersionRange.from_string("vers:npm/>=1.0.0").invert())
185+
'vers:npm/<1.0.0'
186+
"""
187+
inverted_constraints = []
188+
189+
if self.is_star():
190+
# The inverse of "*" is an empty range.
191+
return None
192+
193+
for constraint in self.constraints:
194+
inverted_constraints.append(constraint.invert())
195+
196+
return self.__class__(constraints=inverted_constraints)
197+
167198
def __str__(self):
168199
constraints = "|".join(str(c) for c in sorted(self.constraints))
169200
return f"vers:{self.scheme}/{constraints}"

tests/test_version_constraint.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,30 @@ def test_semver_comparison(version, spec, expected):
6060
version_class=versions.SemverVersion,
6161
)
6262
assert (version in constraint) is expected
63+
64+
65+
@pytest.mark.parametrize(
66+
"original, inverted",
67+
[
68+
(">2.7.1", "<=2.7.1"),
69+
("!=1.1.0", "=1.1.0"),
70+
("=2.0.0", "!=2.0.0"),
71+
("<=0.9999.9999", ">0.9999.9999"),
72+
(">=0.2.9", "<0.2.9"),
73+
("<1.9999.9999", ">=1.9999.9999"),
74+
("*", None),
75+
],
76+
)
77+
def test_invert_opertaion(original, inverted):
78+
constraint = VersionConstraint.from_string(
79+
string=original,
80+
version_class=versions.SemverVersion,
81+
)
82+
if inverted:
83+
inverted_constraint = VersionConstraint.from_string(
84+
string=inverted,
85+
version_class=versions.SemverVersion,
86+
)
87+
assert constraint.invert() == inverted_constraint
88+
else:
89+
assert constraint.invert() is None

tests/test_version_range.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,23 @@ def test_npm_advisory_version_range_parse(test_case):
393393
string=test_case["npm_native"],
394394
)
395395
assert str(result) == test_case["expected_vers"]
396+
397+
398+
def test_invert():
399+
vers_with_equal_operator = VersionRange.from_string("vers:gem/1.0")
400+
assert str(vers_with_equal_operator.invert()) == "vers:gem/!=1.0"
401+
assert VersionRange.from_string("vers:gem/!=1.0").invert() == vers_with_equal_operator
402+
403+
vers_with_less_than_operator = VersionRange.from_string("vers:gem/<1.0")
404+
assert str(vers_with_less_than_operator.invert()) == "vers:gem/>=1.0"
405+
assert VersionRange.from_string("vers:gem/>=1.0").invert() == vers_with_less_than_operator
406+
407+
vers_with_greater_than_operator = VersionRange.from_string("vers:gem/>1.0")
408+
assert str(vers_with_greater_than_operator.invert()) == "vers:gem/<=1.0"
409+
assert VersionRange.from_string("vers:gem/<=1.0").invert() == vers_with_greater_than_operator
410+
411+
vers_with_complex_constraints = VersionRange.from_string("vers:gem/<=1.0|>=3.0|<4.0|!=5.0")
412+
assert str(vers_with_complex_constraints.invert()) == "vers:gem/>1.0|<3.0|>=4.0|5.0"
413+
414+
vers_with_star_operator = VersionRange.from_string("vers:gem/*")
415+
assert vers_with_star_operator.invert() == None

0 commit comments

Comments
 (0)