Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
66 changes: 66 additions & 0 deletions src/univers/version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#
# Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download.

import re
from typing import List
from typing import Union

Expand Down Expand Up @@ -772,6 +773,71 @@ def from_native(cls, string):

return cls(constraints=constraints)

@classmethod
def from_ossa_native(cls, string):
"""
Returns a PypiVersionRange built from an OpenStack Security Advisory (OSSA) version constraint ``string``.

See: https://github.com/openstack/ossa

For example::

>>> str(PypiVersionRange.from_ossa_native("<=5.0.3, >=6.0.0 <=6.1.0 and ==7.0.0"))
'vers:pypi/<=5.0.3|>=6.0.0|<=6.1.0|7.0.0'

>>> str(PypiVersionRange.from_ossa_native("<=14.0.10, >=15.0.0 <=15.0.8, >=16.0.0 <=16.0.3"))
'vers:pypi/<=14.0.10|>=15.0.0|<=15.0.8|>=16.0.0|<=16.0.3'

>>> str(PypiVersionRange.from_ossa_native("<20.2.1, >=21.0.0 <21.2.1, ==22.0.0"))
'vers:pypi/<20.2.1|>=21.0.0|<21.2.1|22.0.0'
"""

# Normalize "and" keyword to comma
# "<=5.0.3, >=6.0.0 <=6.1.0 and ==7.0.0" -> "<=5.0.3, >=6.0.0 <=6.1.0, ==7.0.0"
string = string.replace(" and ", ",")

# Remove spaces around operators
# "<=5.0.3, >=6.0.0 <=6.1.0, ==7.0.0" -> "<=5.0.3,>=6.0.0<=6.1.0,==7.0.0"
string = re.sub(r"\s+([<>=!]+)", r"\1", string)
string = re.sub(r"([<>=!]+)\s+", r"\1", string)

# Insert comma between consecutive constraints
# "<=5.0.3,>=6.0.0<=6.1.0,==7.0.0" -> "<=5.0.3,>=6.0.0,<=6.1.0,==7.0.0"
string = re.sub(r"(\d)([<>=!])", r"\1,\2", string)

constraints = []
for part in string.split(","):

# Default to exact match for bare version numbers
# "1.16.0" -> "=1.16.0"
comparator = "="
version = part

for op, vers_op in cls.vers_by_native_comparators.items():
if part.startswith(op):
comparator = vers_op
version = part[len(op) :]
break

# Handle bare "=" for exact match
# "=18.0.0" -> "18.0.0"
if version.startswith("="):
version = version[1:]

try:
constraints.append(
VersionConstraint(
comparator=comparator,
version=cls.version_class(version),
)
)
except (ValueError, TypeError) as e:
raise InvalidVersionRange(
f"Invalid version constraint {part!r} in OSSA version string {string!r}: {e}"
) from e

return cls(constraints=constraints)


class MavenVersionRange(VersionRange):
"""
Expand Down
42 changes: 42 additions & 0 deletions tests/test_version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,48 @@ def test_PypiVersionRange_raises_ivr_for_unsupported_and_invalid_ranges(range, w
assert expected == str(PypiVersionRange.from_native(range))


@pytest.mark.parametrize(
"string, expected",
[
( # OSSA-2016-013
"<=5.0.3, >=6.0.0 <=6.1.0 and ==7.0.0",
"vers:pypi/<=5.0.3|>=6.0.0|<=6.1.0|7.0.0",
),
( # OSSA-2017-005
"<=14.0.10, >=15.0.0 <=15.0.8, >=16.0.0 <=16.0.3",
"vers:pypi/<=14.0.10|>=15.0.0|<=15.0.8|>=16.0.0|<=16.0.3",
),
( # OSSA-2019-003
"<17.0.12, >=18.0.0 <18.2.2, >=19.0.0 <19.0.2",
"vers:pypi/<17.0.12|>=18.0.0|<18.2.2|>=19.0.0|<19.0.2",
),
( # OSSA-2020-006
"<19.3.1, >=20.0.0 <20.3.1, ==21.0.0",
"vers:pypi/<19.3.1|>=20.0.0|<20.3.1|21.0.0",
),
( # OSSA-2026-001
">=10.5.0 <10.7.2, >=10.8.0 <10.9.1, >=10.10.0 <10.12.1",
"vers:pypi/>=10.5.0|<10.7.2|>=10.8.0|<10.9.1|>=10.10.0|<10.12.1",
),
( # OSSA-2021-001
"<16.3.3, >=17.0.0 <17.1.3, =18.0.0",
"vers:pypi/<16.3.3|>=17.0.0|<17.1.3|18.0.0",
),
( # empty string should raise InvalidVersionRange
"",
"InvalidVersionRange",
),
],
)
def test_PypiVersionRange_from_ossa_native(string, expected):
if expected == "InvalidVersionRange":
with pytest.raises(InvalidVersionRange):
PypiVersionRange.from_ossa_native(string)
else:
result = PypiVersionRange.from_ossa_native(string)
assert expected == str(result)


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"
Expand Down