Skip to content

MNT: Drop distutils #3458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion nipype/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
224 changes: 224 additions & 0 deletions nipype/external/version.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions nipype/interfaces/afni/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions nipype/interfaces/dipy/preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion nipype/interfaces/dipy/reconstruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion nipype/interfaces/dipy/registration.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion nipype/interfaces/dipy/stats.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion nipype/interfaces/dipy/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions nipype/interfaces/mrtrix3/connectivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 7 additions & 7 deletions nipype/interfaces/niftyreg/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
8 changes: 2 additions & 6 deletions nipype/pipeline/engine/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import numpy as np

from ... import logging, config, LooseVersion
from ... import logging, config
from ...utils.filemanip import (
indirectory,
relpath,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion nipype/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions nipype/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Loading