Skip to content

Commit

Permalink
Merge pull request #106 from TG1999/fix_comparison
Browse files Browse the repository at this point in the history
Fix comparison in version subclasses
  • Loading branch information
TG1999 authored Mar 21, 2023
2 parents d38d9fd + 279c8cd commit 5d7aff1
Show file tree
Hide file tree
Showing 8 changed files with 345 additions and 146 deletions.
174 changes: 51 additions & 123 deletions src/univers/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
#
# Visit https://aboutcode.org and https://github.com/nexB/univers for support and download.

import functools
from functools import total_ordering

import attr
import semantic_version
from packaging import version as packaging_version
Expand Down Expand Up @@ -59,7 +56,7 @@ def is_valid_alpine_version(s):
return str(i) == left


@attr.s(frozen=True, order=False, hash=True)
@attr.s(frozen=True, order=True, eq=True, hash=True)
class Version:
"""
Base version mixin to subclass for each version syntax implementation.
Expand All @@ -73,14 +70,16 @@ class Version:
"""

# the original string used to build this Version
string = attr.ib(type=str)
string = attr.ib(type=str, eq=False, order=False, hash=False)

# the normalized string for this Version, stored without spaces and
# lowercased. Any leading v is removed too.
normalized_string = attr.ib(type=str, default=None, repr=False)
normalized_string = attr.ib(
type=str, default=None, repr=False, eq=False, order=False, hash=False
)

# a comparable scheme-specific version object constructed from the version string
value = attr.ib(default=None, repr=False)
value = attr.ib(default=None, repr=False, eq=True, order=True, hash=True)

def __attrs_post_init__(self):
normalized_string = self.normalize(self.string)
Expand Down Expand Up @@ -110,7 +109,7 @@ def normalize(cls, string):
Return a normalized version string from ``string ``. Subclass can override.
"""
# FIXME: Is removing spaces and strip v the right thing to do?
return remove_spaces(string).rstrip("v ").strip()
return remove_spaces(string).lstrip("vV")

@classmethod
def build_value(self, string):
Expand All @@ -133,33 +132,7 @@ def satisfies(self, constraint):
def __str__(self):
return str(self.value)

def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.value.__eq__(other.value)

def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.value.__lt__(other.value)

def __gt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.value.__gt__(other.value)

def __le__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.value.__le__(other.value)

def __ge__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.value.__ge__(other.value)


@attr.s(frozen=True, order=False, hash=True)
class GenericVersion(Version):
@classmethod
def is_valid(cls, string):
Expand All @@ -174,13 +147,13 @@ def is_valid(cls, string):
return super(GenericVersion, cls).is_valid(string)


@attr.s(frozen=True, order=False, eq=False, hash=True)
class PypiVersion(Version):
"""
Python PEP 440 version as implemented in packaging with fallback to "legacy"
"""

# TODO: ensure we deal with triple equal
# TODO: use packvers and handle legacy versions

@classmethod
def build_value(cls, string):
Expand All @@ -199,18 +172,22 @@ def is_valid(cls, string):
except packaging_version.InvalidVersion:
return False

return False

class EnhancedSemanticVersion(semantic_version.Version):
@property
def precedence_key(self):
key = super(EnhancedSemanticVersion, self).precedence_key
return key + (self.build or ())


@attr.s(frozen=True, order=False, eq=False, hash=True)
class SemverVersion(Version):
"""
Strict semver v2.0 with 3 segments.
"""

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

@classmethod
def is_valid(cls, string):
Expand Down Expand Up @@ -241,13 +218,13 @@ def build(self):
return self.value and self.value.build

def next_major(self):
return self.value and self.value.next_major()
return self.value and SemverVersion(str(self.value.next_major()))

def next_minor(self):
return self.value and self.value.next_minor()
return self.value and SemverVersion(str(self.value.next_minor()))

def next_patch(self):
return self.value and self.value.next_patch()
return self.value and SemverVersion(str(self.value.next_patch()))


def is_even(s):
Expand All @@ -265,7 +242,6 @@ def is_even(s):
return (int(s) % 2) == 0


@attr.s(frozen=True, order=False, eq=False, hash=True)
class NginxVersion(SemverVersion):
"""
Semver with 3 segments and extra attribute for stable vs. unstable branches
Expand All @@ -279,7 +255,6 @@ def is_stable(self):
return is_even(self.minor)


@attr.s(frozen=True, order=False, eq=False, hash=True)
class RubygemsVersion(Version):
"""
Rubygems encourages semver version but does not enforce it.
Expand All @@ -296,7 +271,6 @@ def is_valid(cls, string):
return gem.GemVersion.is_correct(string)


@attr.s(frozen=True, order=False, eq=False, hash=True)
class ArchLinuxVersion(Version):
def __eq__(self, other):
if not isinstance(other, self.__class__):
Expand Down Expand Up @@ -324,7 +298,6 @@ def __ge__(self, other):
return arch.vercmp(self.value, other.value) >= 0


@attr.s(frozen=True, order=False, eq=False, hash=True)
class DebianVersion(Version):
@classmethod
def build_value(cls, string):
Expand All @@ -335,7 +308,6 @@ def is_valid(cls, string):
return debian.Version.is_valid(string)


@attr.s(frozen=True, order=False, eq=False, hash=True)
class MavenVersion(Version):
# See https://maven.apache.org/enforcer/enforcer-rules/versionRanges.html
# https://github.com/apache/maven/tree/master/maven-artifact/src/main/java/org/apache/maven/artifact/versioning
Expand All @@ -344,10 +316,16 @@ class MavenVersion(Version):
def build_value(cls, string):
return maven.Version(string)

@classmethod
def is_valid(cls, string):
try:
cls.build_value(string)
return True
except ValueError:
return False


# We will use total ordering to sort the versions, since these versions also consider prereleases.
@attr.s(frozen=True, order=False, eq=False, hash=True)
@functools.total_ordering
class NugetVersion(Version):
# See https://docs.microsoft.com/en-us/nuget/concepts/package-versioning

Expand All @@ -363,33 +341,17 @@ def is_valid(cls, string):
except ValueError:
return False

def __str__(self):
return str(self.string)

def __lt__(self, other):
return nuget.Version.from_string(self.string) < nuget.Version.from_string(other.string)

def __eq__(self, other):
return nuget.Version.from_string(self.string) == nuget.Version.from_string(other.string)


@attr.s(frozen=True, order=False, eq=False, hash=True)
class RpmVersion(Version):
"""
Represent an RPM version.
For example::
# 1:1.1.4|>=2.8.16|<=2.8.16-z
"""

@classmethod
def build_value(cls, string):
return rpm.RpmVersion.from_string(string)


@total_ordering
@attr.s(frozen=True, order=False, eq=False, hash=True)
class GentooVersion(Version):
@classmethod
def is_valid(cls, string):
Expand All @@ -405,44 +367,30 @@ def __lt__(self, other):
return NotImplemented
return gentoo.vercmp(self.value, other.value) == -1


@attr.s(frozen=True, order=False, eq=False, hash=True)
class AlpineLinuxVersion(Version):
@classmethod
def is_valid(cls, string):
return is_valid_alpine_version(string) and gentoo.is_valid(string)

def __eq__(self, other):
def __gt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return gentoo.vercmp(self.value, other.value) == 0
return gentoo.vercmp(self.value, other.value) == 1

def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return gentoo.vercmp(self.value, other.value) < 0

def __gt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return gentoo.vercmp(self.value, other.value) > 0
class AlpineLinuxVersion(GentooVersion):
@classmethod
def is_valid(cls, string):
return is_valid_alpine_version(string) and gentoo.is_valid(string)


@attr.s(frozen=True, order=False, eq=False, hash=True)
class ComposerVersion(SemverVersion):
@classmethod
def build_value(cls, string):
return semantic_version.Version.coerce(string.lstrip("vV"))
return super().build_value(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 super().build_value(string.lstrip("vV"))


@attr.s(frozen=True, order=False, eq=False, hash=True)
class LegacyOpensslVersion(Version):
"""
Represent an Legacy Openssl Version.
Expand Down Expand Up @@ -558,7 +506,6 @@ def is_prerelease(self):
return self.patch.startswith(("-beta", "-alpha"))


@attr.s(frozen=True, order=False, eq=False, hash=True)
class OpensslVersion(Version):
"""
Internally tracks two types of openssl versions
Expand Down Expand Up @@ -653,8 +600,6 @@ def __ge__(self, other):
return isinstance(self.value, SemverVersion)


@attr.s(frozen=True, order=False, eq=False, hash=True)
@total_ordering
class ConanVersion(Version):
@classmethod
def build_value(cls, string):
Expand Down Expand Up @@ -740,38 +685,21 @@ def next_patch(self):
def bump(self, index):
return self.value and self.value.bump(index)

def __eq__(self, other):
if other is None:
return False
if not isinstance(other, ConanVersion):
other = ConanVersion.build_value(other)
return self.value == other
return self.value == other.value

def __lt__(self, other):
if other is None:
return False
if not isinstance(other, ConanVersion):
other = ConanVersion(str(other))
return self.value < other.value

def __le__(self, other):
if other is None:
return False
if not isinstance(other, ConanVersion):
other = ConanVersion(str(other))
return self.value <= other.value

def __gt__(self, other):
if other is None:
return False
if not isinstance(other, ConanVersion):
other = ConanVersion(str(other))
return self.value > other.value

def __ge__(self, other):
if other is None:
return False
if not isinstance(other, ConanVersion):
other = ConanVersion(str(other))
return self.value >= other.value
AVAILABLE_VERSIONS = [
SemverVersion,
GolangVersion,
PypiVersion,
GenericVersion,
ComposerVersion,
NginxVersion,
ArchLinuxVersion,
DebianVersion,
RpmVersion,
MavenVersion,
NugetVersion,
GentooVersion,
OpensslVersion,
LegacyOpensslVersion,
AlpineLinuxVersion,
]
2 changes: 1 addition & 1 deletion tests/data/conan_advisory.json
Original file line number Diff line number Diff line change
Expand Up @@ -2446,7 +2446,7 @@
"test_index": 350,
"scheme": "conan",
"native": ">=1.0.2 <=1.0.2v",
"expected_vers": "vers:conan/<=1.0.2|>=1.0.2",
"expected_vers": "vers:conan/>=1.0.2|<=1.0.2v",
"gitlab_scheme": "conan"
},
{
Expand Down
Loading

0 comments on commit 5d7aff1

Please sign in to comment.