Skip to content

Commit fa403ed

Browse files
Merge pull request #3146 from boegel/fix_list_software_detailed_py3
fix --list-software=detailed when using Python 3 by leveraging sort_looseversions function from py2vs3 module
2 parents 56dbe0c + 8f274ab commit fa403ed

File tree

4 files changed

+114
-4
lines changed

4 files changed

+114
-4
lines changed

easybuild/tools/docs.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
from easybuild.tools.config import build_option
5858
from easybuild.tools.filetools import read_file
5959
from easybuild.tools.modules import modules_tool
60-
from easybuild.tools.py2vs3 import OrderedDict, ascii_lowercase
60+
from easybuild.tools.py2vs3 import OrderedDict, ascii_lowercase, sort_looseversions
6161
from easybuild.tools.toolchain.toolchain import DUMMY_TOOLCHAIN_NAME, SYSTEM_TOOLCHAIN_NAME, is_system_toolchain
6262
from easybuild.tools.toolchain.utilities import search_toolchain
6363
from easybuild.tools.utilities import INDENT_2SPACES, INDENT_4SPACES
@@ -635,14 +635,22 @@ def key_to_ref(name):
635635
table_titles = ['version', 'toolchain']
636636
table_values = [[], []]
637637

638+
# first determine unique pairs of version/versionsuffix
639+
# we can't use LooseVersion yet here, since nub uses set and LooseVersion instances are not hashable
638640
pairs = nub((x['version'], x['versionsuffix']) for x in software[key])
639641

642+
# check whether any non-empty versionsuffixes are in play
640643
with_vsuff = any(vs for (_, vs) in pairs)
641644
if with_vsuff:
642645
table_titles.insert(1, 'versionsuffix')
643646
table_values.insert(1, [])
644647

645-
for ver, vsuff in sorted((LooseVersion(v), vs) for (v, vs) in pairs):
648+
# sort pairs by version (and then by versionsuffix);
649+
# we sort by LooseVersion to obtain chronological version ordering,
650+
# but we also need to retain original string version for filtering-by-version done below
651+
sorted_pairs = sort_looseversions((LooseVersion(v), vs, v) for v, vs in pairs)
652+
653+
for _, vsuff, ver in sorted_pairs:
646654
table_values[0].append('``%s``' % ver)
647655
if with_vsuff:
648656
if vsuff:
@@ -690,8 +698,17 @@ def list_software_txt(software, detailed=False):
690698
"homepage: %s" % software[key][-1]['homepage'],
691699
'',
692700
])
701+
702+
# first determine unique pairs of version/versionsuffix
703+
# we can't use LooseVersion yet here, since nub uses set and LooseVersion instances are not hashable
693704
pairs = nub((x['version'], x['versionsuffix']) for x in software[key])
694-
for ver, vsuff in sorted((LooseVersion(v), vs) for (v, vs) in pairs):
705+
706+
# sort pairs by version (and then by versionsuffix);
707+
# we sort by LooseVersion to obtain chronological version ordering,
708+
# but we also need to retain original string version for filtering-by-version done below
709+
sorted_pairs = sort_looseversions((LooseVersion(v), vs, v) for v, vs in pairs)
710+
711+
for _, vsuff, ver in sorted_pairs:
695712
tcs = [x['toolchain'] for x in software[key] if x['version'] == ver and x['versionsuffix'] == vsuff]
696713

697714
line = " * %s v%s" % (key, ver)

easybuild/tools/py2vs3/py2.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,10 @@ class WrapperBase(object):
8282
__wraps__ = None
8383

8484
return WrapperBase
85+
86+
87+
def sort_looseversions(looseversions):
88+
"""Sort list of (values including) LooseVersion instances."""
89+
# with Python 2, we can safely use 'sorted' on LooseVersion instances
90+
# (but we can't in Python 3, see https://bugs.python.org/issue14894)
91+
return sorted(looseversions)

easybuild/tools/py2vs3/py3.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
import sys
3737
import urllib.request as std_urllib # noqa
3838
from collections import OrderedDict # noqa
39+
from distutils.version import LooseVersion
40+
from functools import cmp_to_key
41+
from itertools import zip_longest
3942
from io import StringIO # noqa
4043
from string import ascii_letters, ascii_lowercase # noqa
4144
from urllib.request import HTTPError, HTTPSHandler, Request, URLError, build_opener, urlopen # noqa
@@ -86,3 +89,59 @@ class WrapperBase(object, metaclass=metaclass):
8689
__wraps__ = None
8790

8891
return WrapperBase
92+
93+
94+
def safe_cmp_looseversions(v1, v2):
95+
"""Safe comparison function for two (values containing) LooseVersion instances."""
96+
97+
if type(v1) != type(v2):
98+
raise TypeError("Can't compare values of different types: %s (%s) vs %s (%s)" % (v1, type(v1), v2, type(v2)))
99+
100+
# if we receive two iterative values, we need to recurse
101+
if isinstance(v1, (list, tuple)) and isinstance(v2, (list, tuple)):
102+
if len(v1) == len(v2):
103+
for x1, x2 in zip(v1, v2):
104+
res = safe_cmp_looseversions(x1, x2)
105+
# if a difference was found, we know the result;
106+
# if not, we need comparison on next item (if any), done in next iteration
107+
if res != 0:
108+
return res
109+
return 0 # no difference
110+
else:
111+
raise ValueError("Can only compare iterative values of same length: %s vs %s" % (v1, v2))
112+
113+
def simple_compare(x1, x2):
114+
"""Helper function for simple comparison using standard operators ==, <, > """
115+
if x1 < x2:
116+
return -1
117+
elif x1 > x2:
118+
return 1
119+
else:
120+
return 0
121+
122+
if isinstance(v1, LooseVersion) and isinstance(v2, LooseVersion):
123+
# implementation based on '14894.patch' patch file provided in https://bugs.python.org/issue14894
124+
for ver1_part, ver2_part in zip_longest(v1.version, v2.version, fillvalue=''):
125+
# use string comparison if version parts have different type
126+
if type(ver1_part) != type(ver2_part):
127+
ver1_part = str(ver1_part)
128+
ver2_part = str(ver2_part)
129+
130+
res = simple_compare(ver1_part, ver2_part)
131+
if res == 0:
132+
continue
133+
else:
134+
return res
135+
136+
# identical versions
137+
return 0
138+
else:
139+
# for non-LooseVersion values, use simple comparison
140+
return simple_compare(v1, v2)
141+
142+
143+
def sort_looseversions(looseversions):
144+
"""Sort list of (values including) LooseVersion instances."""
145+
# with Python 2, we can safely use 'sorted' on LooseVersion instances
146+
# (but we can't in Python 3, see https://bugs.python.org/issue14894)
147+
return sorted(looseversions, key=cmp_to_key(safe_cmp_looseversions))

test/framework/options.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import shutil
3434
import sys
3535
import tempfile
36+
from distutils.version import LooseVersion
3637
from unittest import TextTestRunner
3738

3839
import easybuild.main
@@ -55,7 +56,7 @@
5556
from easybuild.tools.github import URL_SEPARATOR, fetch_github_token
5657
from easybuild.tools.modules import Lmod
5758
from easybuild.tools.options import EasyBuildOptions, parse_external_modules_metadata, set_tmpdir, use_color
58-
from easybuild.tools.py2vs3 import URLError, reload
59+
from easybuild.tools.py2vs3 import URLError, reload, sort_looseversions
5960
from easybuild.tools.toolchain.utilities import TC_CONST_PREFIX
6061
from easybuild.tools.run import run_cmd
6162
from easybuild.tools.version import VERSION
@@ -4637,6 +4638,32 @@ def test_installdir(self):
46374638
eb = EasyBlock(EasyConfig(toy_ec))
46384639
self.assertTrue(eb.installdir.endswith('/software/Core/toy/0.0'))
46394640

4641+
def test_sort_looseversions(self):
4642+
"""Test sort_looseversions funuction."""
4643+
ver1 = LooseVersion('1.2.3')
4644+
ver2 = LooseVersion('4.5.6')
4645+
ver3 = LooseVersion('1.2.3dev')
4646+
ver4 = LooseVersion('system')
4647+
ver5 = LooseVersion('rc3')
4648+
ver6 = LooseVersion('v1802')
4649+
4650+
# some versions are included multiple times on purpose,
4651+
# to also test comparison between equal LooseVersion instances
4652+
input = [ver3, ver5, ver1, ver2, ver4, ver6, ver3, ver4, ver1]
4653+
expected = [ver1, ver1, ver3, ver3, ver2, ver5, ver4, ver4, ver6]
4654+
self.assertEqual(sort_looseversions(input), expected)
4655+
4656+
# also test on list of tuples consisting of a LooseVersion instance + a string
4657+
# (as in the list_software_* functions)
4658+
suff1 = ''
4659+
suff2 = '-foo'
4660+
suff3 = '-bar'
4661+
input = [(ver3, suff1), (ver5, suff3), (ver1, suff2), (ver2, suff3), (ver4, suff1),
4662+
(ver6, suff2), (ver3, suff3), (ver4, suff3), (ver1, suff1)]
4663+
expected = [(ver1, suff1), (ver1, suff2), (ver3, suff1), (ver3, suff3), (ver2, suff3),
4664+
(ver5, suff3), (ver4, suff1), (ver4, suff3), (ver6, suff2)]
4665+
self.assertEqual(sort_looseversions(input), expected)
4666+
46404667

46414668
def suite():
46424669
""" returns all the testcases in this module """

0 commit comments

Comments
 (0)