Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ Changelog
=========


Version v30.9.1
----------------

- Add inverse function to VersionRange.


Version v30.9.0
----------------

Expand Down
13 changes: 11 additions & 2 deletions src/univers/version_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ def operator_star(a, b):
return True


def operator_null(a, b):
"""
Comparison operator for the star "^" constraint comparator. Since it does not matches
Comment thread
TG1999 marked this conversation as resolved.
Outdated
any version, it is always False.
"""
return False


# note: ORDER MATTER here: we tests startswith(key) for each key in sequence
COMPARATORS = {
">=": operator.ge,
Expand All @@ -48,6 +56,7 @@ def operator_star(a, b):
">": operator.gt,
"=": operator.eq,
"*": operator_star,
"^": operator_null,
Comment thread
TG1999 marked this conversation as resolved.
Outdated
}


Expand Down Expand Up @@ -150,10 +159,10 @@ def from_string(cls, string, version_class):
if comparator not in COMPARATORS:
raise ValueError(f"Unknown comparator: {comparator!r}")

if not version and comparator != "*":
if not version and comparator not in ["*", "^"]:
Comment thread
TG1999 marked this conversation as resolved.
Outdated
raise ValueError("Empty version")

if comparator == "*":
if comparator in ["*", "^"]:
version = None
else:
version = version_class(version)
Expand Down
39 changes: 39 additions & 0 deletions src/univers/version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ class InvalidVersionRange(Exception):
"""


INVERTED_COMPARATORS = {
Comment thread
TG1999 marked this conversation as resolved.
">=": "<",
"<=": ">",
"!=": "=",
"<": ">=",
">": "<=",
"=": "!=",
"*": "^",
"^": "*",
}


@attr.s(frozen=True, order=False, eq=True, hash=True)
class VersionRange:
"""
Expand Down Expand Up @@ -164,6 +176,33 @@ def from_versions(cls, sequence):
constraints.append(constraint)
return cls(constraints=constraints)

def inverse(self):
Comment thread
TG1999 marked this conversation as resolved.
Outdated
"""
Return the inverse of this VersionRange. For example, if this range is
Comment thread
TG1999 marked this conversation as resolved.
Outdated
">=1.0.0", the inverse is "<1.0.0".
>>> VersionRange.from_string("vers:npm/>=1.0.0").inverse()
NpmVersionRange(constraints=(VersionConstraint(comparator='<', version=SemverVersion(string='1.0.0')),))
"""
inverted_constraints = []
for constraint in self.constraints:
if constraint.comparator in INVERTED_COMPARATORS:
Comment thread
TG1999 marked this conversation as resolved.
Outdated
inverted_comparator = INVERTED_COMPARATORS[constraint.comparator]
else:
raise NotImplementedError(
Comment thread
TG1999 marked this conversation as resolved.
Outdated
f"Cannot invert a range with a {constraint.comparator!r} comparator."
)
if inverted_comparator == "*" or inverted_comparator == "^":
inverted_constraint = VersionConstraint.from_string(
string=inverted_comparator, version_class=self.version_class
)
else:
inverted_constraint = VersionConstraint(
comparator=inverted_comparator,
version=constraint.version,
)
inverted_constraints.append(inverted_constraint)
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}"
Expand Down
9 changes: 9 additions & 0 deletions tests/test_version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,12 @@ def test_npm_advisory_version_range_parse(test_case):
string=test_case["npm_native"],
)
assert str(result) == test_case["expected_vers"]


@pytest.mark.parametrize("test_case", json.load(open("./tests/data/npm_advisory.json")))
def test_npm_advisory_inverse_version_range_parse(test_case):
result = NpmVersionRange.from_native(
string=test_case["npm_native"],
).inverse()
result = result.inverse()
assert str(result) == test_case["expected_vers"]