Skip to content
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

Fix constraints duplication #69

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
119 changes: 43 additions & 76 deletions src/univers/gem.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,19 +199,14 @@ def __init__(self, version):
if not self.is_correct(version):
raise InvalidVersionError(version)

# If version is an empty string convert it to 0
version = str(version).strip()

self.original = version

# If version is an empty string convert it to 0
if not version:
version = "0"

self.version = version.replace("-", ".pre.")
self._segments = ()
self._canonical_segments = ()
self._bump = None
self._release = None

def __str__(self):
return self.original
Expand All @@ -225,10 +220,10 @@ def equal_strictly(self, other):
return self.version == other.version

def __hash__(self):
return hash(self.canonical_segments)
return hash(tuple(list(self.canonical_segments)))

def __eq__(self, other):
return self.canonical_segments == other.canonical_segments
return list(self.canonical_segments) == list(other.canonical_segments)

def __lt__(self, other):
return self.__cmp__(other) < 0
Expand All @@ -242,6 +237,26 @@ def __gt__(self, other):
def __ge__(self, other):
return self.__cmp__(other) >= 0

@property
keshav-space marked this conversation as resolved.
Show resolved Hide resolved
def segments(self):
"""
Yield segments for this version where segments are
ints or strings parsed from the original version string.
"""
find_segments = re.compile(r"[0-9]+|[a-z]+", re.IGNORECASE).findall
for seg in find_segments(self.version):
yield int(seg) if seg.isdigit() else seg

@property
def canonical_segments(self):
keshav-space marked this conversation as resolved.
Show resolved Hide resolved
"""
Yield "canonical segments" for this version using
the Rubygems way for canonicalization.
"""
for segments in self.split_segments():
segs = list(dropwhile(lambda s: s == 0, reversed(segments)))
yield from reversed(segs)

def bump(self):
"""
Return a new version object where the next to the last revision number
Expand All @@ -252,40 +267,33 @@ def bump(self):
>>> assert GemVersion("5.3.1").bump() == GemVersion("5.4"), repr(GemVersion("5.3.1").bump())
>>> assert GemVersion("5.3.1.4-2").bump() == GemVersion("5.3.2"), GemVersion("5.3.1.4-2").bump()
"""
if not self._bump:
segments = []
for seg in self.segments:
if isinstance(seg, str):
break
else:
segments.append(seg)

if len(segments) > 1:
segments.pop()
segments = []
for seg in self.segments:
if isinstance(seg, str):
break
else:
segments.append(seg)

segments[-1] += 1
segments = [str(r) for r in segments]
self._bump = GemVersion(".".join(segments))
if len(segments) > 1:
segments.pop()

return self._bump
segments[-1] += 1
segments = [str(r) for r in segments]
return GemVersion(".".join(segments))

def release(self):
"""
Return a new GemVersion which is the release for this version (e.g.,
1.2.0.a -> 1.2.0). Non-prerelease versions return themselves. A release
is composed only of numeric segments.
"""
if not self._release:
if self.prerelease():
segments = self.segments
while any(isinstance(s, str) for s in segments):
segments.pop()
segments = (str(s) for s in segments)
self._release = GemVersion(".".join(segments))
else:
self._release = self

return self._release
if self.prerelease():
segments = list(self.segments)
while any(isinstance(s, str) for s in segments):
segments.pop()
segments = (str(s) for s in segments)
return GemVersion(".".join(segments))
return self

def prerelease(self):
"""
Expand All @@ -294,47 +302,6 @@ def prerelease(self):
"""
return any(not str(s).isdigit() for s in self.segments)

@property
def segments(self):
"""
Return a new sequence of segments for this version where segments are
ints or strings parsed from the original version string.
"""
if not self._segments:
self._segments = self.get_segments()
return list(self._segments)

def get_segments(self):
"""
Return a sequence of segments for this version where segments are ints
or strings parsed from the original version string.
"""
find_segments = re.compile(r"[0-9]+|[a-z]+", re.IGNORECASE).findall
segments = []
for seg in find_segments(self.version):
if seg.isdigit():
seg = int(seg)
segments.append(seg)
return tuple(segments)

@property
def canonical_segments(self):
if not self._canonical_segments:
self._canonical_segments = self.get_canonical_segments()
return list(self._canonical_segments)

def get_canonical_segments(self):
"""
Return a new sequence of "canonical segments" for this version using
the Rubygems way.
"""
canonical_segments = []
for segments in self.split_segments():
segs = list(dropwhile(lambda s: s == 0, reversed(segments)))
segs = reversed(segs)
canonical_segments.extend(segs)
return tuple(canonical_segments)

def split_segments(self):
"""
Return a two-tuple of segments:
Expand Down Expand Up @@ -380,11 +347,11 @@ def __cmp__(self, other, trace=False):
if self.version == other.version:
return 0

lhsegments = self.canonical_segments
lhsegments = list(self.canonical_segments)
if trace:
print(f" lhsegments: canonical_segments: {lhsegments!r}")

rhsegments = other.canonical_segments
rhsegments = list(other.canonical_segments)
if trace:
print(f" rhsegments: canonical_segments: {rhsegments!r}")

Expand Down
8 changes: 8 additions & 0 deletions src/univers/nuget.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ def __lt__(self, other):
# Revision is the same, so ignore it for comparison purposes.
return self._base_semver < other._base_semver

def __hash__(self):
keshav-space marked this conversation as resolved.
Show resolved Hide resolved
return hash(
(
self._base_semver.to_tuple(),
keshav-space marked this conversation as resolved.
Show resolved Hide resolved
self._revision,
)
)

@classmethod
def from_string(cls, str_version):
if not str_version:
Expand Down
14 changes: 14 additions & 0 deletions src/univers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#
# Visit https://aboutcode.org and https://github.com/nexB/univers for support and download.

import semantic_version


def remove_spaces(string):
return "".join(string.split())
Expand All @@ -27,3 +29,15 @@ def cmp(x, y):
else:
# note that this is the minimal replacement function
return (x > y) - (x < y)


class SortableSemverVersion(semantic_version.Version):
"""
TODO: This is temporary workaround for unstable sort
Revert this and associated changes once the upstream is fixed.
https://github.com/rbarrois/python-semanticversion/issues/132
"""

@property
def precedence_key(self):
return super().precedence_key + (self.build,)
2 changes: 1 addition & 1 deletion src/univers/version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class VersionRange:
constraints = attr.ib(type=tuple, default=attr.Factory(tuple))

def __attrs_post_init__(self, *args, **kwargs):
constraints = tuple(sorted(self.constraints))
constraints = tuple(sorted(set(self.constraints)))
# Notes: setattr is used because this is an immutable frozen instance.
# See https://www.attrs.org/en/stable/init.html?#post-init
object.__setattr__(self, "constraints", constraints)
Expand Down
10 changes: 5 additions & 5 deletions src/univers/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import functools

import attr
import semantic_version
from univers.utils import SortableSemverVersion
from packaging import version as packaging_version

from univers import arch
Expand Down Expand Up @@ -209,7 +209,7 @@ class SemverVersion(Version):

@classmethod
def build_value(cls, string):
return semantic_version.Version.coerce(string)
return SortableSemverVersion.coerce(string)

@classmethod
def is_valid(cls, string):
Expand Down Expand Up @@ -431,14 +431,14 @@ def __gt__(self, other):
class ComposerVersion(SemverVersion):
@classmethod
def build_value(cls, string):
return semantic_version.Version.coerce(string.lstrip("vV"))
return SortableSemverVersion.coerce(string.lstrip("vV"))


@attr.s(frozen=True, order=False, eq=False, hash=True)
class GolangVersion(SemverVersion):
@classmethod
def build_value(cls, string):
return semantic_version.Version.coerce(string.lstrip("vV"))
return SortableSemverVersion.coerce(string.lstrip("vV"))


@attr.s(frozen=True, order=False, eq=False, hash=True)
Expand Down Expand Up @@ -605,7 +605,7 @@ def is_valid_new(cls, string):
True
"""
if SemverVersion.is_valid(string):
sem = semantic_version.Version.coerce(string)
sem = SortableSemverVersion.coerce(string)
return sem.major >= 3

@classmethod
Expand Down
11 changes: 11 additions & 0 deletions tests/test_nuget.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import unittest
from univers.versions import NugetVersion
from univers.nuget import Version


class NuGetTest(unittest.TestCase):
Expand Down Expand Up @@ -76,3 +77,13 @@ def test_less(self):
self.check_order(self.assertLess, "1.0.0-pre", "1.0.0.1-alpha")
self.check_order(self.assertLess, "1.0.0", "1.0.0.1-alpha")
self.check_order(self.assertLess, "0.9.9.1", "1.0.0")

def test_NugetVersion_hash(self):
vers1 = NugetVersion("1.0.1+23")
vers2 = NugetVersion("1.0.1+23")
assert hash(vers1) == hash(vers2)

def test_nuget_semver_hash(self):
vers1 = Version.from_string("51.0.0+2")
vers2 = Version.from_string("51.0.0+2")
assert hash(vers1) == hash(vers2)
16 changes: 16 additions & 0 deletions tests/test_python_semver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#
# Copyright (c) nexB Inc. and others.
# SPDX-License-Identifier: Apache-2.0
#
# Visit https://aboutcode.org and https://github.com/nexB/univers for support and download.

from unittest import TestCase
import semver


class TestPythonSemver(TestCase):
def test_semver_hash(self):
# python-semver doesn't consider build while hashing
vers1 = semver.VersionInfo.parse("1.2.3")
vers2 = semver.VersionInfo.parse("1.2.3+1")
assert hash(vers1) == hash(vers2)
12 changes: 6 additions & 6 deletions tests/test_rubygems_gem_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,22 +168,22 @@ def test_semver():

def test_segments():
# modifying the segments of a version should not affect the segments of the cached version object
ver = GemVersion("9.8.7")
secondseg = ver.segments[2]
ver_segment = list(GemVersion("9.8.7").segments)
secondseg = ver_segment[2]
secondseg += 1

refute_version_eql("9.8.8", "9.8.7")
assert GemVersion("9.8.7").segments == [9, 8, 7]
assert list(GemVersion("9.8.7").segments) == [9, 8, 7]


def test_split_segments():
assert GemVersion("3.2.4-2").split_segments() == ([3, 2, 4], ["pre", 2])


def test_canonical_segments():
assert GemVersion("1.0.0").canonical_segments == [1]
assert GemVersion("1.0.0.a.1.0").canonical_segments == [1, "a", 1]
assert GemVersion("1.2.3-1").canonical_segments == [1, 2, 3, "pre", 1]
assert list(GemVersion("1.0.0").canonical_segments) == [1]
assert list(GemVersion("1.0.0.a.1.0").canonical_segments) == [1, "a", 1]
assert list(GemVersion("1.2.3-1").canonical_segments) == [1, 2, 3, "pre", 1]


def test_frozen_version():
Expand Down
19 changes: 19 additions & 0 deletions tests/test_version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from univers.version_range import RANGE_CLASS_BY_SCHEMES
from univers.version_range import NpmVersionRange
from univers.version_range import OpensslVersionRange
from univers.version_range import NginxVersionRange
from univers.versions import PypiVersion
from univers.versions import NugetVersion
from univers.versions import RubygemsVersion
Expand Down Expand Up @@ -278,6 +279,24 @@ def test_nuget_version_range(self):
assert version_range == expected
assert version_range.to_string() == "vers:nuget/>=1.0.0|<2.0.0"

def test_version_range_constraint_duplication(self):
version_range = VersionRange(
constraints=(
VersionConstraint(comparator=">=", version=SemverVersion(string="1.4.0")),
VersionConstraint(comparator=">=", version=SemverVersion(string="1.4.0")),
VersionConstraint(comparator="=", version=SemverVersion(string="2.5.0")),
VersionConstraint(comparator="=", version=SemverVersion(string="2.5.0")),
)
)

expected = VersionRange(
constraints=(
VersionConstraint(comparator=">=", version=SemverVersion(string="1.4.0")),
VersionConstraint(comparator="=", version=SemverVersion(string="2.5.0")),
)
)
assert version_range == expected


VERSION_RANGE_TESTS_BY_SCHEME = {
"nginx": ["0.8.40+", "0.7.52-0.8.39", "0.9.10", "1.5.0+, 1.4.1+"],
Expand Down