Commit b5e6a9ed authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

Merge branch 'mnt/3.6.0-prep' into 'master'

Mnt/3.6.0 prep

See merge request fsl/fslpy!293
parents f53bb6f8 a18b60cb
...@@ -11,18 +11,21 @@ Changed ...@@ -11,18 +11,21 @@ Changed
^^^^^^^ ^^^^^^^
* The ``fslpy`` API ocumentation is now hosted at * 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 * The :mod:`fsl` and :mod:`fsl.scripts` packages have been changed from being
`pkgutil-style `pkgutil-style
<https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages>`_ <https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages>`_
namespace packages to now being `native namespace packages to now being `native
<https://packaging.python.org/guides/packaging-namespace-packages/#native-namespace-packages>`_ <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 * 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 * 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 Deprecated
^^^^^^^^^^ ^^^^^^^^^^
...@@ -32,17 +35,20 @@ Deprecated ...@@ -32,17 +35,20 @@ Deprecated
:mod:`fsl.utils.platform` module, including ``frozen``, ``haveGui``, :mod:`fsl.utils.platform` module, including ``frozen``, ``haveGui``,
``canHaveGui``, ``inSSHSession``, ``inVNCSession``, ``wxPlatform``, ``canHaveGui``, ``inSSHSession``, ``inVNCSession``, ``wxPlatform``,
``wxFlavour``, ``glVersion``, ``glRenderer``, and ``glIsSoftwareRenderer``. ``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 * 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 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
^^^^^ ^^^^^
* Fixed an edge-case in the :mod:`.gifti` module, where a surface with a * 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).
......
...@@ -19,7 +19,7 @@ programming library written in Python. It is used by `FSLeyes ...@@ -19,7 +19,7 @@ programming library written in Python. It is used by `FSLeyes
<https://git.fmrib.ox.ac.uk/fsl/fsleyes/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 Installation
......
...@@ -9,13 +9,13 @@ files. Pillow is required to use the ``Bitmap`` class. ...@@ -9,13 +9,13 @@ files. Pillow is required to use the ``Bitmap`` class.
""" """
import os.path as op import os.path as op
import logging import pathlib
import six 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__) log = logging.getLogger(__name__)
...@@ -51,7 +51,7 @@ class Bitmap(object): ...@@ -51,7 +51,7 @@ class Bitmap(object):
data. data.
""" """
if isinstance(bmp, six.string_types): if isinstance(bmp, (pathlib.Path, str)):
try: try:
# Allow big images # Allow big images
...@@ -61,7 +61,7 @@ class Bitmap(object): ...@@ -61,7 +61,7 @@ class Bitmap(object):
except ImportError: except ImportError:
raise RuntimeError('Install Pillow to use the Bitmap class') raise RuntimeError('Install Pillow to use the Bitmap class')
src = bmp src = str(bmp)
img = Image.open(src) img = Image.open(src)
# If this is a palette/LUT # If this is a palette/LUT
......
...@@ -32,22 +32,21 @@ and file names: ...@@ -32,22 +32,21 @@ and file names:
""" """
import os import os
import os.path as op import os.path as op
import itertools as it import itertools as it
import json import json
import string import string
import logging import logging
import tempfile import tempfile
import six from pathlib import Path
import numpy as np from typing import Union
import numpy as np
import nibabel as nib import nibabel as nib
import nibabel.fileslice as fileslice import nibabel.fileslice as fileslice
from pathlib import Path
import fsl.utils.meta as meta import fsl.utils.meta as meta
import fsl.transform.affine as affine import fsl.transform.affine as affine
import fsl.utils.notifier as notifier import fsl.utils.notifier as notifier
...@@ -58,6 +57,10 @@ import fsl.data.constants as constants ...@@ -58,6 +57,10 @@ import fsl.data.constants as constants
import fsl.data.imagewrapper as imagewrapper import fsl.data.imagewrapper as imagewrapper
PathLike = Union[str, Path]
ImageSource = Union[PathLike, nib.Nifti1Image, np.ndarray, 'Image']
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -993,21 +996,21 @@ class Image(Nifti): ...@@ -993,21 +996,21 @@ class Image(Nifti):
def __init__(self, def __init__(self,
image, image : ImageSource,
name=None, name : str = None,
header=None, header : nib.Nifti1Header = None,
xform=None, xform : np.ndarray = None,
loadData=True, loadData : bool = True,
calcRange=True, calcRange : bool = True,
threaded=False, threaded : bool = False,
dataSource=None, dataSource : PathLike = None,
loadMeta=False, loadMeta : bool = False,
**kwargs): **kwargs):
"""Create an ``Image`` object with the given image data or file name. """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, :arg image: A string containing the name of an image file to load,
or a Path object pointing to an image file, or a or a Path object pointing to an image file, or a
:mod:`numpy` array, or a :mod:`nibabel` image object, :mod:`numpy` array, or a :mod:`nibabel` image object,
or an ``Image`` object. or an ``Image`` object.
:arg name: A name for the image. :arg name: A name for the image.
...@@ -1086,16 +1089,11 @@ class Image(Nifti): ...@@ -1086,16 +1089,11 @@ class Image(Nifti):
header.set_qform(xform, code=qform) header.set_qform(xform, code=qform)
# The image parameter may be the name of an image file # 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)) image = op.abspath(addExt(image))
nibImage = nib.load(image, **kwargs) nibImage = nib.load(image, **kwargs)
dataSource = image dataSource = image
saved = True 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, # Or a numpy array - we wrap it in a nibabel image,
# with an identity transformation (each voxel maps # with an identity transformation (each voxel maps
...@@ -1141,15 +1139,13 @@ class Image(Nifti): ...@@ -1141,15 +1139,13 @@ class Image(Nifti):
nibImage = image nibImage = image
# Figure out the name of this image, if # 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 name is None:
# If this image was loaded # If this image was loaded
# from disk, use the file name. # from disk, use the file name.
if isinstance(image, six.string_types): if isinstance(image, (str, Path)):
name = removeExt(op.basename(image)) name = removeExt(op.basename(image))
elif isinstance(image, Path):
name = image.name
# Or the image was created from a numpy array # Or the image was created from a numpy array
elif isinstance(image, np.ndarray): elif isinstance(image, np.ndarray):
......
...@@ -10,8 +10,8 @@ Freesurfer ``mgh``/``mgz`` image files. ...@@ -10,8 +10,8 @@ Freesurfer ``mgh``/``mgz`` image files.
import os.path as op import os.path as op
import pathlib
import six
import numpy as np import numpy as np
import nibabel as nib import nibabel as nib
...@@ -47,7 +47,7 @@ class MGHImage(fslimage.Image): ...@@ -47,7 +47,7 @@ class MGHImage(fslimage.Image):
All other arguments are passed through to :meth:`Image.__init__` 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) filename = op.abspath(image)
name = op.basename(filename) name = op.basename(filename)
image = nib.load(image) image = nib.load(image)
......
...@@ -14,8 +14,6 @@ that some condition is met. ...@@ -14,8 +14,6 @@ that some condition is met.
""" """
import six
import nibabel as nib import nibabel as nib
import fsl.data.image as fslimage import fsl.data.image as fslimage
...@@ -24,7 +22,7 @@ import fsl.data.image as fslimage ...@@ -24,7 +22,7 @@ import fsl.data.image as fslimage
def ensureIsImage(img): def ensureIsImage(img):
"""Ensures that the given ``img`` is an in-memory ``nibabel`` object. """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 = fslimage.addExt(img)
img = nib.load(img) img = nib.load(img)
return img return img
...@@ -37,7 +37,7 @@ Example usage, building a short pipeline:: ...@@ -37,7 +37,7 @@ Example usage, building a short pipeline::
""" """
from six import BytesIO from io import BytesIO
import os.path as op import os.path as op
import glob import glob
import time import time
...@@ -439,9 +439,9 @@ _external_job = ("""#!{} ...@@ -439,9 +439,9 @@ _external_job = ("""#!{}
# This is a temporary file designed to run the python function {}, # This is a temporary file designed to run the python function {},
# so that it can be submitted to the cluster # so that it can be submitted to the cluster
import pickle import pickle
from six import BytesIO from io import BytesIO
from importlib import import_module from importlib import import_module
{} {}
pickle_bytes = BytesIO({}) pickle_bytes = BytesIO({})
name_type, name, func_name, args, kwargs = pickle.load(pickle_bytes) name_type, name, func_name, args, kwargs = pickle.load(pickle_bytes)
...@@ -455,7 +455,7 @@ elif name_type == 'script': ...@@ -455,7 +455,7 @@ elif name_type == 'script':
func = local_execute[func_name] func = local_execute[func_name]
else: else:
raise ValueError('Unknown name_type: %r' % name_type) raise ValueError('Unknown name_type: %r' % name_type)
{} {}
""") """)
......
...@@ -21,7 +21,6 @@ a function: ...@@ -21,7 +21,6 @@ a function:
import logging import logging
import hashlib import hashlib
import functools import functools
import six
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -171,7 +170,7 @@ def memoizeMD5(func): ...@@ -171,7 +170,7 @@ def memoizeMD5(func):
# compatible) bytes , and take # compatible) bytes , and take
# the hash of those bytes. # the hash of those bytes.
for arg in args: for arg in args:
if not isinstance(arg, six.string_types): if not isinstance(arg, str):
arg = str(arg) arg = str(arg)
arg = arg.encode('utf-8') arg = arg.encode('utf-8')
hashobj.update(arg) hashobj.update(arg)
......
...@@ -14,9 +14,6 @@ import inspect ...@@ -14,9 +14,6 @@ import inspect
import contextlib import contextlib
import collections import collections
import six
import fsl.utils.idle as idle import fsl.utils.idle as idle
import fsl.utils.weakfuncref as weakfuncref import fsl.utils.weakfuncref as weakfuncref
...@@ -297,7 +294,7 @@ class Notifier(object): ...@@ -297,7 +294,7 @@ class Notifier(object):
:arg topic: Topic or topics that the listener is registered on. :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] topic = [topic]
topics = topic topics = topic
......
...@@ -32,16 +32,21 @@ import os.path as op ...@@ -32,16 +32,21 @@ import os.path as op
import os import os
import glob import glob
import operator import operator
import pathlib
import re import re
from typing import Sequence, Tuple, Union
from fsl.utils.platform import platform from fsl.utils.platform import platform
PathLike = Union[str, pathlib.Path]
class PathError(Exception): class PathError(Exception):
"""``Exception`` class raised by the functions defined in this module """``Exception`` class raised by the functions defined in this module
when something goes wrong. when something goes wrong.
""" """
pass
def deepest(path, suffixes): def deepest(path, suffixes):
...@@ -52,12 +57,12 @@ def deepest(path, suffixes): ...@@ -52,12 +57,12 @@ def deepest(path, suffixes):
path = path.strip() path = path.strip()
if path == op.sep or path == '': if path in (op.sep, ''):
return None return None
path = path.rstrip(op.sep) 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 path
return deepest(op.dirname(path), suffixes) return deepest(op.dirname(path), suffixes)
...@@ -81,7 +86,7 @@ def shallowest(path, suffixes): ...@@ -81,7 +86,7 @@ def shallowest(path, suffixes):
if parent is not None: if parent is not None:
return parent return parent
if any([path.endswith(s) for s in suffixes]): if any(path.endswith(s) for s in suffixes):
return path return path
return None return None
...@@ -101,19 +106,23 @@ def allFiles(root): ...@@ -101,19 +106,23 @@ def allFiles(root):
return files return files
def hasExt(path, allowedExts): def hasExt(path : PathLike,
allowedExts : Sequence[str]) -> bool:
"""Convenience function which returns ``True`` if the given ``path`` """Convenience function which returns ``True`` if the given ``path``
ends with any of the given ``allowedExts``, ``False`` otherwise. ends with any of the given ``allowedExts``, ``False`` otherwise.
""" """
return any([path.endswith(e) for e in allowedExts]) path = str(path)
return any(path.endswith(e) for e in allowedExts)
def addExt(prefix,
allowedExts=None, def addExt(
mustExist=True, prefix : PathLike,
defaultExt=None, allowedExts : Sequence[str] = None,
fileGroups=None, mustExist : bool = True,
unambiguous=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``. """Adds a file extension to the given file ``prefix``.
If ``mustExist`` is False, and the file does not already have a If ``mustExist`` is False, and the file does not already have a
...@@ -148,6 +157,8 @@ def addExt(prefix, ...@@ -148,6 +157,8 @@ def addExt(prefix,
containing *all* matching files is returned. containing *all* matching files is returned.
""" """
prefix = str(prefix)
if allowedExts is None: allowedExts = [] if allowedExts is None: allowedExts = []
if fileGroups is None: fileGroups = {} if fileGroups is None: fileGroups = {}
...@@ -189,7 +200,8 @@ def addExt(prefix, ...@@ -189,7 +200,8 @@ def addExt(prefix,
# If ambiguity is ok, return # If ambiguity is ok, return
# all matching paths # all matching paths
elif not unambiguous: if not unambiguous:
return allPaths return allPaths
# Ambiguity is not ok! More than # Ambiguity is not ok! More than
...@@ -223,19 +235,29 @@ def addExt(prefix, ...@@ -223,19 +235,29 @@ def addExt(prefix,
return allPaths[0] 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`. """ """Returns the base name of the given file name. See :func:`splitExt`. """
return splitExt(filename, allowedExts, firstDot)[0] 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`. """ """Returns the extension of the given file name. See :func:`splitExt`. """
return splitExt(filename, allowedExts, firstDot)[1] 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. """Returns the base name and the extension from the given file name.
If ``allowedExts`` is ``None`` and ``firstDot`` is ``False``, this If ``allowedExts`` is ``None`` and ``firstDot`` is ``False``, this
...@@ -262,6 +284,8 @@ def splitExt(filename, allowedExts=None, firstDot=False): ...@@ -262,6 +284,8 @@ def splitExt(filename, allowedExts=None, firstDot=False):
last period. Ignored if ``allowedExts`` is specified. last period. Ignored if ``allowedExts`` is specified.
""" """
filename = str(filename)
# If allowedExts is not specified # If allowedExts is not specified
# we split on a period character # we split on a period character
if allowedExts is None: if allowedExts is None:
...@@ -465,7 +489,7 @@ def removeDuplicates(paths, allowedExts=None, fileGroups=None): ...@@ -465,7 +489,7 @@ def removeDuplicates(paths, allowedExts=None, fileGroups=None):