Skip to content

Commit baa1aaf

Browse files
committed
Fix comparison in version subclasses
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent d38d9fd commit baa1aaf

3 files changed

Lines changed: 160 additions & 88 deletions

File tree

src/univers/versions.py

Lines changed: 42 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
#
55
# Visit https://aboutcode.org and https://github.com/nexB/univers for support and download.
66

7-
import functools
8-
from functools import total_ordering
9-
107
import attr
118
import semantic_version
129
from packaging import version as packaging_version
@@ -59,7 +56,7 @@ def is_valid_alpine_version(s):
5956
return str(i) == left
6057

6158

62-
@attr.s(frozen=True, order=False, hash=True)
59+
@attr.s(frozen=True, order=True, eq=True, hash=True)
6360
class Version:
6461
"""
6562
Base version mixin to subclass for each version syntax implementation.
@@ -73,14 +70,16 @@ class Version:
7370
"""
7471

7572
# the original string used to build this Version
76-
string = attr.ib(type=str)
73+
string = attr.ib(type=str, eq=False, order=False, hash=False)
7774

7875
# the normalized string for this Version, stored without spaces and
7976
# lowercased. Any leading v is removed too.
80-
normalized_string = attr.ib(type=str, default=None, repr=False)
77+
normalized_string = attr.ib(
78+
type=str, default=None, repr=False, eq=False, order=False, hash=False
79+
)
8180

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

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

115114
@classmethod
116115
def build_value(self, string):
@@ -133,33 +132,7 @@ def satisfies(self, constraint):
133132
def __str__(self):
134133
return str(self.value)
135134

136-
def __eq__(self, other):
137-
if not isinstance(other, self.__class__):
138-
return NotImplemented
139-
return self.value.__eq__(other.value)
140-
141-
def __lt__(self, other):
142-
if not isinstance(other, self.__class__):
143-
return NotImplemented
144-
return self.value.__lt__(other.value)
145-
146-
def __gt__(self, other):
147-
if not isinstance(other, self.__class__):
148-
return NotImplemented
149-
return self.value.__gt__(other.value)
150135

151-
def __le__(self, other):
152-
if not isinstance(other, self.__class__):
153-
return NotImplemented
154-
return self.value.__le__(other.value)
155-
156-
def __ge__(self, other):
157-
if not isinstance(other, self.__class__):
158-
return NotImplemented
159-
return self.value.__ge__(other.value)
160-
161-
162-
@attr.s(frozen=True, order=False, hash=True)
163136
class GenericVersion(Version):
164137
@classmethod
165138
def is_valid(cls, string):
@@ -174,13 +147,13 @@ def is_valid(cls, string):
174147
return super(GenericVersion, cls).is_valid(string)
175148

176149

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

183155
# TODO: ensure we deal with triple equal
156+
# TODO: use packvers and handle legacy versions
184157

185158
@classmethod
186159
def build_value(cls, string):
@@ -199,18 +172,22 @@ def is_valid(cls, string):
199172
except packaging_version.InvalidVersion:
200173
return False
201174

202-
return False
175+
176+
class EnhancedSemanticVersion(semantic_version.Version):
177+
@property
178+
def precedence_key(self):
179+
key = super(EnhancedSemanticVersion, self).precedence_key
180+
return key + (self.build or ())
203181

204182

205-
@attr.s(frozen=True, order=False, eq=False, hash=True)
206183
class SemverVersion(Version):
207184
"""
208185
Strict semver v2.0 with 3 segments.
209186
"""
210187

211188
@classmethod
212189
def build_value(cls, string):
213-
return semantic_version.Version.coerce(string)
190+
return EnhancedSemanticVersion.coerce(string)
214191

215192
@classmethod
216193
def is_valid(cls, string):
@@ -265,7 +242,6 @@ def is_even(s):
265242
return (int(s) % 2) == 0
266243

267244

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

281257

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

298273

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

326300

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

337310

338-
@attr.s(frozen=True, order=False, eq=False, hash=True)
339311
class MavenVersion(Version):
340312
# See https://maven.apache.org/enforcer/enforcer-rules/versionRanges.html
341313
# https://github.com/apache/maven/tree/master/maven-artifact/src/main/java/org/apache/maven/artifact/versioning
@@ -346,8 +318,6 @@ def build_value(cls, string):
346318

347319

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

@@ -363,33 +333,17 @@ def is_valid(cls, string):
363333
except ValueError:
364334
return False
365335

366-
def __str__(self):
367-
return str(self.string)
368336

369-
def __lt__(self, other):
370-
return nuget.Version.from_string(self.string) < nuget.Version.from_string(other.string)
371-
372-
def __eq__(self, other):
373-
return nuget.Version.from_string(self.string) == nuget.Version.from_string(other.string)
374-
375-
376-
@attr.s(frozen=True, order=False, eq=False, hash=True)
377337
class RpmVersion(Version):
378338
"""
379339
Represent an RPM version.
380-
381-
For example::
382-
383-
# 1:1.1.4|>=2.8.16|<=2.8.16-z
384340
"""
385341

386342
@classmethod
387343
def build_value(cls, string):
388344
return rpm.RpmVersion.from_string(string)
389345

390346

391-
@total_ordering
392-
@attr.s(frozen=True, order=False, eq=False, hash=True)
393347
class GentooVersion(Version):
394348
@classmethod
395349
def is_valid(cls, string):
@@ -405,44 +359,30 @@ def __lt__(self, other):
405359
return NotImplemented
406360
return gentoo.vercmp(self.value, other.value) == -1
407361

408-
409-
@attr.s(frozen=True, order=False, eq=False, hash=True)
410-
class AlpineLinuxVersion(Version):
411-
@classmethod
412-
def is_valid(cls, string):
413-
return is_valid_alpine_version(string) and gentoo.is_valid(string)
414-
415-
def __eq__(self, other):
362+
def __gt__(self, other):
416363
if not isinstance(other, self.__class__):
417364
return NotImplemented
418-
return gentoo.vercmp(self.value, other.value) == 0
365+
return gentoo.vercmp(self.value, other.value) == 1
419366

420-
def __lt__(self, other):
421-
if not isinstance(other, self.__class__):
422-
return NotImplemented
423-
return gentoo.vercmp(self.value, other.value) < 0
424367

425-
def __gt__(self, other):
426-
if not isinstance(other, self.__class__):
427-
return NotImplemented
428-
return gentoo.vercmp(self.value, other.value) > 0
368+
class AlpineLinuxVersion(GentooVersion):
369+
@classmethod
370+
def is_valid(cls, string):
371+
return is_valid_alpine_version(string) and gentoo.is_valid(string)
429372

430373

431-
@attr.s(frozen=True, order=False, eq=False, hash=True)
432374
class ComposerVersion(SemverVersion):
433375
@classmethod
434376
def build_value(cls, string):
435-
return semantic_version.Version.coerce(string.lstrip("vV"))
377+
return super().build_value(string.lstrip("vV"))
436378

437379

438-
@attr.s(frozen=True, order=False, eq=False, hash=True)
439380
class GolangVersion(SemverVersion):
440381
@classmethod
441382
def build_value(cls, string):
442-
return semantic_version.Version.coerce(string.lstrip("vV"))
383+
return super().build_value(string.lstrip("vV"))
443384

444385

445-
@attr.s(frozen=True, order=False, eq=False, hash=True)
446386
class LegacyOpensslVersion(Version):
447387
"""
448388
Represent an Legacy Openssl Version.
@@ -558,7 +498,6 @@ def is_prerelease(self):
558498
return self.patch.startswith(("-beta", "-alpha"))
559499

560500

561-
@attr.s(frozen=True, order=False, eq=False, hash=True)
562501
class OpensslVersion(Version):
563502
"""
564503
Internally tracks two types of openssl versions
@@ -654,7 +593,6 @@ def __ge__(self, other):
654593

655594

656595
@attr.s(frozen=True, order=False, eq=False, hash=True)
657-
@total_ordering
658596
class ConanVersion(Version):
659597
@classmethod
660598
def build_value(cls, string):
@@ -775,3 +713,22 @@ def __ge__(self, other):
775713
if not isinstance(other, ConanVersion):
776714
other = ConanVersion(str(other))
777715
return self.value >= other.value
716+
717+
718+
AVAILABLE_VERSIONS = [
719+
SemverVersion,
720+
GolangVersion,
721+
PypiVersion,
722+
GenericVersion,
723+
ComposerVersion,
724+
NginxVersion,
725+
ArchLinuxVersion,
726+
DebianVersion,
727+
RpmVersion,
728+
MavenVersion,
729+
NugetVersion,
730+
GentooVersion,
731+
OpensslVersion,
732+
LegacyOpensslVersion,
733+
AlpineLinuxVersion,
734+
]

tests/test_pypi_version.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,25 @@
88

99
from packaging import version as packaging_version
1010

11-
from univers import versions
11+
from univers.versions import InvalidVersion
12+
from univers.versions import PypiVersion
1213

1314
# version comparison is already tested at:
1415
# https://github.com/pypa/packaging/blob/main/tests/test_version.py
1516

1617

1718
class TestPYPIVersion(TestCase):
1819
def test_constructor(self):
19-
pypi_version = versions.PypiVersion("2.4.5")
20+
pypi_version = PypiVersion("2.4.5")
2021
assert pypi_version.value == packaging_version.Version("2.4.5")
21-
self.assertRaises(versions.InvalidVersion, versions.PypiVersion, "2.//////")
22+
self.assertRaises(InvalidVersion, PypiVersion, "2.//////")
23+
24+
def test_compare(self):
25+
pypi_version = PypiVersion("2.4.5")
26+
assert pypi_version == PypiVersion("2.4.5")
27+
assert pypi_version != PypiVersion("2.4.6")
28+
assert pypi_version > PypiVersion("2.4.4")
29+
assert pypi_version >= PypiVersion("2.4.4")
30+
assert pypi_version < PypiVersion("2.4.6")
31+
assert pypi_version <= PypiVersion("2.4.6")
32+
assert PypiVersion("2.4") == PypiVersion("2.4.0")

0 commit comments

Comments
 (0)