diff --git a/src/univers/version_range.py b/src/univers/version_range.py index c62d5b21..1368aca8 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -124,7 +124,8 @@ def from_string(cls, vers, simplify=False, validate=False): range_class = RANGE_CLASS_BY_SCHEMES.get(versioning_scheme) if not range_class: raise ValueError( - f"{vers!r} has an unknown versioning scheme: " f"{versioning_scheme!r}.", + f"{vers!r} has an unknown versioning scheme: " + f"{versioning_scheme!r}.", ) version_class = range_class.version_class @@ -294,6 +295,79 @@ def get_npm_version_constraints_from_semver_npm_spec(string, cls): return anyof_constraints +class PubVersionRange(VersionRange): + + scheme = "pub" + version_class = versions.DartVersion + + vers_by_native_comparators = { + "<=": "<=", + ">=": ">=", + "<": "<", + ">": ">", + "=": "=", + } + + @classmethod + def from_native(cls, string): + """ + Return a VersionRange built from an pub range ``string``. + + For example: + >>> result = PubVersionRange.from_native("1.5.10") + >>> assert str(result) == "vers:pub/1.5.10" + + >>> result = PubVersionRange.from_native(">=1.2.23 <=1.9.0") + >>> assert str(result) == "vers:pub/>=1.2.23|<=1.9.0" + + >>> result = PubVersionRange.from_native("^1.4.8") + >>> assert str(result) == "vers:pub/>=1.4.8|<2.0.0" + + """ + + constraints = [] + comparator = "" + + for constraint_item in string.split(): + # caret + if constraint_item.startswith("^"): + base_version = cls.version_class(constraint_item.lstrip("^")) + + if base_version.major > 0: + upper = cls.version_class(str(base_version.next_major())) + elif base_version.major == 0: + upper = cls.version_class(str(base_version.next_minor())) + + lower = base_version + + constraints.extend( + [ + VersionConstraint(comparator=">=", version=lower), + VersionConstraint(comparator="<", version=upper), + ] + ) + continue + + else: + # comparator, version = split_req( + # string = constraint_item, + # comparators=cls.vers_by_native_comparators, + # default="=" + # ) + + comparator, version = VersionConstraint.split(constraint_item) + + constraints.append( + VersionConstraint( + comparator=comparator, version=cls.version_class(version) + ) + ) + + comparator = "" + + return cls(constraints=constraints) + + class NpmVersionRange(VersionRange): scheme = "npm" version_class = versions.SemverVersion @@ -320,7 +394,9 @@ def from_native(cls, string): if string == "*": return cls( constraints=[ - VersionConstraint.from_string(string="*", version_class=cls.version_class) + VersionConstraint.from_string( + string="*", version_class=cls.version_class + ) ] ) @@ -335,7 +411,9 @@ def from_native(cls, string): for range in string.split("||"): if " - " in range: constraints.extend( - get_npm_version_constraints_from_semver_npm_spec(string=range, cls=cls) + get_npm_version_constraints_from_semver_npm_spec( + string=range, cls=cls + ) ) continue comparator = "" @@ -354,7 +432,9 @@ def from_native(cls, string): else: constraint = constraint.lstrip("vV") constraints.append( - VersionConstraint(comparator=comparator, version=vrc(constraint)) + VersionConstraint( + comparator=comparator, version=vrc(constraint) + ) ) else: # Handle caret range expression. @@ -684,14 +764,17 @@ def from_native(cls, string): # TODO: handle .* version, ~= and === operators if ";" in string: - raise InvalidVersionRange(f"Unsupported PyPI environment marker: {string!r}") + raise InvalidVersionRange( + f"Unsupported PyPI environment marker: {string!r}" + ) unsupported_chars = ";\\/|{}()`?'\"\t\n " string = "".join(string.split(" ")) if any(c in string for c in unsupported_chars): raise InvalidVersionRange( - f"Unsupported character: {unsupported_chars!r} " f"in PyPI version: {string!r}" + f"Unsupported character: {unsupported_chars!r} " + f"in PyPI version: {string!r}" ) try: @@ -758,7 +841,9 @@ def from_native(cls, string): if lower_bound == upper_bound: constraints.append( - VersionConstraint(comparator="=", version=cls.version_class(str(lower_bound))) + VersionConstraint( + comparator="=", version=cls.version_class(str(lower_bound)) + ) ) continue @@ -769,7 +854,8 @@ def from_native(cls, string): comparator = ">" constraints.append( VersionConstraint( - comparator=comparator, version=cls.version_class(str(lower_bound)) + comparator=comparator, + version=cls.version_class(str(lower_bound)), ) ) @@ -780,7 +866,8 @@ def from_native(cls, string): comparator = "<" constraints.append( VersionConstraint( - comparator=comparator, version=cls.version_class(str(upper_bound)) + comparator=comparator, + version=cls.version_class(str(upper_bound)), ) ) @@ -1063,7 +1150,9 @@ def from_native(cls, string): cleaned = remove_spaces(string).lower() if cleaned == "all": return cls( - constraints=[VersionConstraint(comparator="*", version_class=cls.version_class)] + constraints=[ + VersionConstraint(comparator="*", version_class=cls.version_class) + ] ) constraints = [] @@ -1174,7 +1263,9 @@ def from_gitlab_native(gitlab_scheme, string): continue if comparator: constraints.append( - VersionConstraint(comparator=comparator, version=vrc.version_class(constraint_item)) + VersionConstraint( + comparator=comparator, version=vrc.version_class(constraint_item) + ) ) else: comparator, version_constraint = split_req( @@ -1247,7 +1338,9 @@ def build_range_from_github_advisory_constraint(scheme: str, string: str): constraints = [] vrc = RANGE_CLASS_BY_SCHEMES[scheme] for constraint in constraint_strings: - constraints.append(build_constraint_from_github_advisory_string(scheme, constraint)) + constraints.append( + build_constraint_from_github_advisory_string(scheme, constraint) + ) return vrc(constraints=constraints) @@ -1273,6 +1366,7 @@ def build_range_from_github_advisory_constraint(scheme: str, string: str): "openssl": OpensslVersionRange, "mattermost": MattermostVersionRange, "conan": ConanVersionRange, + "pub": PubVersionRange, } PURL_TYPE_BY_GITLAB_SCHEME = { diff --git a/src/univers/versions.py b/src/univers/versions.py index 768a2c86..8adf4715 100644 --- a/src/univers/versions.py +++ b/src/univers/versions.py @@ -385,6 +385,12 @@ def build_value(cls, string): return super().build_value(string.lstrip("vV")) +class DartVersion(SemverVersion): + @classmethod + def build_value(cls, string): + return super().build_value(string) + + class GolangVersion(SemverVersion): @classmethod def build_value(cls, string): @@ -487,18 +493,22 @@ def __lt__(self, other): if not isinstance(other, self.__class__): return NotImplemented # Check if versions have the same base, and `one and only one` of them is a pre-release. - if (self.major, self.minor, self.build) == (other.major, other.minor, other.build) and ( - self.is_prerelease() != other.is_prerelease() - ): + if (self.major, self.minor, self.build) == ( + other.major, + other.minor, + other.build, + ) and (self.is_prerelease() != other.is_prerelease()): return self.is_prerelease() return self.value.__lt__(other.value) def __gt__(self, other): if not isinstance(other, self.__class__): return NotImplemented - if (self.major, self.minor, self.build) == (other.major, other.minor, other.build) and ( - self.is_prerelease() != other.is_prerelease() - ): + if (self.major, self.minor, self.build) == ( + other.major, + other.minor, + other.build, + ) and (self.is_prerelease() != other.is_prerelease()): return other.is_prerelease() return self.value.__gt__(other.value) @@ -702,4 +712,5 @@ def bump(self, index): OpensslVersion, LegacyOpensslVersion, AlpineLinuxVersion, + DartVersion, ] diff --git a/tests/test_pub_version.py b/tests/test_pub_version.py new file mode 100644 index 00000000..45da97a9 --- /dev/null +++ b/tests/test_pub_version.py @@ -0,0 +1,86 @@ +import pytest + +from univers.versions import DartVersion + + +def test_equal(): + assert DartVersion("1.2.3") == DartVersion("1.2.3") + assert DartVersion("1.4.5-dev") == DartVersion("1.4.5-dev") + + # test cases from https://github.com/dart-lang/pub_semver/blob/master/test/version_test.dart + assert DartVersion("01.2.3") == DartVersion("1.2.3") + assert DartVersion("1.02.3") == DartVersion("1.2.3") + assert DartVersion("1.2.03") == DartVersion("1.2.3") + # assert DartVersion("1.2.3-01") == DartVersion("1.2.3-1") + # assert DartVersion("1.2.3+01") == DartVersion("1.2.3+1") + + +def test_compare(): + versions = [ + "1.0.0-alpha", + "1.0.0-alpha.1", + "1.0.0-beta.2", + "1.0.0-beta.11", + "1.0.0-rc.1", + "1.0.0-rc.1+build.1", + "1.0.0", + "1.0.0+0.3.7", + "1.3.7+build", + #'1.3.7+build.2.b8f12d7', + "1.3.7+build.11.e0f985a", + "2.0.0", + "2.1.0", + "2.2.0", + "2.11.0", + "2.11.1", + ] + + for i in range(len(versions)): + for j in range(len(versions)): + a = DartVersion(versions[i]) + b = DartVersion(versions[j]) + assert (a < b) == (i < j) + assert (a <= b) == (i <= j) + assert (a > b) == (i > j) + assert (a >= b) == (i >= j) + assert (a == b) == (i == j) + assert (a != b) == (i != j) + + +def test_next_major(): + assert DartVersion(DartVersion("1.2.3").next_major().string) == DartVersion("2.0.0") + assert DartVersion(DartVersion("1.1.4").next_major().string) == DartVersion("2.0.0") + assert DartVersion(DartVersion("2.0.0").next_major().string) == DartVersion("3.0.0") + assert DartVersion(DartVersion("1.2.3-dev").next_major().string) == DartVersion( + "2.0.0" + ) + assert DartVersion(DartVersion("2.0.0-dev").next_major().string) == DartVersion( + "2.0.0" + ) + assert DartVersion(DartVersion("1.2.3+1").next_major().string) == DartVersion( + "2.0.0" + ) + + +def test_next_minor(): + assert DartVersion(DartVersion("1.2.3").next_minor().string) == DartVersion("1.3.0") + assert DartVersion(DartVersion("1.1.4").next_minor().string) == DartVersion("1.2.0") + assert DartVersion(DartVersion("1.3.0").next_minor().string) == DartVersion("1.4.0") + assert DartVersion(DartVersion("1.2.3-dev").next_minor().string) == DartVersion( + "1.3.0" + ) + # assert DartVersion(DartVersion("1.3.0-dev").next_minor().string) == DartVersion("1.4.0") + assert DartVersion(DartVersion("1.2.3+1").next_minor().string) == DartVersion( + "1.3.0" + ) + + +def test_next_patch(): + assert DartVersion(DartVersion("1.2.3").next_patch().string) == DartVersion("1.2.4") + assert DartVersion(DartVersion("2.0.0").next_patch().string) == DartVersion("2.0.1") + assert DartVersion(DartVersion("1.2.4-dev").next_patch().string) == DartVersion( + "1.2.4" + ) + assert DartVersion(DartVersion("1.2.3+2").next_patch().string) == DartVersion( + "1.2.4" + ) diff --git a/tests/test_pub_version.py.ABOUT b/tests/test_pub_version.py.ABOUT new file mode 100644 index 00000000..b953bd82 --- /dev/null +++ b/tests/test_pub_version.py.ABOUT @@ -0,0 +1,8 @@ +about_resource: test_pub_version.py +package_url: pkg:pub/pub_semver@2.1.1 +download_url: https://github.com/dart-lang/pub_semver/blob/master/test/version_test.dart +homepage_url: https://github.com/dart-lang/pub_semver +license_expression: BSD-3-Clause +notice_file: test_pub_version.py.NOTICE +copyright: Copyright (c) nexB Inc. and others. + \ No newline at end of file diff --git a/tests/test_pub_version.py.NOTICE b/tests/test_pub_version.py.NOTICE new file mode 100644 index 00000000..3e10522a --- /dev/null +++ b/tests/test_pub_version.py.NOTICE @@ -0,0 +1,27 @@ +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/tests/test_pub_version_range.py b/tests/test_pub_version_range.py new file mode 100644 index 00000000..b8a84e6d --- /dev/null +++ b/tests/test_pub_version_range.py @@ -0,0 +1,36 @@ +import pytest + +from univers.version_range import InvalidVersionRange +from univers.version_range import PubVersionRange +from univers.versions import DartVersion + +values = [ + # https://github.com/dart-lang/pub_semver/blob/master/test/version_range_test.dart + # version must be greater than min + [">1.2.3", [[]], ["1.3.3", "2.3.3"], ["1.2.2", "1.2.3"]], + # version must be min or greater if includeMin + [">=1.2.3", [[]], ["1.2.3", "1.3.3"], ["1.2.2"]], + # pre-release versions of inclusive min are excluded + # version must be less than max + ["<2.3.4", [[]], ["2.3.3"], ["2.3.4", "2.4.3"]], + # pre-release versions of non-pre-release max are excluded + # pre-release versions of non-pre-release max are included if min is a pre-release of the same version + # pre-release versions of pre-release max are included + # version must be max or less if includeMax + [">1.2.3 <=2.3.4", [[]], ["2.3.3", "2.3.4", "2.3.4-dev"], ["2.4.3"]], + # has no min if one was not set + ["<1.2.3", [[]], ["0.0.0"], ["1.2.3"]], + # has no max if one was not set + [">1.2.3", [[]], ["1.3.3", "999.3.3"], ["1.2.3"]], +] + + +@pytest.mark.parametrize("version_range, conditions, versions_in, versions_out", values) +def test_range(version_range, conditions, versions_in, versions_out): + r = PubVersionRange.from_native(version_range) + + for v in versions_in: + assert DartVersion(v) in r + + for v in versions_out: + assert DartVersion(v) not in r diff --git a/tests/test_pub_version_range.py.ABOUT b/tests/test_pub_version_range.py.ABOUT new file mode 100644 index 00000000..be82a415 --- /dev/null +++ b/tests/test_pub_version_range.py.ABOUT @@ -0,0 +1,8 @@ +about_resource: test_pub_version_range.py +package_url: pkg:pub/pub_semver@2.1.1 +download_url: https://github.com/dart-lang/pub_semver/blob/master/test/version_range_test.dart +homepage_url: https://github.com/dart-lang/pub_semver +license_expression: BSD-3-Clause +notice_file: test_pub_version_range.py.NOTICE +copyright: Copyright (c) nexB Inc. and others. + \ No newline at end of file diff --git a/tests/test_pub_version_range.py.NOTICE b/tests/test_pub_version_range.py.NOTICE new file mode 100644 index 00000000..3e10522a --- /dev/null +++ b/tests/test_pub_version_range.py.NOTICE @@ -0,0 +1,27 @@ +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file