Skip to content

Commit

Permalink
Eliminate use of distutils.version.StrictVersion
Browse files Browse the repository at this point in the history
distutils was removed in Python 3.12. The only reason EasyBuild uses
StrictVersion is that it orders beta/rc versions before the released
version, unlike LooseVersion. E.g. 5.0.0-beta < 5.0.0 (but > for
LooseVersion).

So a new method
`is_earlier_or_prerelease(self, other, markers)`
was added to LooseVersion to handle that particular case.

Addresses part of easybuilders#3963
  • Loading branch information
bartoldeman committed Mar 6, 2024
1 parent 82c8516 commit e9cec34
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 35 deletions.
9 changes: 0 additions & 9 deletions easybuild/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,5 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__)


import distutils.version
import warnings
from easybuild.tools.loose_version import LooseVersion # noqa(F401)


class StrictVersion(distutils.version.StrictVersion):
"""Temporary wrapper over distuitls StrictVersion that silences the deprecation warning"""
def __init__(self, *args, **kwargs):
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
distutils.version.StrictVersion.__init__(self, *args, **kwargs)
16 changes: 16 additions & 0 deletions easybuild/tools/loose_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ def version(self):
"""Readonly access to the parsed version (list or None)"""
return self._version

def is_earlier_or_prerelease(self, other, markers):
"""Check if this is an earlier version or prerelease of other
Markers is a list of strings that denote a prerelease
"""
if isinstance(other, str):
vstring = other
else:
vstring = other._vstring
if self._vstring.startswith(vstring):
prerelease = self._vstring[len(vstring):]
for marker in markers:
if prerelease.startswith(marker):
return True
return self < other

def __str__(self):
return self._vstring

Expand Down
27 changes: 10 additions & 17 deletions easybuild/tools/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
import shlex

from easybuild.base import fancylogger
from easybuild.tools import StrictVersion
from easybuild.tools import LooseVersion
from easybuild.tools.build_log import EasyBuildError, print_warning
from easybuild.tools.config import ERROR, IGNORE, PURGE, UNLOAD, UNSET
from easybuild.tools.config import EBROOT_ENV_VAR_ACTIONS, LOADED_MODULES_ACTIONS
Expand Down Expand Up @@ -144,11 +144,11 @@ class ModulesTool(object):
COMMAND_SHELL = None
# option to determine the version
VERSION_OPTION = '--version'
# minimal required version (StrictVersion; suffix rc replaced with b (and treated as beta by StrictVersion))
# minimal required version (cannot include -beta or rc)
REQ_VERSION = None
# deprecated version limit (support for versions below this version is deprecated)
DEPR_VERSION = None
# maximum version allowed (StrictVersion; suffix rc replaced with b (and treated as beta by StrictVersion))
# maximum version allowed (cannot include -beta or rc)
MAX_VERSION = None
# the regexp, should have a "version" group (multiline search)
VERSION_REGEXP = None
Expand Down Expand Up @@ -239,14 +239,6 @@ def set_and_check_version(self):
if res:
self.version = res.group('version')
self.log.info("Found %s version %s", self.NAME, self.version)

# make sure version is a valid StrictVersion (e.g., 5.7.3.1 is invalid),
# and replace 'rc' by 'b', to make StrictVersion treat it as a beta-release
self.version = self.version.replace('rc', 'b').replace('-beta', 'b1')
if len(self.version.split('.')) > 3:
self.version = '.'.join(self.version.split('.')[:3])

self.log.info("Converted actual version to '%s'" % self.version)
else:
raise EasyBuildError("Failed to determine %s version from option '%s' output: %s",
self.NAME, self.VERSION_OPTION, txt)
Expand All @@ -259,24 +251,25 @@ def set_and_check_version(self):
elif build_option('modules_tool_version_check'):
self.log.debug("Checking whether %s version %s meets requirements", self.NAME, self.version)

version = LooseVersion(self.version)
if self.REQ_VERSION is not None:
self.log.debug("Required minimum %s version defined: %s", self.NAME, self.REQ_VERSION)
if StrictVersion(self.version) < StrictVersion(self.REQ_VERSION):
if version.is_earlier_or_prerelease(self.REQ_VERSION, ['rc', '-beta']):
raise EasyBuildError("EasyBuild requires %s >= v%s, found v%s",
self.NAME, self.REQ_VERSION, self.version)
else:
self.log.debug('%s version %s matches requirement >= %s', self.NAME, self.version, self.REQ_VERSION)

if self.DEPR_VERSION is not None:
self.log.debug("Deprecated %s version limit defined: %s", self.NAME, self.DEPR_VERSION)
if StrictVersion(self.version) < StrictVersion(self.DEPR_VERSION):
if version.is_earlier_or_prerelease(self.DEPR_VERSION, ['rc', '-beta']):
depr_msg = "Support for %s version < %s is deprecated, " % (self.NAME, self.DEPR_VERSION)
depr_msg += "found version %s" % self.version
self.log.deprecated(depr_msg, '6.0')

if self.MAX_VERSION is not None:
self.log.debug("Maximum allowed %s version defined: %s", self.NAME, self.MAX_VERSION)
if StrictVersion(self.version) > StrictVersion(self.MAX_VERSION):
if version.is_earlier_or_prerelease(self.MAX_VERSION, ['rc', '-beta']):
raise EasyBuildError("EasyBuild requires %s <= v%s, found v%s",
self.NAME, self.MAX_VERSION, self.version)
else:
Expand Down Expand Up @@ -1390,7 +1383,7 @@ def available(self, mod_name=None, extra_args=None):
if extra_args is None:
extra_args = []
# make hidden modules visible (requires Environment Modules 4.6.0)
if StrictVersion(self.version) >= StrictVersion('4.6.0'):
if LooseVersion(self.version) >= LooseVersion('4.6.0'):
extra_args.append(self.SHOW_HIDDEN_OPTION)

return super(EnvironmentModules, self).available(mod_name=mod_name, extra_args=extra_args)
Expand Down Expand Up @@ -1440,11 +1433,11 @@ def __init__(self, *args, **kwargs):
setvar('LMOD_EXTENDED_DEFAULT', 'no', verbose=False)

super(Lmod, self).__init__(*args, **kwargs)
version = StrictVersion(self.version)
version = LooseVersion(self.version)

self.supports_depends_on = True
# See https://lmod.readthedocs.io/en/latest/125_personal_spider_cache.html
if version >= '8.7.12':
if version >= LooseVersion('8.7.12'):
self.USER_CACHE_DIR = os.path.join(os.path.expanduser('~'), '.cache', 'lmod')
else:
self.USER_CACHE_DIR = os.path.join(os.path.expanduser('~'), '.lmod.d', '.cache')
Expand Down
14 changes: 8 additions & 6 deletions test/framework/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
import easybuild.tools.modules as mod
from easybuild.framework.easyblock import EasyBlock
from easybuild.framework.easyconfig.easyconfig import EasyConfig
from easybuild.tools import StrictVersion
from easybuild.tools import LooseVersion
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.environment import modify_env
from easybuild.tools.filetools import adjust_permissions, copy_file, copy_dir, mkdir
Expand Down Expand Up @@ -226,15 +226,16 @@ def test_avail(self):

# all test modules are accounted for
ms = self.modtool.available()
version = LooseVersion(self.modtool.version)

if isinstance(self.modtool, Lmod) and StrictVersion(self.modtool.version) >= StrictVersion('5.7.5'):
if isinstance(self.modtool, Lmod) and not version.is_prerelease_or_earlier('5.7.5', ['rc']):
# with recent versions of Lmod, also the hidden modules are included in the output of 'avail'
self.assertEqual(len(ms), TEST_MODULES_COUNT + 3)
self.assertIn('bzip2/.1.0.6', ms)
self.assertIn('toy/.0.0-deps', ms)
self.assertIn('OpenMPI/.2.1.2-GCC-6.4.0-2.28', ms)
elif (isinstance(self.modtool, EnvironmentModules)
and StrictVersion(self.modtool.version) >= StrictVersion('4.6.0')):
and not version.is_prerelease_or_earlier('4.6.0', ['-beta'])):
# bzip2/.1.0.6 is not there, since that's a module file in Lua syntax
self.assertEqual(len(ms), TEST_MODULES_COUNT + 2)
self.assertIn('toy/.0.0-deps', ms)
Expand Down Expand Up @@ -314,7 +315,8 @@ def test_exist(self):

avail_mods = self.modtool.available()
self.assertIn('Java/1.8.0_181', avail_mods)
if isinstance(self.modtool, Lmod) and StrictVersion(self.modtool.version) >= StrictVersion('7.0'):
version = LooseVersion(self.modtool.version)
if isinstance(self.modtool, Lmod) and not version.is_earlier_or_prerelease('7.0', ['rc']):
self.assertIn('Java/1.8', avail_mods)
self.assertIn('Java/site_default', avail_mods)
self.assertIn('JavaAlias', avail_mods)
Expand Down Expand Up @@ -374,7 +376,7 @@ def test_exist(self):
self.assertEqual(self.modtool.exist(['Core/Java/1.8', 'Core/Java/site_default']), [True, True])

# also check with .modulerc.lua for Lmod 7.8 or newer
if isinstance(self.modtool, Lmod) and StrictVersion(self.modtool.version) >= StrictVersion('7.8'):
if isinstance(self.modtool, Lmod) and not version.is_earlier_or_prerelease('7.8', ['rc']):
shutil.move(os.path.join(self.test_prefix, 'Core', 'Java'), java_mod_dir)
reset_module_caches()

Expand Down Expand Up @@ -406,7 +408,7 @@ def test_exist(self):
self.assertEqual(self.modtool.exist(['Core/Java/site_default']), [True])

# Test alias in home directory .modulerc
if isinstance(self.modtool, Lmod) and StrictVersion(self.modtool.version) >= StrictVersion('7.0'):
if isinstance(self.modtool, Lmod) and not version.is_earlier_or_prerelease('7.0', ['rc']):
# Required or temporary HOME would be in MODULEPATH already
self.init_testmods()
# Sanity check: Module aliases don't exist yet
Expand Down
6 changes: 3 additions & 3 deletions test/framework/modulestool.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from unittest import TextTestRunner

from easybuild.base import fancylogger
from easybuild.tools import modules, StrictVersion
from easybuild.tools import modules, LooseVersion
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.filetools import read_file, which, write_file
from easybuild.tools.modules import EnvironmentModules, Lmod
Expand Down Expand Up @@ -76,7 +76,7 @@ def test_mock(self):
mmt = MockModulesTool(mod_paths=[], testing=True)

# the version of the MMT is the commandline option
self.assertEqual(mmt.version, StrictVersion(MockModulesTool.VERSION_OPTION))
self.assertEqual(mmt.version, LooseVersion(MockModulesTool.VERSION_OPTION))

cmd_abspath = which(MockModulesTool.COMMAND)

Expand All @@ -100,7 +100,7 @@ def test_environment_command(self):
bmmt = BrokenMockModulesTool(mod_paths=[], testing=True)
cmd_abspath = which(MockModulesTool.COMMAND)

self.assertEqual(bmmt.version, StrictVersion(MockModulesTool.VERSION_OPTION))
self.assertEqual(bmmt.version, LooseVersion(MockModulesTool.VERSION_OPTION))
self.assertEqual(bmmt.cmd, cmd_abspath)

# clean it up
Expand Down
4 changes: 4 additions & 0 deletions test/framework/utilities_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ def test_LooseVersion(self):
# Careful here: 1.0 > 1 !!!
self.assertGreater(LooseVersion('1.0'), LooseVersion('1'))
self.assertLess(LooseVersion('1'), LooseVersion('1.0'))
# checking prereleases
self.assertGreater(LooseVersion('4.0.0-beta'), LooseVersion('4.0.0'))
self.assertEqual(LooseVersion('4.0.0-beta').is_earlier_or_prerelease('4.0.0'), ['-beta'], True)
self.assertEqual(LooseVersion('4.0.0-beta').is_earlier_or_prerelease('4.0.0'), ['rc'], False)

# The following test is taken from Python distutils tests
# licensed under the Python Software Foundation License Version 2
Expand Down

0 comments on commit e9cec34

Please sign in to comment.