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