Skip to content

Added support for the from_native function in both Composer and Golang version ranges #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
135 changes: 132 additions & 3 deletions src/univers/version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,8 +842,11 @@ class NugetVersionRange(MavenVersionRange):


class ComposerVersionRange(VersionRange):
# TODO composer may need its own scheme see https//github.com/aboutcode-org/univers/issues/5
# and https//getcomposer.org/doc/articles/versions.md
"""
Composer version range as documented at
https://getcomposer.org/doc/articles/versions.md
"""

scheme = "composer"
version_class = versions.ComposerVersion

Expand All @@ -856,6 +859,99 @@ class ComposerVersionRange(VersionRange):
"=": "=", # This is not a native composer-semver comparator, but is used in the gitlab version range for composer packages.
}

@classmethod
def from_native(cls, string):
"""
Parse a Composer version range string into a version range object.
"""
string = string.strip()
string = string.replace("|", "||")
string = string.replace(".x", ".*")

if "-" in string:
start, end = map(str.strip, string.split("-"))
start_parts = (start + ".0.0").split(".")[:3]
end_parts = (end + ".0.0").split(".")[:3]

if len(end.split(".")) < 3:
major = int(end_parts[0])
minor = int(end_parts[1])
upper_constraint = VersionConstraint(
comparator="<", version=cls.version_class(f"{major}.{minor + 1}.0")
)
else:
upper_constraint = VersionConstraint(
comparator="<=", version=cls.version_class(".".join(end_parts))
)

lower_constraint = VersionConstraint(
comparator=">=", version=cls.version_class(".".join(start_parts))
)

return cls(constraints=[lower_constraint, upper_constraint])

if string.startswith("^"):
base_version = string[1:]
base_version_obj = cls.version_class(base_version)
base_parts = base_version.split(".")
if base_parts[0] == "0":
upper_constraint = VersionConstraint(
comparator="<", version=cls.version_class(f"0.{int(base_parts[1]) + 1}.0")
)
else:
upper_constraint = VersionConstraint(
comparator="<", version=cls.version_class(f"{int(base_parts[0]) + 1}.0.0")
)
lower_constraint = VersionConstraint(comparator=">=", version=base_version_obj)
return cls(constraints=[lower_constraint, upper_constraint])

if string.startswith("~"):
base_version = string[1:]
base_version_obj = cls.version_class(base_version)
base_parts = base_version.split(".")

if len(base_parts) == 3:
upper_constraint = VersionConstraint(
comparator="<",
version=cls.version_class(f"{base_parts[0]}.{int(base_parts[1]) + 1}.0"),
)
else:
upper_constraint = VersionConstraint(
comparator="<", version=cls.version_class(f"{int(base_parts[0]) + 1}.0.0")
)

lower_constraint = VersionConstraint(comparator=">=", version=base_version_obj)
return cls(constraints=[lower_constraint, upper_constraint])

if ".*" in string:
base_version = string.replace(".*", ".0")
base_version_obj = cls.version_class(base_version)
base_parts = base_version.split(".")
upper_constraint = VersionConstraint(
comparator="<",
version=cls.version_class(f"{base_parts[0]}.{int(base_parts[1]) + 1}.0"),
)
lower_constraint = VersionConstraint(comparator=">=", version=base_version_obj)
return cls(constraints=[lower_constraint, upper_constraint])

constraints = []

segments = string.split("||")

for segment in segments:
if not any(op in string for op in cls.vers_by_native_comparators):
segment = "==" + segment
specifiers = SpecifierSet(segment)
for spec in specifiers:
operator = spec.operator
version = spec.version
version = cls.version_class(version)
comparator = cls.vers_by_native_comparators.get(operator, "=")
constraint = VersionConstraint(comparator=comparator, version=version)
constraints.append(constraint)

return cls(constraints=constraints)


class RpmVersionRange(VersionRange):
# http://ftp.rpm.org/api/4.4.2.2/dependencies.html
Expand Down Expand Up @@ -942,7 +1038,7 @@ def from_natives(cls, strings):

class GolangVersionRange(VersionRange):
"""
Go modules use strict semver with pseudo numbering for Git repos
Go modules use strict semver with pseudo numbering for Git commits.
https://go.dev/doc/modules/version-numbers
"""

Expand All @@ -958,6 +1054,39 @@ class GolangVersionRange(VersionRange):
"=": "=", # This is not a native golang-semver comparator, but is used in the gitlab version range for go packages.
}

@classmethod
def from_native(cls, string):
"""
Parse a native GoLang version range into a set of constraints.
"""
constraints = []

segments = string.split("||")
for segment in segments:

if not any(op in string for op in cls.vers_by_native_comparators):
segment = "==" + segment

specifiers = SpecifierSet(segment)
for spec in specifiers:
operator = spec.operator
version = spec.version
version = cls.version_class(version)
comparator = cls.vers_by_native_comparators.get(operator, "=")
constraint = VersionConstraint(comparator=comparator, version=version)
constraints.append(constraint)

return cls(constraints=constraints)

@classmethod
def from_natives(cls, strings):
if isinstance(strings, str):
return cls.from_native(strings)
constraints = []
for rel in strings:
constraints.extend(cls.from_native(rel).constraints)
return cls(constraints=constraints)


class GenericVersionRange(VersionRange):
scheme = "generic"
Expand Down
152 changes: 152 additions & 0 deletions tests/test_composer_version_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Copyright (c) nexB Inc. and others.
# SPDX-License-Identifier: Apache-2.0
#
# Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download.

from univers.version_constraint import VersionConstraint
from univers.version_range import ComposerVersionRange
from univers.versions import ComposerVersion


def test_composer_exact_version():
version_range = ComposerVersionRange.from_native("1.3.2")
assert version_range == ComposerVersionRange(
constraints=(VersionConstraint(comparator="=", version=ComposerVersion(string="1.3.2")),)
)


def test_composer_greater_than_or_equal():
version_range = ComposerVersionRange.from_native(">=1.3.2")
assert version_range == ComposerVersionRange(
constraints=(VersionConstraint(comparator=">=", version=ComposerVersion(string="1.3.2")),)
)


def test_composer_less_than():
version_range = ComposerVersionRange.from_native("<1.3.2")
assert version_range == ComposerVersionRange(
constraints=(VersionConstraint(comparator="<", version=ComposerVersion(string="1.3.2")),)
)


def test_composer_wildcard():
version_range = ComposerVersionRange.from_native("1.3.*")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="1.3.0")),
VersionConstraint(comparator="<", version=ComposerVersion(string="1.4.0")),
)
)


def test_composer_tilde_patch():
version_range = ComposerVersionRange.from_native("~1.3.2")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="1.3.2")),
VersionConstraint(comparator="<", version=ComposerVersion(string="1.4.0")),
)
)


def test_composer_tilde_minor():
version_range = ComposerVersionRange.from_native("~1.3")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="1.3.0")),
VersionConstraint(comparator="<", version=ComposerVersion(string="2.0.0")),
)
)


def test_composer_caret_patch():
version_range = ComposerVersionRange.from_native("^1.3.2")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="1.3.2")),
VersionConstraint(comparator="<", version=ComposerVersion(string="2.0.0")),
)
)


def test_composer_caret_zero_minor():
version_range = ComposerVersionRange.from_native("^0.3.2")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="0.3.2")),
VersionConstraint(comparator="<", version=ComposerVersion(string="0.4.0")),
)
)


def test_composer_range_with_multiple_constraints():
version_range = ComposerVersionRange.from_native(">=1.2.3, <2.0.0")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="1.2.3")),
VersionConstraint(comparator="<", version=ComposerVersion(string="2.0.0")),
)
)


def test_composer_range_with_or_constraints():
version_range = ComposerVersionRange.from_native(">=1.0.0 || <2.0.0")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="1.0.0")),
VersionConstraint(comparator="<", version=ComposerVersion(string="2.0.0")),
)
)


def test_composer_invalid_syntax():
try:
ComposerVersionRange.from_native(">1.0.0 <2.0.0")
assert False, "Should have raised a ValueError"
except ValueError:
assert True


def test_composer_range_str_representation():
version_range = ComposerVersionRange.from_native(">=1.0.0, <2.0.0")
assert str(version_range) == "vers:composer/>=1.0.0|<2.0.0"


def test_composer_legacy_pipe():
version_range = ComposerVersionRange.from_native(">=1.0.0 | <2.0.0")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="1.0.0")),
VersionConstraint(comparator="<", version=ComposerVersion(string="2.0.0")),
)
)


def test_composer_hyphen_partial_range():
version_range = ComposerVersionRange.from_native("1.0 - 2.0")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="1.0.0")),
VersionConstraint(comparator="<", version=ComposerVersion(string="2.1")),
)
)


def test_composer_hyphen_full_range():
version_range = ComposerVersionRange.from_native("1.0.0 - 2.1.0")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="1.0.0")),
VersionConstraint(comparator="<=", version=ComposerVersion(string="2.1.0")),
)
)


def test_composer_x_wildcard():
version_range = ComposerVersionRange.from_native("1.5.x")
assert version_range == ComposerVersionRange(
constraints=(
VersionConstraint(comparator=">=", version=ComposerVersion(string="1.5.0")),
VersionConstraint(comparator="<", version=ComposerVersion(string="1.6.0")),
)
)
Loading