diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 10c1c88da120f620f30ad21ea131693453c51544..ebbb4a14792901da84824e7c12480a8cb3ee9cec 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,18 +11,21 @@ Changed ^^^^^^^ * The ``fslpy`` API ocumentation is now hosted at - https://open.win.ox.ac.uk/fsl/fslpy + https://open.win.ox.ac.uk/fsl/fslpy (!290). * The :mod:`fsl` and :mod:`fsl.scripts` packages have been changed from being `pkgutil-style <https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages>`_ namespace packages to now being `native <https://packaging.python.org/guides/packaging-namespace-packages/#native-namespace-packages>`_ - namespace packages. + namespace packages (!290). * The :class:`.TaskThread` now allows an error handler function to be - specified, which is run on the :mod:`.idle` loop. + specified, which is run on the :mod:`.idle` loop (!283). * The :func:`.bids.loadMetadata` function no long resolves sym-links when - determining whether a file is contained within a BIDS data set. - + determining whether a file is contained within a BIDS data set (!287). +* The :class:`.Image` class can now be created from a ``pathlib.Path`` object + (!292). +* Some functions in the :mod:`.path` module can now be used with + ``pathlib.Path`` objects (!293). Deprecated ^^^^^^^^^^ @@ -32,17 +35,20 @@ Deprecated :mod:`fsl.utils.platform` module, including ``frozen``, ``haveGui``, ``canHaveGui``, ``inSSHSession``, ``inVNCSession``, ``wxPlatform``, ``wxFlavour``, ``glVersion``, ``glRenderer``, and ``glIsSoftwareRenderer``. - Equivalent functions are being added to the ``fsleyes-widgets`` library. + Equivalent functions are being added to the ``fsleyes-widgets`` library + (!285). * The :mod:`fsl.utils.filetree` package has been deprecated, and will be removed in a future version of ``fslpy`` - it is now published as a separate - library on [PyPI](https://pypi.org/project/file-tree/). + library on [PyPI](https://pypi.org/project/file-tree/) (!286). Fixed ^^^^^ * Fixed an edge-case in the :mod:`.gifti` module, where a surface with a - single triangle was being loaded incorrectly. + single triangle was being loaded incorrectly (!288). +* Fixed an issue in the :func:`.func_to_cmd` function, where it was + unintentionally leaving flie handles open (!291). diff --git a/README.rst b/README.rst index f8b8fd8a118d8b0ee4e3070735c2d56b6d3e7b56..06df04918be7c407e33517f1466553048e2c5edc 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ programming library written in Python. It is used by `FSLeyes <https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/>`_. -``fslpy`` is tested against Python versions 3.6, 3.7, 3.8 and 3.9. +``fslpy`` is tested against Python versions 3.7, 3.8 and 3.9. Installation diff --git a/fsl/data/bitmap.py b/fsl/data/bitmap.py index 51b354f7ddfcef93cb44a2d2a83231b518d89fe2..4562300a794357d6382a3317a34b277f62d2b816 100644 --- a/fsl/data/bitmap.py +++ b/fsl/data/bitmap.py @@ -9,13 +9,13 @@ files. Pillow is required to use the ``Bitmap`` class. """ -import os.path as op -import logging -import six +import os.path as op +import pathlib +import logging -import numpy as np +import numpy as np -from . import image as fslimage +import fsl.data.image as fslimage log = logging.getLogger(__name__) @@ -51,7 +51,7 @@ class Bitmap(object): data. """ - if isinstance(bmp, six.string_types): + if isinstance(bmp, (pathlib.Path, str)): try: # Allow big images @@ -61,7 +61,7 @@ class Bitmap(object): except ImportError: raise RuntimeError('Install Pillow to use the Bitmap class') - src = bmp + src = str(bmp) img = Image.open(src) # If this is a palette/LUT diff --git a/fsl/data/image.py b/fsl/data/image.py index 3dfe8a818286126c98939d83a3979f2f363cb375..b5d0cad72d12850aecbeeab7ac072aca9f9d3485 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -32,22 +32,21 @@ and file names: """ -import os -import os.path as op -import itertools as it -import json -import string -import logging -import tempfile - -import six -import numpy as np +import os +import os.path as op +import itertools as it +import json +import string +import logging +import tempfile + +from pathlib import Path +from typing import Union +import numpy as np import nibabel as nib import nibabel.fileslice as fileslice -from pathlib import Path - import fsl.utils.meta as meta import fsl.transform.affine as affine import fsl.utils.notifier as notifier @@ -58,6 +57,10 @@ import fsl.data.constants as constants import fsl.data.imagewrapper as imagewrapper +PathLike = Union[str, Path] +ImageSource = Union[PathLike, nib.Nifti1Image, np.ndarray, 'Image'] + + log = logging.getLogger(__name__) @@ -993,21 +996,21 @@ class Image(Nifti): def __init__(self, - image, - name=None, - header=None, - xform=None, - loadData=True, - calcRange=True, - threaded=False, - dataSource=None, - loadMeta=False, + image : ImageSource, + name : str = None, + header : nib.Nifti1Header = None, + xform : np.ndarray = None, + loadData : bool = True, + calcRange : bool = True, + threaded : bool = False, + dataSource : PathLike = None, + loadMeta : bool = False, **kwargs): """Create an ``Image`` object with the given image data or file name. :arg image: A string containing the name of an image file to load, - or a Path object pointing to an image file, or a - :mod:`numpy` array, or a :mod:`nibabel` image object, + or a Path object pointing to an image file, or a + :mod:`numpy` array, or a :mod:`nibabel` image object, or an ``Image`` object. :arg name: A name for the image. @@ -1086,16 +1089,11 @@ class Image(Nifti): header.set_qform(xform, code=qform) # The image parameter may be the name of an image file - if isinstance(image, six.string_types): + if isinstance(image, (str, Path)): image = op.abspath(addExt(image)) nibImage = nib.load(image, **kwargs) dataSource = image saved = True - # The image parameter may be a Path object pointing to an image file - elif isinstance(image, Path): - nibImage = nib.load(image, **kwargs) - dataSource = str(image) - saved = True # Or a numpy array - we wrap it in a nibabel image, # with an identity transformation (each voxel maps @@ -1141,15 +1139,13 @@ class Image(Nifti): nibImage = image # Figure out the name of this image, if - # it has not beenbeen explicitly passed in + # it has not been explicitly passed in if name is None: # If this image was loaded # from disk, use the file name. - if isinstance(image, six.string_types): + if isinstance(image, (str, Path)): name = removeExt(op.basename(image)) - elif isinstance(image, Path): - name = image.name # Or the image was created from a numpy array elif isinstance(image, np.ndarray): diff --git a/fsl/data/mghimage.py b/fsl/data/mghimage.py index 38f854446d3333cd7f150bd082d80d76df6cc236..8464d3d2ce674e7c07fc7d7b03b3c5b2f4cdd898 100644 --- a/fsl/data/mghimage.py +++ b/fsl/data/mghimage.py @@ -10,8 +10,8 @@ Freesurfer ``mgh``/``mgz`` image files. import os.path as op +import pathlib -import six import numpy as np import nibabel as nib @@ -47,7 +47,7 @@ class MGHImage(fslimage.Image): All other arguments are passed through to :meth:`Image.__init__` """ - if isinstance(image, six.string_types): + if isinstance(image, (str, pathlib.Path)): filename = op.abspath(image) name = op.basename(filename) image = nib.load(image) diff --git a/fsl/utils/ensure.py b/fsl/utils/ensure.py index a2fd8d4913dc921f541a1e8430933b72bec43b12..3e3ea8e92e1a2ce570a53c3898521af38c1af0ef 100644 --- a/fsl/utils/ensure.py +++ b/fsl/utils/ensure.py @@ -14,8 +14,6 @@ that some condition is met. """ -import six - import nibabel as nib import fsl.data.image as fslimage @@ -24,7 +22,7 @@ import fsl.data.image as fslimage def ensureIsImage(img): """Ensures that the given ``img`` is an in-memory ``nibabel`` object. """ - if isinstance(img, six.string_types): + if isinstance(img, str): img = fslimage.addExt(img) img = nib.load(img) return img diff --git a/fsl/utils/fslsub.py b/fsl/utils/fslsub.py index 4a09f0828cb0bea65305c1e4272437fc4094b57f..22e9de1ece00c4d7a62238f6da0c4193274dac88 100644 --- a/fsl/utils/fslsub.py +++ b/fsl/utils/fslsub.py @@ -37,7 +37,7 @@ Example usage, building a short pipeline:: """ -from six import BytesIO +from io import BytesIO import os.path as op import glob import time @@ -439,9 +439,9 @@ _external_job = ("""#!{} # This is a temporary file designed to run the python function {}, # so that it can be submitted to the cluster import pickle -from six import BytesIO +from io import BytesIO from importlib import import_module -{} +{} pickle_bytes = BytesIO({}) name_type, name, func_name, args, kwargs = pickle.load(pickle_bytes) @@ -455,7 +455,7 @@ elif name_type == 'script': func = local_execute[func_name] else: raise ValueError('Unknown name_type: %r' % name_type) - + {} """) diff --git a/fsl/utils/memoize.py b/fsl/utils/memoize.py index b777eed0c945a048858806107487bf214d710984..b7ee159c2d891db915e92d0bf37929ff3b5a7870 100644 --- a/fsl/utils/memoize.py +++ b/fsl/utils/memoize.py @@ -21,7 +21,6 @@ a function: import logging import hashlib import functools -import six log = logging.getLogger(__name__) @@ -171,7 +170,7 @@ def memoizeMD5(func): # compatible) bytes , and take # the hash of those bytes. for arg in args: - if not isinstance(arg, six.string_types): + if not isinstance(arg, str): arg = str(arg) arg = arg.encode('utf-8') hashobj.update(arg) diff --git a/fsl/utils/notifier.py b/fsl/utils/notifier.py index cd07204e4907175c8f8a5bf3bfa21ebd7a5d2fd9..b294f6192ce01261275aed1f07213d3d0cb7a01d 100644 --- a/fsl/utils/notifier.py +++ b/fsl/utils/notifier.py @@ -14,9 +14,6 @@ import inspect import contextlib import collections -import six - - import fsl.utils.idle as idle import fsl.utils.weakfuncref as weakfuncref @@ -297,7 +294,7 @@ class Notifier(object): :arg topic: Topic or topics that the listener is registered on. """ - if topic is None or isinstance(topic, six.string_types): + if topic is None or isinstance(topic, str): topic = [topic] topics = topic diff --git a/fsl/utils/path.py b/fsl/utils/path.py index 0a0859528945a7b944482442b0ffc658d0dc05f9..09546f4b2cdf2660e234e49a7b09814232fac83e 100644 --- a/fsl/utils/path.py +++ b/fsl/utils/path.py @@ -32,16 +32,21 @@ import os.path as op import os import glob import operator +import pathlib import re +from typing import Sequence, Tuple, Union + from fsl.utils.platform import platform +PathLike = Union[str, pathlib.Path] + + class PathError(Exception): """``Exception`` class raised by the functions defined in this module when something goes wrong. """ - pass def deepest(path, suffixes): @@ -52,12 +57,12 @@ def deepest(path, suffixes): path = path.strip() - if path == op.sep or path == '': + if path in (op.sep, ''): return None path = path.rstrip(op.sep) - if any([path.endswith(s) for s in suffixes]): + if any(path.endswith(s) for s in suffixes): return path return deepest(op.dirname(path), suffixes) @@ -81,7 +86,7 @@ def shallowest(path, suffixes): if parent is not None: return parent - if any([path.endswith(s) for s in suffixes]): + if any(path.endswith(s) for s in suffixes): return path return None @@ -101,19 +106,23 @@ def allFiles(root): return files -def hasExt(path, allowedExts): +def hasExt(path : PathLike, + allowedExts : Sequence[str]) -> bool: """Convenience function which returns ``True`` if the given ``path`` ends with any of the given ``allowedExts``, ``False`` otherwise. """ - return any([path.endswith(e) for e in allowedExts]) - - -def addExt(prefix, - allowedExts=None, - mustExist=True, - defaultExt=None, - fileGroups=None, - unambiguous=True): + path = str(path) + return any(path.endswith(e) for e in allowedExts) + + +def addExt( + prefix : PathLike, + allowedExts : Sequence[str] = None, + mustExist : bool = True, + defaultExt : str = None, + fileGroups : Sequence[Sequence[str]] = None, + unambiguous : bool = True +) -> Union[Sequence[str], str]: """Adds a file extension to the given file ``prefix``. If ``mustExist`` is False, and the file does not already have a @@ -148,6 +157,8 @@ def addExt(prefix, containing *all* matching files is returned. """ + prefix = str(prefix) + if allowedExts is None: allowedExts = [] if fileGroups is None: fileGroups = {} @@ -189,7 +200,8 @@ def addExt(prefix, # If ambiguity is ok, return # all matching paths - elif not unambiguous: + if not unambiguous: + return allPaths # Ambiguity is not ok! More than @@ -223,19 +235,29 @@ def addExt(prefix, return allPaths[0] -def removeExt(filename, allowedExts=None, firstDot=False): +def removeExt( + filename : PathLike, + allowedExts : Sequence[str] = None, + firstDot : bool = False +) -> str: """Returns the base name of the given file name. See :func:`splitExt`. """ - return splitExt(filename, allowedExts, firstDot)[0] -def getExt(filename, allowedExts=None, firstDot=False): +def getExt( + filename : PathLike, + allowedExts : Sequence[str] = None, + firstDot : bool = False +) -> str: """Returns the extension of the given file name. See :func:`splitExt`. """ - return splitExt(filename, allowedExts, firstDot)[1] -def splitExt(filename, allowedExts=None, firstDot=False): +def splitExt( + filename : PathLike, + allowedExts : Sequence[str] = None, + firstDot : bool = False +) -> Tuple[str, str]: """Returns the base name and the extension from the given file name. If ``allowedExts`` is ``None`` and ``firstDot`` is ``False``, this @@ -262,6 +284,8 @@ def splitExt(filename, allowedExts=None, firstDot=False): last period. Ignored if ``allowedExts`` is specified. """ + filename = str(filename) + # If allowedExts is not specified # we split on a period character if allowedExts is None: @@ -465,7 +489,7 @@ def removeDuplicates(paths, allowedExts=None, fileGroups=None): groupFiles = getFileGroup(path, allowedExts, fileGroups) - if not any([p in unique for p in groupFiles]): + if not any(p in unique for p in groupFiles): unique.append(groupFiles[0]) return unique @@ -492,14 +516,13 @@ def uniquePrefix(path): break # Should never happen if path is valid - elif len(hits) == 0 or idx >= len(filename) - 1: + if len(hits) == 0 or idx >= len(filename) - 1: raise PathError('No unique prefix for {}'.format(filename)) # Not unique - continue looping - else: - idx += 1 - prefix = prefix + filename[idx] - hits = [h for h in hits if h.startswith(prefix)] + idx += 1 + prefix = prefix + filename[idx] + hits = [h for h in hits if h.startswith(prefix)] return prefix @@ -525,54 +548,56 @@ def commonBase(paths): last = base - if all([p.startswith(base) for p in paths]): + if all(p.startswith(base) for p in paths): return base raise PathError('No common base') -def wslpath(winpath): - """ - Convert Windows path (or a command line argument containing a Windows path) - to the equivalent WSL path (e.g. ``c:\\Users`` -> ``/mnt/c/Users``). Also supports - paths in the form ``\\wsl$\\(distro)\\users\\...`` - - :param winpath: Command line argument which may (or may not) contain a Windows path. It is assumed to be - either of the form <windows path> or --<arg>=<windows path>. Note that we don't need to - handle --arg <windows path> or -a <windows path> since in these cases the argument - and the path will be parsed as separate entities. - :return: If ``winpath`` matches a Windows path, the converted argument (including the --<arg>= portion). - Otherwise returns ``winpath`` unchanged. +def wslpath(path): + """Convert Windows path (or a command line argument containing a Windows + path) to the equivalent WSL path (e.g. ``c:\\Users`` -> ``/mnt/c/Users``). + Also supports paths in the form ``\\wsl$\\(distro)\\users\\...`` + + :param winpath: Command line argument which may (or may not) contain a + Windows path. It is assumed to be either of the form + <windows path> or --<arg>=<windows path>. Note that we + don't need to handle --arg <windows path> or -a <windows + path> since in these cases the argument and the path will + be parsed as separate entities. + :return: If ``winpath`` matches a Windows path, the converted + argument (including the --<arg>= portion). Otherwise + returns ``winpath`` unchanged. + """ - match = re.match(r"^(--[\w-]+=)?\\\\wsl\$[\\\/][^\\^\/]+(.*)$", winpath) + match = re.match(r"^(--[\w-]+=)?\\\\wsl\$[\\\/][^\\^\/]+(.*)$", path) if match: arg, path = match.group(1, 2) if arg is None: arg = "" return arg + path.replace("\\", "/") - match = re.match(r"^(--[\w-]+=)?([a-zA-z]):(.+)$", winpath) + match = re.match(r"^(--[\w-]+=)?([a-zA-z]):(.+)$", path) if match: arg, drive, path = match.group(1, 2, 3) if arg is None: arg = "" return arg + "/mnt/" + drive.lower() + path.replace("\\", "/") - return winpath + return path -def winpath(wslpath): - """ - Convert a WSL-local filepath (for example ``/usr/local/fsl/``) into a path that can be used from - Windows. +def winpath(path): + """Convert a WSL-local filepath (for example ``/usr/local/fsl/``) into a + path that can be used from Windows. If ``self.fslwsl`` is ``False``, simply returns ``wslpath`` unmodified Otherwise, uses ``FSLDIR`` to deduce the WSL distro in use for FSL. - This requires WSL2 which supports the ``\\wsl$\`` network path. + This requires WSL2 which supports the ``\\wsl$\\`` network path. wslpath is assumed to be an absolute path. """ if not platform.fslwsl: - return wslpath + return path else: match = re.match(r"^\\\\wsl\$\\([^\\]+).*$", platform.fsldir) if match: @@ -581,6 +606,7 @@ def winpath(wslpath): distro = None if not distro: - raise RuntimeError("Could not identify WSL installation from FSLDIR (%s)" % platform.fsldir) + raise RuntimeError('Could not identify WSL installation from ' + 'FSLDIR (%s)' % platform.fsldir) - return "\\\\wsl$\\" + distro + wslpath.replace("/", "\\") + return "\\\\wsl$\\" + distro + path.replace("/", "\\") diff --git a/fsl/utils/run.py b/fsl/utils/run.py index 1e535c8a336de44f3a7d819f067417075eaa16d3..a7e4971baea5125e9723ce184d6a65dc5a58c0ce 100644 --- a/fsl/utils/run.py +++ b/fsl/utils/run.py @@ -30,8 +30,6 @@ import subprocess as sp import os.path as op import os -import six - from fsl.utils.platform import platform as fslplatform import fsl.utils.fslsub as fslsub import fsl.utils.tempdir as tempdir @@ -83,7 +81,7 @@ def prepareArgs(args): if len(args) == 1: # Argument was a command string - if isinstance(args[0], six.string_types): + if isinstance(args[0], str): args = shlex.split(args[0]) # Argument was an unpacked sequence diff --git a/fsl/utils/weakfuncref.py b/fsl/utils/weakfuncref.py index 37e118566e814da3c5421037377ae02496483ab0..6216c2dd5f3ed267c6cdf2643707db7865634c69 100644 --- a/fsl/utils/weakfuncref.py +++ b/fsl/utils/weakfuncref.py @@ -7,13 +7,12 @@ """This module provides the :class:`WeakFunctionRef` class. """ -import six import types import weakref import inspect -class WeakFunctionRef(object): +class WeakFunctionRef: """Class which encapsulates a :mod:`weakref` to a function or method. This class is used by :class:`.Notifier` instances to reference @@ -28,10 +27,10 @@ class WeakFunctionRef(object): """ # Bound method - if self.__isMethod(func): + if inspect.ismethod(func): - boundMeth = six.get_method_function(func) - boundSelf = six.get_method_self( func) + boundMeth = func.__func__ + boundSelf = func.__self__ # We can't take a weakref of the method # object, so we have to weakref the object @@ -73,35 +72,6 @@ class WeakFunctionRef(object): return self.__str__() - def __isMethod(self, func): - """Returns ``True`` if the given function is a bound method, - ``False`` otherwise. - - This seems to be one of the few areas where python 2 and 3 are - irreconcilably incompatible (or just where :mod:`six` does not have a - function to help us). - - In Python 3 there is no difference between an unbound method and a - function. But in Python 2, an unbound method is still a method (and - inspect.ismethod returns True). - """ - - ismethod = False - - # Therefore, in python2 we need to test - # whether the function is a method, and - # also test whether it is bound. - if six.PY2: - ismethod = (inspect.ismethod(func) and - six.get_method_self(func) is not None) - - # But in python3, if the function is a - # method it is, by definition, bound. - elif six.PY3: - ismethod = inspect.ismethod(func) - - return ismethod - def __findPrivateMethod(self): """Finds and returns the bound method associated with the encapsulated @@ -125,8 +95,7 @@ class WeakFunctionRef(object): att = getattr(obj, name) - if isinstance(att, types.MethodType) and \ - six.get_method_function(att) is func: + if isinstance(att, types.MethodType) and att.__func__ is func: return att return None diff --git a/fsl/wrappers/fast.py b/fsl/wrappers/fast.py index e18ec69effd6be4b6ef7cac023f7b1ae77a2414e..c51c9f245ad153018d1a76a81dd2d743f3b437c5 100644 --- a/fsl/wrappers/fast.py +++ b/fsl/wrappers/fast.py @@ -10,8 +10,6 @@ """ -import six - import fsl.utils.assertions as asrt from . import wrapperutils as wutils @@ -28,7 +26,7 @@ def fast(imgs, out='fast', **kwargs): command line option) """ - if isinstance(imgs, six.string_types): + if isinstance(imgs, str): imgs = [imgs] asrt.assertIsNifti(*imgs) diff --git a/fsl/wrappers/fsl_anat.py b/fsl/wrappers/fsl_anat.py index 868dacb70d206fe5a9b53907668cc7641513bf52..0b5e5e163ceb7b885bebf95e2bc714b13c12ebc0 100644 --- a/fsl/wrappers/fsl_anat.py +++ b/fsl/wrappers/fsl_anat.py @@ -8,8 +8,6 @@ `FSL_ANAT <https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/fsl_anat>`_ command. """ -import six - import fsl.utils.assertions as asrt from . import wrapperutils as wutils diff --git a/fsl/wrappers/wrapperutils.py b/fsl/wrappers/wrapperutils.py index 8739a1a34fea38b025a956326e23d11471f343e5..60413b866bee6100c3e9be198ac14abb42e221fd 100644 --- a/fsl/wrappers/wrapperutils.py +++ b/fsl/wrappers/wrapperutils.py @@ -103,8 +103,6 @@ import tempfile import warnings import functools - -import six import nibabel as nib import numpy as np @@ -346,8 +344,7 @@ def applyArgStyle(style, # always returns a sequence def fmtval(val): - if isinstance(val, abc.Sequence) and \ - not isinstance(val, six.string_types): + if isinstance(val, abc.Sequence) and (not isinstance(val, str)): val = [str(v) for v in val] if valsep == ' ': return val @@ -711,8 +708,7 @@ class FileOrThing(object): kwargs.get('cmdonly', False): allargs = {**dict(zip(argnames, args)), **kwargs} for name, val in allargs.items(): - if (name in self.__things) and \ - (not isinstance(val, six.string_types)): + if (name in self.__things) and (not isinstance(val, str)): raise ValueError('Cannot use in-memory objects ' 'or LOAD with submit=True!') return func(*args, **kwargs) diff --git a/requirements.txt b/requirements.txt index 23dc58fde626c364d58edc0b6ac7d76f077412b7..faf34174fc2bb63d9c44706feb097f8f2b619dee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,4 @@ h5py>=2.9 nibabel>=2.4 numpy>=1 scipy>=0.18 -six>=1 dataclasses diff --git a/setup.py b/setup.py index ebfa3afea50ef5e16cb9ccdeda8022dac84d3876..60c5d46c22b80ed27ad076a5e4ffe4c4b41c057e 100644 --- a/setup.py +++ b/setup.py @@ -8,8 +8,9 @@ from __future__ import print_function -import os.path as op -import shutil +import os.path as op +import shutil +import unittest.mock as mock from setuptools import setup from setuptools import find_namespace_packages @@ -68,11 +69,6 @@ class doc(Command): import sphinx.cmd.build as sphinx_build - try: - import unittest.mock as mock - except: - import mock - mockobj = mock.MagicMock() mockobj.__version__ = '2.2.0' mockedModules = open(op.join(docdir, 'mock_modules.txt')).readlines() @@ -104,6 +100,7 @@ setup( 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Software Development :: Libraries :: Python Modules'], packages=packages, diff --git a/tests/__init__.py b/tests/__init__.py index 24587392fc29de81bea199cb8e991b6012cba22e..58e6d23beda36a3c4cf2ce61046c54310173d5e8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -20,11 +20,9 @@ import os.path as op import numpy as np import nibabel as nib -from six import StringIO +from io import StringIO - -try: from unittest import mock -except ImportError: import mock +from unittest import mock import fsl.data.image as fslimage from fsl.utils.tempdir import tempdir diff --git a/tests/test_fsl_utils_path.py b/tests/test_fsl_utils_path.py index 0361ced5ee2843f4c6827e4bbcabd705251d6cdf..fbf95953682951961db2078bdc26f04b98d310c5 100644 --- a/tests/test_fsl_utils_path.py +++ b/tests/test_fsl_utils_path.py @@ -5,15 +5,15 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -from __future__ import print_function - import os import os.path as op import shutil +import pathlib import tempfile +from unittest import mock + import pytest -import mock import fsl.utils.path as fslpath import fsl.data.image as fslimage @@ -150,7 +150,8 @@ def test_hasExt(): ] for path, aexts, expected in tests: - assert fslpath.hasExt(path, aexts) == expected + assert fslpath.hasExt(path, aexts) == expected + assert fslpath.hasExt(pathlib.Path(path), aexts) == expected def test_addExt_imageFiles_mustExist_shouldPass(): @@ -248,18 +249,15 @@ def test_addExt_imageFiles_mustExist_shouldPass(): for f in files_to_create: make_dummy_image_file(op.join(workdir, f)) - print('files_to_create: ', files_to_create) - print('workdir: ', os.listdir(workdir)) - print('prefix: ', prefix) - print('expected: ', expected) - - result = fslpath.addExt(op.join(workdir, prefix), - allowedExts, - mustExist=True, - fileGroups=groups) - - print('result: ', result) - + result = fslpath.addExt(op.join(workdir, prefix), + allowedExts, + mustExist=True, + fileGroups=groups) + assert result == op.join(workdir, expected) + result = fslpath.addExt(pathlib.Path(op.join(workdir, prefix)), + allowedExts, + mustExist=True, + fileGroups=groups) assert result == op.join(workdir, expected) cleardir(workdir) @@ -336,20 +334,15 @@ def test_addExt_otherFiles_mustExist_shouldPass(): for f in files_to_create: make_dummy_file(op.join(workdir, f)) - print('files_to_create: ', files_to_create) - print('prefix: ', prefix) - print('allowedExts: ', allowedExts) - print('fileGroups: ', fileGroups) - print('workdir: ', os.listdir(workdir)) - print('expected: ', expected) - - result = fslpath.addExt(op.join(workdir, prefix), - allowedExts=allowedExts, - mustExist=True, - fileGroups=fileGroups) - - print('result: ', result) - + result = fslpath.addExt(op.join(workdir, prefix), + allowedExts=allowedExts, + mustExist=True, + fileGroups=fileGroups) + assert result == op.join(workdir, expected) + result = fslpath.addExt(pathlib.Path(op.join(workdir, prefix)), + allowedExts=allowedExts, + mustExist=True, + fileGroups=fileGroups) assert result == op.join(workdir, expected) cleardir(workdir) @@ -422,18 +415,16 @@ def test_addExt_imageFiles_mustExist_shouldFail(): for f in files_to_create: make_dummy_file(op.join(workdir, f)) - print('files_to_create: ', files_to_create) - print('prefix: ', prefix) - print('workdir: ', os.listdir(workdir)) - with pytest.raises(fslpath.PathError): - - result = fslpath.addExt(op.join(workdir, prefix), - allowedExts=allowedExts, - mustExist=True, - fileGroups=fileGroups) - - print('result: ', result) + fslpath.addExt(op.join(workdir, prefix), + allowedExts=allowedExts, + mustExist=True, + fileGroups=fileGroups) + with pytest.raises(fslpath.PathError): + fslpath.addExt(pathlib.Path(op.join(workdir, prefix)), + allowedExts=allowedExts, + mustExist=True, + fileGroups=fileGroups) finally: shutil.rmtree(workdir) @@ -481,23 +472,19 @@ def test_addExt_otherFiles_mustExist_shouldFail(): for f in files_to_create: make_dummy_file(op.join(workdir, f)) - print('files_to_create: ', files_to_create) - print('prefix: ', prefix) - print('workdir: ', os.listdir(workdir)) - with pytest.raises(fslpath.PathError): - - result = fslpath.addExt(op.join(workdir, prefix), - allowedExts=allowedExts, - mustExist=True, - fileGroups=fileGroups) - - print('result: ', result) - + fslpath.addExt(op.join(workdir, prefix), + allowedExts=allowedExts, + mustExist=True, + fileGroups=fileGroups) + with pytest.raises(fslpath.PathError): + fslpath.addExt(pathlib.Path(op.join(workdir, prefix)), + allowedExts=allowedExts, + mustExist=True, + fileGroups=fileGroups) finally: shutil.rmtree(workdir) - pass def test_addExt_noExist(): @@ -544,11 +531,14 @@ def test_addExt_noExist(): ] for prefix, defaultExt, allowedExts, expected in tests: - assert fslpath.addExt(prefix, allowedExts, defaultExt=defaultExt, mustExist=False) == expected + assert fslpath.addExt(pathlib.Path(prefix), + allowedExts, + defaultExt=defaultExt, + mustExist=False) == expected def test_addExt_unambiguous(): @@ -582,15 +572,20 @@ def test_addExt_unambiguous(): expected = expected.split() with testdir(create) as td: - result = fslpath.addExt(prefix, allowedExts=exts, fileGroups=groups, defaultExt=defaultExt, unambiguous=False) - + assert sorted(expected) == sorted(result) + result = fslpath.addExt(pathlib.Path(prefix), + allowedExts=exts, + fileGroups=groups, + defaultExt=defaultExt, + unambiguous=False) assert sorted(expected) == sorted(result) + def test_removeExt(): allowedExts = fslimage.ALLOWED_EXTENSIONS @@ -622,7 +617,8 @@ def test_removeExt(): if len(test) == 2: allowed = allowedExts else: allowed = test[2] - assert fslpath.removeExt(path, allowed) == output + assert fslpath.removeExt(path, allowed) == output + assert fslpath.removeExt(pathlib.Path(path), allowed) == output def test_getExt(): @@ -658,8 +654,8 @@ def test_getExt(): if len(test) == 2: allowed = allowedExts else: allowed = test[2] - print(filename, '==', output) - assert fslpath.getExt(filename, allowed) == output + assert fslpath.getExt(filename, allowed) == output + assert fslpath.getExt(pathlib.Path(filename), allowed) == output def test_splitExt(): @@ -704,13 +700,14 @@ def test_splitExt(): for test in tests: filename = test[0] + pfilename = pathlib.Path(filename) outbase, outext = test[1] if len(test) == 2: allowed = allowedExts else: allowed = test[2] - print(filename, '==', (outbase, outext)) - assert fslpath.splitExt(filename, allowed) == (outbase, outext) + assert fslpath.splitExt(filename, allowed) == (outbase, outext) + assert fslpath.splitExt(pfilename, allowed) == (outbase, outext) # firstDot=True tests = [ @@ -721,7 +718,9 @@ def test_splitExt(): ] for f, exp in tests: - assert fslpath.splitExt(f, firstDot=True) == exp + pf = pathlib.Path(f) + assert fslpath.splitExt(f, firstDot=True) == exp + assert fslpath.splitExt(pf, firstDot=True) == exp def test_getFileGroup_imageFiles_shouldPass(): @@ -798,11 +797,6 @@ def test_getFileGroup_imageFiles_shouldPass(): with open(op.join(workdir, fn), 'wt') as f: f.write('{}\n'.format(fn)) - print() - print('files_to_create: ', files_to_create) - print('path: ', path) - print('files_to_expect: ', files_to_expect) - fullPaths = fslpath.getFileGroup( op.join(workdir, path), allowedExts=allowedExts, @@ -918,13 +912,6 @@ def test_getFileGroup_otherFiles_shouldPass(): with open(op.join(workdir, fn), 'wt') as f: f.write('{}\n'.format(fn)) - print() - print('files_to_create: ', files_to_create) - print('path: ', path) - print('allowedExts: ', allowedExts) - print('fileGroups: ', fileGroups) - print('files_to_expect: ', files_to_expect) - fullPaths = fslpath.getFileGroup( op.join(workdir, path), allowedExts=allowedExts, @@ -1006,32 +993,22 @@ def test_getFileGroup_shouldFail(): with open(op.join(workdir, fn), 'wt') as f: f.write('{}\n'.format(fn)) - print() - print('files_to_create: ', files_to_create) - print('path: ', path) - print('allowedExts: ', allowedExts) - print('fileGroups: ', fileGroups) - with pytest.raises(fslpath.PathError): - fullPaths = fslpath.getFileGroup( + fslpath.getFileGroup( op.join(workdir, path), allowedExts=allowedExts, fileGroups=fileGroups, fullPaths=True, unambiguous=unambiguous) - print('fullPaths: ', fullPaths) - with pytest.raises(fslpath.PathError): - exts = fslpath.getFileGroup( + fslpath.getFileGroup( op.join(workdir, path), allowedExts=allowedExts, fileGroups=fileGroups, fullPaths=False, unambiguous=unambiguous) - print('exts: ', exts) - cleardir(workdir) finally: @@ -1117,16 +1094,9 @@ def test_removeDuplicates_imageFiles_shouldPass(): paths = paths.split() expected = expected.split() - print() - print('files_to_create: ', files_to_create) - print('paths: ', paths) - print('expected: ', expected) - paths = [op.join(workdir, p) for p in paths] result = fslpath.removeDuplicates(paths, allowedExts, groups) - print('result: ', result) - assert result == [op.join(workdir, e) for e in expected] cleardir(workdir) @@ -1196,20 +1166,10 @@ def test_removeDuplicates_otherFiles_shouldPass(): for f in files_to_create: make_dummy_file(op.join(workdir, f)) - - print('files_to_create: {}'.format(files_to_create)) - print('paths: {}'.format(paths)) - print('allowedExts: {}'.format(allowedExts)) - print('fileGroups: {}'.format(fileGroups)) - print('workdir: {}'.format(os.listdir(workdir))) - print('expected: {}'.format(expected)) - result = fslpath.removeDuplicates([op.join(workdir, p) for p in paths], allowedExts=allowedExts, fileGroups=fileGroups) - print('result: {}'.format(result)) - assert result == [op.join(workdir, e) for e in expected] cleardir(workdir) @@ -1256,17 +1216,10 @@ def test_removeDuplicates_shouldFail(): with open(op.join(workdir, fn), 'wt') as f: f.write('{}\n'.format(fn)) - print() - print('files_to_create: ', files_to_create) - print('path: ', path) - print('allowedExts: ', allowedExts) - print('fileGroups: ', fileGroups) - with pytest.raises(fslpath.PathError): - result = fslpath.removeDuplicates(path, - allowedExts=allowedExts, - fileGroups=fileGroups) - print('result: ', result) + fslpath.removeDuplicates(path, + allowedExts=allowedExts, + fileGroups=fileGroups) finally: shutil.rmtree(workdir) diff --git a/tests/test_idle.py b/tests/test_idle.py index edf0ecb171b54fe382d233c5c69a49ae77b57f1c..6ceb010bb91aa36ce642d1b3034f5655d7bc58c8 100644 --- a/tests/test_idle.py +++ b/tests/test_idle.py @@ -9,11 +9,10 @@ import gc import time import threading import random - -from six.moves import reload_module +import importlib import pytest -import mock +from unittest import mock import fsl.utils.idle as idle from fsl.utils.platform import platform as fslplatform @@ -428,7 +427,7 @@ def test_idle_alwaysQueue4(): with pytest.raises(ImportError): import wx - reload_module(fsl.utils.platform) + importlib.reload(fsl.utils.platform) assert called[0] diff --git a/tests/test_image.py b/tests/test_image.py index e2e7f213f0d7c791737c62ceb9eaa57a75f42344..34a5482ce8f63a7d66ed83d881f9a575680a55ab 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -32,11 +32,7 @@ from fsl.utils.tempdir import tempdir from . import make_random_image from . import make_dummy_file -try: - from unittest import mock -except ImportError: - import mock - +from unittest import mock try: import indexed_gzip as igzip @@ -208,6 +204,23 @@ def test_create(): assert np.all(np.isclose(img.pixdim, (2, 3, 4))) +def test_name_dataSource(): + with tempdir(): + + expName = 'image' + expDataSource = op.abspath('image.nii.gz') + make_image('image.nii.gz') + + tests = ['image', 'image.nii.gz', op.abspath('image'), + op.abspath('image.nii.gz')] + tests = tests + [Path(t) for t in tests] + + for t in tests: + i = fslimage.Image(t) + assert i.name == expName + assert i.dataSource == expDataSource + + def test_bad_create(): class BadThing(object): diff --git a/tests/test_image_advanced.py b/tests/test_image_advanced.py index fd7f64359d9bc071b33689ff95a8697170c247d0..1f1aed8a2ba827836314f61709e3a0316e41616f 100644 --- a/tests/test_image_advanced.py +++ b/tests/test_image_advanced.py @@ -7,8 +7,8 @@ import os.path as op import time +from unittest import mock -import mock import pytest import numpy as np diff --git a/tests/test_memoize.py b/tests/test_memoize.py index 386674659b4a9af2648dadf445efd1a84a170f37..30844884d4695c040e82c11af2db7295118d1a17 100644 --- a/tests/test_memoize.py +++ b/tests/test_memoize.py @@ -6,7 +6,6 @@ # import collections -import six import numpy as np @@ -44,7 +43,7 @@ def test_memoize(): assert timesCalled[0] == 6 # Unicode arg - s = six.u('\u25B2') + s = '\u25B2' assert memoized(s) == s * 5 assert timesCalled[0] == 7 assert memoized(s) == s * 5 @@ -146,7 +145,7 @@ def test_memoizeMD5(): assert timesCalled[0] == 6 # Unicode arg (and return value) - s = six.u('\u25B2') + s = '\u25B2' assert memoized(s) == s * 5 assert timesCalled[0] == 7 assert memoized(s) == s * 5 diff --git a/tests/test_platform.py b/tests/test_platform.py index 9024bec8262658dd419c48ef288f2d4af2a75c8c..a38408175e438447570a75b9c0115e9da3f1d927 100644 --- a/tests/test_platform.py +++ b/tests/test_platform.py @@ -6,15 +6,14 @@ # -import os -import gc -import os.path as op -import sys -import shutil -import tempfile -import pytest - -import mock +import os +import gc +import os.path as op +import sys +import shutil +import tempfile +import pytest +from unittest import mock import fsl.utils.platform as fslplatform @@ -216,7 +215,7 @@ def test_detect_ssh(): def test_fslwsl(): """ Note that ``Platform.fsldir`` requires the directory in ``FSLDIR`` to exist and - sets ``FSLDIR`` to ``None`` if it doesn't. So we create a ``Platform`` first + sets ``FSLDIR`` to ``None`` if it doesn't. So we create a ``Platform`` first and then overwrite ``FSLDIR``. This is a bit of a hack but the logic we are testing here is whether ``Platform.fslwsl`` recognizes a WSL ``FSLDIR`` string """ diff --git a/tests/test_run.py b/tests/test_run.py index 7308b849c72f6f82afd00eb417c4d479b58d0f30..287ccd6432c4d98223f23712dda1da2c1bf668c2 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -11,12 +11,8 @@ import os import shutil import textwrap -# python 3 -try: from unittest import mock -# python 2 -except ImportError: import mock +from unittest import mock -import six import pytest import fsl.utils.tempdir as tempdir @@ -273,7 +269,7 @@ def test_runfsl(): def mock_submit(cmd, **kwargs): - if isinstance(cmd, six.string_types): + if isinstance(cmd, str): name = cmd.split()[0] else: name = cmd[0] diff --git a/tests/test_settings.py b/tests/test_settings.py index b17b1b87ab8e7ed747d2c2d0dd56d4e13053737a..fc0ca9edf7f90c0f5051d09c8e30a59319aae6c4 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -12,12 +12,7 @@ import pickle import textwrap import tempfile -# python 3 -try: - import unittest.mock as mock -# python 2 -except: - import mock +import unittest.mock as mock import pytest diff --git a/tests/test_transform/test_affine.py b/tests/test_transform/test_affine.py index f1dceb61312c7f05f97c9d871dc8a4a0ae95f492..e686080fc7040983b61047ce207f8d0fcb1966b2 100644 --- a/tests/test_transform/test_affine.py +++ b/tests/test_transform/test_affine.py @@ -6,8 +6,6 @@ # -from __future__ import division - import random import glob import os.path as op @@ -15,8 +13,6 @@ import itertools as it import numpy as np import numpy.linalg as npla -import six - import pytest import fsl.transform.affine as affine @@ -39,8 +35,7 @@ def readlines(filename): # # Pass it [bytes, bytes, ...], and it works # fine. - if six.PY3: - lines = [l.encode('ascii') for l in lines] + lines = [l.encode('ascii') for l in lines] return lines diff --git a/tests/test_wrappers/test_wrapperutils.py b/tests/test_wrappers/test_wrapperutils.py index 9bddb5c6b97e7ba6b9b78690c579fc3011aada81..f8a9d0dd4baa98f47003bf1846a92652f8e270bd 100644 --- a/tests/test_wrappers/test_wrapperutils.py +++ b/tests/test_wrappers/test_wrapperutils.py @@ -11,10 +11,8 @@ import shlex import pathlib import textwrap -try: from unittest import mock -except ImportError: import mock +from unittest import mock -import six import pytest import numpy as np @@ -317,7 +315,7 @@ def test_fileOrThing_sequence(): @wutils.fileOrArray('arrs', 'out') def func(arrs, out): - if isinstance(arrs, six.string_types): + if isinstance(arrs, str): arrs = [arrs] arrs = [np.loadtxt(a) for a in arrs]