Skip to content

Commit cad3801

Browse files
authored
Merge pull request #4691 from Flamefire/looseversion
make `LooseVersion('1.0') == LooseVersion('1')`
2 parents 34466de + aa038b3 commit cad3801

File tree

5 files changed

+78
-36
lines changed

5 files changed

+78
-36
lines changed

easybuild/framework/easyconfig/format/format.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ def _squash(self, vt_tuple, processed, sanity):
433433
# walk over dictionary of parsed sections, and check for marker conflicts (using .add())
434434
for key, value in processed.items():
435435
if isinstance(value, NestedDict):
436-
tmp = self._squash_netsed_dict(key, value, squashed, sanity, vt_tuple)
436+
tmp = self._squash_nested_dict(key, value, squashed, sanity, vt_tuple)
437437
res_sections.update(tmp)
438438
elif key in self.VERSION_OPERATOR_VALUE_TYPES:
439439
self.log.debug("Found VERSION_OPERATOR_VALUE_TYPES entry (%s)" % key)
@@ -453,7 +453,7 @@ def _squash(self, vt_tuple, processed, sanity):
453453
(processed, squashed.versions, squashed.result))
454454
return squashed
455455

456-
def _squash_netsed_dict(self, key, nested_dict, squashed, sanity, vt_tuple):
456+
def _squash_nested_dict(self, key, nested_dict, squashed, sanity, vt_tuple):
457457
"""
458458
Squash NestedDict instance, returns dict with already squashed data
459459
from possible higher sections

easybuild/framework/easyconfig/format/version.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,16 @@
4545

4646

4747
class EasyVersion(LooseVersion):
48-
"""Exact LooseVersion. No modifications needed (yet)"""
48+
"""Represent a version"""
49+
50+
def __init__(self, vstring, is_default=False):
51+
super().__init__(vstring)
52+
self._is_default = is_default
53+
54+
@property
55+
def is_default(self):
56+
"""Return whether this is the default version used when no explicit version is specified"""
57+
return self._is_default
4958

5059
def __len__(self):
5160
"""Determine length of this EasyVersion instance."""
@@ -74,7 +83,7 @@ class VersionOperator(object):
7483
OPERATOR_FAMILIES = [['>', '>='], ['<', '<=']] # similar operators
7584

7685
# default version and operator when version is undefined
77-
DEFAULT_UNDEFINED_VERSION = EasyVersion('0.0.0')
86+
DEFAULT_UNDEFINED_VERSION = EasyVersion('0.0', is_default=True)
7887
DEFAULT_UNDEFINED_VERSION_OPERATOR = OPERATOR_MAP['>']
7988
# default operator when operator is undefined (but version is)
8089
DEFAULT_UNDEFINED_OPERATOR = OPERATOR_MAP['==']
@@ -256,7 +265,7 @@ def _convert_operator(self, operator_str, version=None):
256265
"""Return the operator"""
257266
operator = None
258267
if operator_str is None:
259-
if version == self.DEFAULT_UNDEFINED_VERSION or version is None:
268+
if version is None or version.is_default:
260269
operator = self.DEFAULT_UNDEFINED_VERSION_OPERATOR
261270
else:
262271
operator = self.DEFAULT_UNDEFINED_OPERATOR

easybuild/tools/loose_version.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
# This file contains the LooseVersion class based on the class with the same name
2-
# as present in Python 3.7.4 distutils.
3-
# The original class is licensed under the Python Software Foundation License Version 2.
4-
# It was slightly simplified as needed to make it shorter and easier to read.
5-
# In particular the following changes were made:
6-
# - Subclass object directly instead of abstract Version class
7-
# - Fully init the class in the constructor removing the parse method
8-
# - Always set self.vstring and self.version
9-
# - Shorten the comparison operators as the NotImplemented case doesn't apply anymore
10-
# - Changes to documentation and formatting
1+
"""
2+
This file contains the LooseVersion class based on the class with the same name
3+
as present in Python 3.7.4 distutils.
4+
The original class is licensed under the Python Software Foundation License Version 2.
5+
It was slightly simplified as needed to make it shorter and easier to read.
6+
In particular the following changes were made:
7+
- Subclass object directly instead of abstract Version class
8+
- Fully init the class in the constructor removing the parse method
9+
- Always set self.vstring and self.version
10+
- Shorten the comparison operators as the NotImplemented case doesn't apply anymore
11+
- Changes to documentation and formatting
12+
"""
1113

1214
import re
1315
from itertools import zip_longest
@@ -75,17 +77,19 @@ def _cmp(self, other):
7577
if isinstance(other, str):
7678
other = LooseVersion(other)
7779

78-
# Modified: Behave the same in Python 2 & 3 when parts are of different types
79-
# Taken from https://bugs.python.org/issue14894
80-
for i, j in zip_longest(self.version, other.version, fillvalue=''):
81-
if not type(i) is type(j):
80+
# Modified: Use string comparison for different types and fill with zeroes/empty strings
81+
# Based on https://bugs.python.org/issue14894
82+
for i, j in zip_longest(self.version, other.version):
83+
if i is None:
84+
i = 0 if isinstance(j, int) else ''
85+
elif j is None:
86+
j = 0 if isinstance(i, int) else ''
87+
elif not type(i) is type(j):
8288
i = str(i)
8389
j = str(j)
84-
if i == j:
85-
continue
86-
elif i < j:
90+
if i < j:
8791
return -1
88-
else: # i > j
92+
if i > j:
8993
return 1
9094
return 0
9195

test/framework/ebconfigobj.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,32 @@ def test_squash_simple(self):
116116
res = cov.squash(version, tc['name'], tc['version'])
117117
self.assertEqual(res, {}) # very simple
118118

119+
# Ensure that a version of '0' with trailing '.0's is matched against '0.0' but not anything higher
120+
# This is for testing the DEFAULT_UNDEFINED_VERSION detection
121+
for num_zeroes in range(1, 6):
122+
tc = tc_first
123+
zero_version = '.'.join(['0'] * num_zeroes)
124+
txt = [
125+
'[SUPPORTED]',
126+
'versions = ' + zero_version,
127+
'toolchains = ' + tc_tmpl % tc,
128+
'[DEFAULT]',
129+
'y=a',
130+
]
131+
co = ConfigObj(txt)
132+
cov = EBConfigObj(co)
133+
self.assertEqual(cov.squash('0.0', tc['name'], tc['version']), {'y': 'a'})
134+
self.assertEqual(cov.squash('0.1', tc['name'], tc['version']), {})
135+
119136
def test_squash_invalid(self):
120137
"""Try to squash invalid files. Should trigger error"""
121138
tc_first = {'version': '10', 'name': self.tc_first}
122139
tc_last = {'version': '100', 'name': self.tc_last}
123140

124141
tc_tmpl = '%(name)s == %(version)s'
125142

126-
default_version = '1.0'
127-
all_wrong_versions = [default_version, '>= 0.0', '< 1.0']
143+
default_version = '1.1'
144+
all_wrong_versions = [default_version, '>= 0.0', '< 1.1']
128145

129146
# all txt should have default version and first toolchain unmodified
130147

test/framework/utilities_test.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,22 @@ def test_LooseVersion(self):
140140
self.assertLess(LooseVersion('2.1.5'), LooseVersion('2.2'))
141141
self.assertLess(LooseVersion('2.1.3'), LooseVersion('3'))
142142
self.assertLessEqual(LooseVersion('2.1.0'), LooseVersion('2.2'))
143-
# Careful here: 1.0 > 1 !!!
144-
self.assertGreater(LooseVersion('1.0'), LooseVersion('1'))
145-
self.assertLess(LooseVersion('1'), LooseVersion('1.0'))
146-
# checking prereleases
147-
self.assertGreater(LooseVersion('4.0.0-beta'), LooseVersion('4.0.0'))
148-
self.assertEqual(LooseVersion('4.0.0-beta').is_prerelease('4.0.0', ['-beta']), True)
149-
self.assertEqual(LooseVersion('4.0.0-beta').is_prerelease('4.0.0', ['rc']), False)
143+
# Missing components are either empty strings or zeroes
144+
self.assertEqual(LooseVersion('1.0'), LooseVersion('1'))
145+
self.assertEqual(LooseVersion('1'), LooseVersion('1.0'))
146+
self.assertEqual(LooseVersion('1.0'), LooseVersion('1.'))
147+
self.assertGreater(LooseVersion('2.1.a'), LooseVersion('2.1'))
148+
self.assertGreater(LooseVersion('2.a'), LooseVersion('2'))
150149

151-
# The following test is taken from Python distutils tests
150+
# checking prereleases
151+
version_4beta = LooseVersion('4.0.0-beta')
152+
self.assertGreater(version_4beta, LooseVersion('4.0.0'))
153+
self.assertTrue(version_4beta.is_prerelease('4.0.0', ['-beta']))
154+
self.assertTrue(version_4beta.is_prerelease(LooseVersion('4.0.0'), ['-beta']))
155+
self.assertFalse(version_4beta.is_prerelease('4.0.0', ['rc']))
156+
self.assertFalse(version_4beta.is_prerelease('4.0.0', ['rc, -beta']))
157+
158+
# The following test is based on the Python distutils tests
152159
# licensed under the Python Software Foundation License Version 2
153160
versions = (('1.5.1', '1.5.2b2', -1),
154161
('161', '3.10a', 1),
@@ -158,16 +165,21 @@ def test_LooseVersion(self):
158165
('2g6', '11g', -1),
159166
('0.960923', '2.2beta29', -1),
160167
('1.13++', '5.5.kw', -1),
161-
# Added from https://bugs.python.org/issue14894
162168
('a.12.b.c', 'a.b.3', -1),
163-
('1.0', '1', 1),
164-
('1', '1.0', -1))
169+
('1.0', '1', 0),
170+
('1.a', '1', 1),
171+
)
165172

166173
for v1, v2, wanted in versions:
167174
res = LooseVersion(v1)._cmp(LooseVersion(v2))
168175
self.assertEqual(res, wanted,
169176
'cmp(%s, %s) should be %s, got %s' %
170177
(v1, v2, wanted, res))
178+
# Test the inverse
179+
res = LooseVersion(v2)._cmp(LooseVersion(v1))
180+
self.assertEqual(res, -wanted,
181+
'cmp(%s, %s) should be %s, got %s' %
182+
(v2, v1, -wanted, res))
171183
# vstring is the unparsed version
172184
self.assertEqual(LooseVersion(v1).vstring, v1)
173185

0 commit comments

Comments
 (0)