Skip to content

Commit 2cf099a

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

3 files changed

Lines changed: 160 additions & 87 deletions

File tree

src/univers/versions.py

Lines changed: 42 additions & 84 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
@@ -58,7 +55,7 @@ def is_valid_alpine_version(s):
5855
return str(i) == left
5956

6057

61-
@attr.s(frozen=True, order=False, hash=True)
58+
@attr.s(frozen=True, order=True, eq=True, hash=True)
6259
class Version:
6360
"""
6461
Base version mixin to subclass for each version syntax implementation.
@@ -72,14 +69,16 @@ class Version:
7269
"""
7370

7471
# the original string used to build this Version
75-
string = attr.ib(type=str)
72+
string = attr.ib(type=str, eq=False, order=False, hash=False)
7673

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

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

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

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

135-
def __eq__(self, other):
136-
if not isinstance(other, self.__class__):
137-
return NotImplemented
138-
return self.value.__eq__(other.value)
139-
140-
def __lt__(self, other):
141-
if not isinstance(other, self.__class__):
142-
return NotImplemented
143-
return self.value.__lt__(other.value)
144134

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

175148

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

182154
# TODO: ensure we deal with triple equal
155+
# TODO: use packvers and handle legacy versions
183156

184157
@classmethod
185158
def build_value(cls, string):
@@ -198,18 +171,22 @@ def is_valid(cls, string):
198171
except packaging_version.InvalidVersion:
199172
return False
200173

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

203181

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

210187
@classmethod
211188
def build_value(cls, string):
212-
return semantic_version.Version.coerce(string)
189+
return EnhancedSemanticVersion.coerce(string)
213190

214191
@classmethod
215192
def is_valid(cls, string):
@@ -264,7 +241,6 @@ def is_even(s):
264241
return (int(s) % 2) == 0
265242

266243

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

280256

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

297272

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

325299

326-
@attr.s(frozen=True, order=False, eq=False, hash=True)
327300
class DebianVersion(Version):
328301
@classmethod
329302
def build_value(cls, string):
@@ -334,7 +307,6 @@ def is_valid(cls, string):
334307
return debian.Version.is_valid(string)
335308

336309

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

346318

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

@@ -362,33 +332,17 @@ def is_valid(cls, string):
362332
except ValueError:
363333
return False
364334

365-
def __str__(self):
366-
return str(self.string)
367-
368-
def __lt__(self, other):
369-
return nuget.Version.from_string(self.string) < nuget.Version.from_string(other.string)
370-
371-
def __eq__(self, other):
372-
return nuget.Version.from_string(self.string) == nuget.Version.from_string(other.string)
373335

374-
375-
@attr.s(frozen=True, order=False, eq=False, hash=True)
376336
class RpmVersion(Version):
377337
"""
378338
Represent an RPM version.
379-
380-
For example::
381-
382-
# 1:1.1.4|>=2.8.16|<=2.8.16-z
383339
"""
384340

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

389345

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

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

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

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

429372

430-
@attr.s(frozen=True, order=False, eq=False, hash=True)
431373
class ComposerVersion(SemverVersion):
432374
@classmethod
433375
def build_value(cls, string):
434-
return semantic_version.Version.coerce(string.lstrip("vV"))
376+
return super().build_value(string.lstrip("vV"))
435377

436378

437-
@attr.s(frozen=True, order=False, eq=False, hash=True)
438379
class GolangVersion(SemverVersion):
439380
@classmethod
440381
def build_value(cls, string):
441-
return semantic_version.Version.coerce(string.lstrip("vV"))
382+
return super().build_value(string.lstrip("vV"))
442383

443384

444-
@attr.s(frozen=True, order=False, eq=False, hash=True)
445385
class LegacyOpensslVersion(Version):
446386
"""
447387
Represent an Legacy Openssl Version.
@@ -557,7 +497,6 @@ def is_prerelease(self):
557497
return self.patch.startswith(("-beta", "-alpha"))
558498

559499

560-
@attr.s(frozen=True, order=False, eq=False, hash=True)
561500
class OpensslVersion(Version):
562501
"""
563502
Internally tracks two types of openssl versions
@@ -650,3 +589,22 @@ def __ge__(self, other):
650589
return self.value.__ge__(other.value)
651590
# version value are of diff type, then semver one is always ahead of legacy
652591
return isinstance(self.value, SemverVersion)
592+
593+
594+
AVAILABLE_VERSIONS = [
595+
SemverVersion,
596+
GolangVersion,
597+
PypiVersion,
598+
GenericVersion,
599+
ComposerVersion,
600+
NginxVersion,
601+
ArchLinuxVersion,
602+
DebianVersion,
603+
RpmVersion,
604+
MavenVersion,
605+
NugetVersion,
606+
GentooVersion,
607+
OpensslVersion,
608+
LegacyOpensslVersion,
609+
AlpineLinuxVersion,
610+
]

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)