diff --git a/src/univers/version_range.py b/src/univers/version_range.py index 69f84f24..ba2eee42 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -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 @@ -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 @@ -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 """ @@ -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" diff --git a/tests/test_composer_version_range.py b/tests/test_composer_version_range.py new file mode 100644 index 00000000..208dd8fb --- /dev/null +++ b/tests/test_composer_version_range.py @@ -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")), + ) + ) diff --git a/tests/test_golang_version_range.py b/tests/test_golang_version_range.py new file mode 100644 index 00000000..53c0401c --- /dev/null +++ b/tests/test_golang_version_range.py @@ -0,0 +1,99 @@ +# 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 GolangVersionRange +from univers.versions import GolangVersion + + +def test_golang_exact_version(): + version_range = GolangVersionRange.from_native("v1.2.3") + assert version_range == GolangVersionRange( + constraints=(VersionConstraint(comparator="=", version=GolangVersion(string="v1.2.3")),) + ) + + +def test_golang_greater_than(): + version_range = GolangVersionRange.from_native(">v1.2.3") + assert version_range == GolangVersionRange( + constraints=(VersionConstraint(comparator=">", version=GolangVersion(string="v1.2.3")),) + ) + + +def test_golang_greater_than_or_equal(): + version_range = GolangVersionRange.from_native(">=v1.2.3") + assert version_range == GolangVersionRange( + constraints=(VersionConstraint(comparator=">=", version=GolangVersion(string="v1.2.3")),) + ) + + +def test_golang_less_than(): + version_range = GolangVersionRange.from_native("=v1.2.3, =v1.2.3, =1.2.3|<2.0.0" + + +def test_golang_version_range_with_pre_and_build(): + version_range = GolangVersionRange.from_native("v1.2.3-alpha+build123") + assert version_range == GolangVersionRange( + constraints=( + VersionConstraint( + comparator="=", version=GolangVersion(string="v1.2.3-alpha+build123") + ), + ) + ) + + +def test_golang_version_with_major_zero(): + version_range = GolangVersionRange.from_native("v0.1.5") + assert version_range == GolangVersionRange( + constraints=(VersionConstraint(comparator="=", version=GolangVersion(string="v0.1.5")),) + ) + + +def test_golang_version_with_only_major(): + version_range = GolangVersionRange.from_native("v1") + assert version_range == GolangVersionRange( + constraints=(VersionConstraint(comparator="=", version=GolangVersion(string="v1")),) + ) + + +def test_golang_version_with_upper_case(): + version_range = GolangVersionRange.from_native("V1.2.3") + assert version_range == GolangVersionRange( + constraints=(VersionConstraint(comparator="=", version=GolangVersion(string="v1.2.3")),) + )