From 30430bc983492a4da5889b78dcf28256af4f0b48 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 27 Apr 2022 11:13:19 -0400 Subject: [PATCH 1/6] MNT: Vendor distutils.version to ease transition --- nipype/external/version.py | 224 +++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 nipype/external/version.py diff --git a/nipype/external/version.py b/nipype/external/version.py new file mode 100644 index 0000000000..0a2fbf167e --- /dev/null +++ b/nipype/external/version.py @@ -0,0 +1,224 @@ +# This module has been vendored from CPython distutils/version.py +# last updated in 662db125cddbca1db68116c547c290eb3943d98e +# +# It is licensed according to the Python Software Foundation License Version 2 +# which may be found in full in the following (hopefully persistent) locations: +# +# https://github.com/python/cpython/blob/main/LICENSE +# https://spdx.org/licenses/Python-2.0.html +# +# The following changes have been made: +# +# 2022.04.27 - Minor changes are made to the comments, +# - The StrictVersion class was removed +# - Black styling was applied +# + +# distutils/version.py +# +# Implements multiple version numbering conventions for the +# Python Module Distribution Utilities. + +"""Provides classes to represent module version numbers (one class for +each style of version numbering). There are currently two such classes +implemented: StrictVersion and LooseVersion. + +Every version number class implements the following interface: + * the 'parse' method takes a string and parses it to some internal + representation; if the string is an invalid version number, + 'parse' raises a ValueError exception + * the class constructor takes an optional string argument which, + if supplied, is passed to 'parse' + * __str__ reconstructs the string that was passed to 'parse' (or + an equivalent string -- ie. one that will generate an equivalent + version number instance) + * __repr__ generates Python code to recreate the version number instance + * _cmp compares the current instance with either another instance + of the same class or a string (which will be parsed to an instance + of the same class, thus must follow the same rules) +""" + +import re + + +class Version: + """Abstract base class for version numbering classes. Just provides + constructor (__init__) and reproducer (__repr__), because those + seem to be the same for all version numbering classes; and route + rich comparisons to _cmp. + """ + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def __repr__(self): + return "%s ('%s')" % (self.__class__.__name__, str(self)) + + def __eq__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c == 0 + + def __lt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c < 0 + + def __le__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c <= 0 + + def __gt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c > 0 + + def __ge__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c >= 0 + + +# The rules according to Greg Stein: +# 1) a version number has 1 or more numbers separated by a period or by +# sequences of letters. If only periods, then these are compared +# left-to-right to determine an ordering. +# 2) sequences of letters are part of the tuple for comparison and are +# compared lexicographically +# 3) recognize the numeric components may have leading zeroes +# +# The LooseVersion class below implements these rules: a version number +# string is split up into a tuple of integer and string components, and +# comparison is a simple tuple comparison. This means that version +# numbers behave in a predictable and obvious way, but a way that might +# not necessarily be how people *want* version numbers to behave. There +# wouldn't be a problem if people could stick to purely numeric version +# numbers: just split on period and compare the numbers as tuples. +# However, people insist on putting letters into their version numbers; +# the most common purpose seems to be: +# - indicating a "pre-release" version +# ('alpha', 'beta', 'a', 'b', 'pre', 'p') +# - indicating a post-release patch ('p', 'pl', 'patch') +# but of course this can't cover all version number schemes, and there's +# no way to know what a programmer means without asking him. +# +# The problem is what to do with letters (and other non-numeric +# characters) in a version number. The current implementation does the +# obvious and predictable thing: keep them as strings and compare +# lexically within a tuple comparison. This has the desired effect if +# an appended letter sequence implies something "post-release": +# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". +# +# However, if letters in a version number imply a pre-release version, +# the "obvious" thing isn't correct. Eg. you would expect that +# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison +# implemented here, this just isn't so. +# +# Two possible solutions come to mind. The first is to tie the +# comparison algorithm to a particular set of semantic rules, as has +# been done in the StrictVersion class above. This works great as long +# as everyone can go along with bondage and discipline. Hopefully a +# (large) subset of Python module programmers will agree that the +# particular flavour of bondage and discipline provided by StrictVersion +# provides enough benefit to be worth using, and will submit their +# version numbering scheme to its domination. The free-thinking +# anarchists in the lot will never give in, though, and something needs +# to be done to accommodate them. +# +# Perhaps a "moderately strict" version class could be implemented that +# lets almost anything slide (syntactically), and makes some heuristic +# assumptions about non-digits in version number strings. This could +# sink into special-case-hell, though; if I was as talented and +# idiosyncratic as Larry Wall, I'd go ahead and implement a class that +# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is +# just as happy dealing with things like "2g6" and "1.13++". I don't +# think I'm smart enough to do it right though. +# +# In any case, I've coded the test suite for this module (see +# ../test/test_version.py) specifically to fail on things like comparing +# "1.2a2" and "1.2". That's not because the *code* is doing anything +# wrong, it's because the simple, obvious design doesn't match my +# complicated, hairy expectations for real-world version numbers. It +# would be a snap to fix the test suite to say, "Yep, LooseVersion does +# the Right Thing" (ie. the code matches the conception). But I'd rather +# have a conception that matches common notions about version numbers. + + +class LooseVersion(Version): + + """Version numbering for anarchists and software realists. + Implements the standard interface for version number classes as + described above. A version number consists of a series of numbers, + separated by either periods or strings of letters. When comparing + version numbers, the numeric components will be compared + numerically, and the alphabetic components lexically. The following + are all valid version numbers, in no particular order: + + 1.5.1 + 1.5.2b2 + 161 + 3.10a + 8.02 + 3.4j + 1996.07.12 + 3.2.pl0 + 3.1.1.6 + 2g6 + 11g + 0.960923 + 2.2beta29 + 1.13++ + 5.5.kw + 2.0b1pl0 + + In fact, there is no such thing as an invalid version number under + this scheme; the rules for comparison are simple and predictable, + but may not always give the results you want (for some definition + of "want"). + """ + + component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def parse(self, vstring): + # I've given up on thinking I can reconstruct the version string + # from the parsed tuple -- so I just store the string here for + # use by __str__ + self.vstring = vstring + components = [x for x in self.component_re.split(vstring) if x and x != '.'] + for i, obj in enumerate(components): + try: + components[i] = int(obj) + except ValueError: + pass + + self.version = components + + def __str__(self): + return self.vstring + + def __repr__(self): + return "LooseVersion ('%s')" % str(self) + + def _cmp(self, other): + if isinstance(other, str): + other = LooseVersion(other) + elif not isinstance(other, LooseVersion): + return NotImplemented + + if self.version == other.version: + return 0 + if self.version < other.version: + return -1 + if self.version > other.version: + return 1 From 5c7f2db901b648cd03620f892baefa08a8da367c Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 10 May 2022 11:47:58 -0400 Subject: [PATCH 2/6] RF: Import LooseVersion from nipype.external.version --- nipype/__init__.py | 4 +++- nipype/interfaces/dipy/preprocess.py | 3 +-- nipype/interfaces/dipy/reconstruction.py | 2 +- nipype/interfaces/dipy/registration.py | 2 +- nipype/interfaces/dipy/stats.py | 2 +- nipype/interfaces/dipy/tracks.py | 2 +- nipype/utils/config.py | 2 +- nipype/utils/misc.py | 4 ++-- 8 files changed, 11 insertions(+), 10 deletions(-) diff --git a/nipype/__init__.py b/nipype/__init__.py index 72b7241020..bfc1e16a5b 100644 --- a/nipype/__init__.py +++ b/nipype/__init__.py @@ -12,7 +12,9 @@ """ import os -from distutils.version import LooseVersion + +# XXX Deprecate this import +from .external.version import LooseVersion from .info import URL as __url__, STATUS as __status__, __version__ from .utils.config import NipypeConfig diff --git a/nipype/interfaces/dipy/preprocess.py b/nipype/interfaces/dipy/preprocess.py index 7e3c67b977..d4271b6159 100644 --- a/nipype/interfaces/dipy/preprocess.py +++ b/nipype/interfaces/dipy/preprocess.py @@ -3,9 +3,8 @@ import os.path as op import nibabel as nb import numpy as np -from distutils.version import LooseVersion - +from nipype.external.version import LooseVersion from ... import logging from ..base import traits, TraitedSpec, File, isdefined from .base import ( diff --git a/nipype/interfaces/dipy/reconstruction.py b/nipype/interfaces/dipy/reconstruction.py index 27a9e632ad..cef7579772 100644 --- a/nipype/interfaces/dipy/reconstruction.py +++ b/nipype/interfaces/dipy/reconstruction.py @@ -7,7 +7,7 @@ import numpy as np import nibabel as nb -from distutils.version import LooseVersion +from nipype.external.version import LooseVersion from ... import logging from ..base import TraitedSpec, File, traits, isdefined diff --git a/nipype/interfaces/dipy/registration.py b/nipype/interfaces/dipy/registration.py index f70c566194..e07859560d 100644 --- a/nipype/interfaces/dipy/registration.py +++ b/nipype/interfaces/dipy/registration.py @@ -1,4 +1,4 @@ -from distutils.version import LooseVersion +from nipype.external.version import LooseVersion from ... import logging from .base import HAVE_DIPY, dipy_version, dipy_to_nipype_interface, get_dipy_workflows diff --git a/nipype/interfaces/dipy/stats.py b/nipype/interfaces/dipy/stats.py index fff0184a56..971857b64e 100644 --- a/nipype/interfaces/dipy/stats.py +++ b/nipype/interfaces/dipy/stats.py @@ -1,4 +1,4 @@ -from distutils.version import LooseVersion +from nipype.external.version import LooseVersion from ... import logging from .base import HAVE_DIPY, dipy_version, dipy_to_nipype_interface, get_dipy_workflows diff --git a/nipype/interfaces/dipy/tracks.py b/nipype/interfaces/dipy/tracks.py index 9ac9e0b59c..6b1da93a95 100644 --- a/nipype/interfaces/dipy/tracks.py +++ b/nipype/interfaces/dipy/tracks.py @@ -3,7 +3,7 @@ import os.path as op import numpy as np import nibabel as nb -from distutils.version import LooseVersion +from nipype.external.version import LooseVersion from ... import logging from ..base import TraitedSpec, BaseInterfaceInputSpec, File, isdefined, traits diff --git a/nipype/utils/config.py b/nipype/utils/config.py index e7020eb30d..3106bd4c8c 100644 --- a/nipype/utils/config.py +++ b/nipype/utils/config.py @@ -14,7 +14,7 @@ import errno import atexit from warnings import warn -from distutils.version import LooseVersion +from nipype.external.version import LooseVersion import configparser import numpy as np diff --git a/nipype/utils/misc.py b/nipype/utils/misc.py index 6b106da952..ba8687110c 100644 --- a/nipype/utils/misc.py +++ b/nipype/utils/misc.py @@ -9,7 +9,7 @@ from collections.abc import Iterator from warnings import warn -from distutils.version import LooseVersion +from nipype.external.version import LooseVersion import numpy as np @@ -145,7 +145,7 @@ def package_check( packages. Default is *Nipype*. checker : object, optional The class that will perform the version checking. Default is - distutils.version.LooseVersion. + nipype.external.version.LooseVersion. exc_failed_import : Exception, optional Class of the exception to be thrown if import failed. exc_failed_check : Exception, optional From 12cd44f17fade0406bf4bfa877e485f44870e11b Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 26 Apr 2022 22:11:53 -0400 Subject: [PATCH 3/6] MNT: Drop distutils.spawn.find_executable for shutils.which --- nipype/interfaces/afni/base.py | 4 ++-- nipype/interfaces/mrtrix3/connectivity.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nipype/interfaces/afni/base.py b/nipype/interfaces/afni/base.py index 3f338eb0ce..660e913dc3 100644 --- a/nipype/interfaces/afni/base.py +++ b/nipype/interfaces/afni/base.py @@ -4,7 +4,7 @@ """Provide a base interface to AFNI commands.""" import os from sys import platform -from distutils import spawn +import shutil from ... import logging, LooseVersion from ...utils.filemanip import split_filename, fname_presuffix @@ -317,7 +317,7 @@ class AFNIPythonCommand(AFNICommand): def cmd(self): """Revise the command path.""" orig_cmd = super(AFNIPythonCommand, self).cmd - found = spawn.find_executable(orig_cmd) + found = shutil.which(orig_cmd) return found if found is not None else orig_cmd @property diff --git a/nipype/interfaces/mrtrix3/connectivity.py b/nipype/interfaces/mrtrix3/connectivity.py index 308eccd45f..95e3546266 100644 --- a/nipype/interfaces/mrtrix3/connectivity.py +++ b/nipype/interfaces/mrtrix3/connectivity.py @@ -208,9 +208,9 @@ def _parse_inputs(self, skip=None): skip = [] if not isdefined(self.inputs.in_config): - from distutils.spawn import find_executable + from shutil import which - path = find_executable(self._cmd) + path = which(self._cmd) if path is None: path = os.getenv(MRTRIX3_HOME, "/opt/mrtrix3") else: From 71876b314743f6001c7b0c06465f1e9e19562c10 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 27 Apr 2022 09:16:35 -0400 Subject: [PATCH 4/6] MNT: Drop networkx<2 hack --- nipype/pipeline/engine/utils.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/nipype/pipeline/engine/utils.py b/nipype/pipeline/engine/utils.py index a7ba7f5f34..32aa21bcc6 100644 --- a/nipype/pipeline/engine/utils.py +++ b/nipype/pipeline/engine/utils.py @@ -18,7 +18,7 @@ import numpy as np -from ... import logging, config, LooseVersion +from ... import logging, config from ...utils.filemanip import ( indirectory, relpath, @@ -1076,11 +1076,7 @@ def make_field_func(*pair): inode._id += ".%sI" % iterable_prefix # merge the iterated subgraphs - # dj: the behaviour of .copy changes in version 2 - if LooseVersion(nx.__version__) < LooseVersion("2"): - subgraph = graph_in.subgraph(subnodes) - else: - subgraph = graph_in.subgraph(subnodes).copy() + subgraph = graph_in.subgraph(subnodes).copy() graph_in = _merge_graphs( graph_in, subnodes, From f580aa0d59ccd7e31b3a355c5fe7f898fc110dab Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 28 Apr 2022 08:06:58 -0400 Subject: [PATCH 5/6] MNT: Purge StrictVersion --- nipype/interfaces/niftyreg/base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nipype/interfaces/niftyreg/base.py b/nipype/interfaces/niftyreg/base.py index 375a3ada29..f62a92b84a 100644 --- a/nipype/interfaces/niftyreg/base.py +++ b/nipype/interfaces/niftyreg/base.py @@ -15,8 +15,8 @@ See the docstrings of the individual classes for examples. """ -from distutils.version import StrictVersion import os +from packaging.version import Version from ... import logging from ..base import CommandLine, CommandLineInputSpec, traits, Undefined, PackageInfo @@ -65,13 +65,13 @@ def __init__(self, required_version=None, **inputs): self.required_version = required_version _version = self.version if _version: - if self._min_version is not None and StrictVersion( - _version - ) < StrictVersion(self._min_version): + if self._min_version is not None and Version(_version) < Version( + self._min_version + ): msg = "A later version of Niftyreg is required (%s < %s)" iflogger.warning(msg, _version, self._min_version) if required_version is not None: - if StrictVersion(_version) != StrictVersion(required_version): + if Version(_version) != Version(required_version): msg = "The version of NiftyReg differs from the required" msg += "(%s != %s)" iflogger.warning(msg, _version, self.required_version) @@ -101,11 +101,11 @@ def check_version(self): _version = self.version if not _version: raise Exception("Niftyreg not found") - if StrictVersion(_version) < StrictVersion(self._min_version): + if Version(_version) < Version(self._min_version): err = "A later version of Niftyreg is required (%s < %s)" raise ValueError(err % (_version, self._min_version)) if self.required_version: - if StrictVersion(_version) != StrictVersion(self.required_version): + if Version(_version) != Version(self.required_version): err = "The version of NiftyReg differs from the required" err += "(%s != %s)" raise ValueError(err % (_version, self.required_version)) From 1acd3fa5b1b79d8275a6fa91c132afa9f5967770 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Thu, 28 Apr 2022 08:14:35 -0400 Subject: [PATCH 6/6] MNT: Drop unused tools/toollib.py --- tools/toollib.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 tools/toollib.py diff --git a/tools/toollib.py b/tools/toollib.py deleted file mode 100644 index 77d864f142..0000000000 --- a/tools/toollib.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -"""Various utilities common to IPython release and maintenance tools. -""" - -from builtins import map - -# Library imports -import os -import sys - -from subprocess import Popen, PIPE, CalledProcessError, check_call - -from distutils.dir_util import remove_tree - -# Useful shorthands -pjoin = os.path.join -cd = os.chdir - -# Utility functions - -# ----------------------------------------------------------------------------- -# Functions -# ----------------------------------------------------------------------------- - - -def sh(cmd): - """Execute command in a subshell, return status code.""" - return check_call(cmd, shell=True) - - -def compile_tree(): - """Compile all Python files below current directory.""" - vstr = ".".join(map(str, sys.version_info[:2])) - stat = os.system("%s %s/lib/python%s/compileall.py ." % (sys.executable, sys.prefix, vstr)) - if stat: - msg = "*** ERROR: Some Python files in tree do NOT compile! ***\n" - msg += "See messages above for the actual file that produced it.\n" - raise SystemExit(msg)