diff --git a/fsl/data/featanalysis.py b/fsl/data/featanalysis.py index 75fe43a61a9d2a58347455f2e50e46e3ea9e9b60..c1fd09557903580e36ed6d3de26a04d3dc68da1c 100644 --- a/fsl/data/featanalysis.py +++ b/fsl/data/featanalysis.py @@ -90,7 +90,7 @@ def isFEATDir(path): try: fslimage.addExt(op.join(path, 'filtered_func_data'), mustExist=True) - except ValueError: + except fslimage.PathError: return False if not op.exists(op.join(dirname, 'design.fsf')): return False @@ -451,7 +451,7 @@ def getDataFile(featdir): """Returns the name of the file in the FEAT directory which contains the model input data (typically called ``filtered_func_data.nii.gz``). - Raises a :exc:`ValueError` if the file does not exist. + Raises a :exc:`.PathError` if the file does not exist. :arg featdir: A FEAT directory. """ @@ -464,7 +464,7 @@ def getMelodicFile(featdir): components (if melodic ICA was performed as part of the FEAT analysis). This file can be loaded as a :class:`.MelodicImage`. - Raises a :exc:`ValueError` if the file does not exist. + Raises a :exc:`.PathError` if the file does not exist. """ melfile = op.join(featdir, 'filtered_func_data.ica', 'melodic_IC') return fslimage.addExt(melfile, mustExist=True) @@ -474,7 +474,7 @@ def getResidualFile(featdir): """Returns the name of the file in the FEAT results which contains the model fit residuals (typically called ``res4d.nii.gz``). - Raises a :exc:`ValueError` if the file does not exist. + Raises a :exc:`.PathError` if the file does not exist. :arg featdir: A FEAT directory. """ @@ -485,7 +485,7 @@ def getResidualFile(featdir): def getPEFile(featdir, ev): """Returns the path of the PE file for the specified EV. - Raises a :exc:`ValueError` if the file does not exist. + Raises a :exc:`.PathError` if the file does not exist. :arg featdir: A FEAT directory. :arg ev: The EV number (0-indexed). @@ -497,7 +497,7 @@ def getPEFile(featdir, ev): def getCOPEFile(featdir, contrast): """Returns the path of the COPE file for the specified contrast. - Raises a :exc:`ValueError` if the file does not exist. + Raises a :exc:`.PathError` if the file does not exist. :arg featdir: A FEAT directory. :arg contrast: The contrast number (0-indexed). @@ -509,7 +509,7 @@ def getCOPEFile(featdir, contrast): def getZStatFile(featdir, contrast): """Returns the path of the Z-statistic file for the specified contrast. - Raises a :exc:`ValueError` if the file does not exist. + Raises a :exc:`.PathError` if the file does not exist. :arg featdir: A FEAT directory. :arg contrast: The contrast number (0-indexed). @@ -521,7 +521,7 @@ def getZStatFile(featdir, contrast): def getClusterMaskFile(featdir, contrast): """Returns the path of the cluster mask file for the specified contrast. - Raises a :exc:`ValueError` if the file does not exist. + Raises a :exc:`.PathError` if the file does not exist. :arg featdir: A FEAT directory. :arg contrast: The contrast number (0-indexed). diff --git a/fsl/data/image.py b/fsl/data/image.py index 696ebd855a92b0dd67b0a257cef5e9ab2193db13..384768aab24bbfdbf3404e2b2292f28c9461cd90 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -769,10 +769,22 @@ EXTENSION_DESCRIPTIONS = ['Compressed NIFTI images', """Descriptions for each of the extensions in :data:`ALLOWED_EXTENSIONS`. """ +REPLACEMENTS = {'.hdr' : ['.img', '.img.gz']} +"""Suffix replacements used by :func:`addExt` to resolve file path +ambiguities - see :func:`fsl.utils.path.addExt`. +""" + + DEFAULT_EXTENSION = '.nii.gz' """The default file extension (TODO read this from ``$FSLOUTPUTTYPE``).""" +PathError = fslpath.PathError +"""Error raised by :mod:`fsl.utils.path` functions when an error occurs. +Made available in this module for convenience. +""" + + def looksLikeImage(filename, allowedExts=None): """Returns ``True`` if the given file looks like an image, ``False`` otherwise. @@ -810,7 +822,8 @@ def addExt(prefix, mustExist=True): return fslpath.addExt(prefix, ALLOWED_EXTENSIONS, mustExist, - DEFAULT_EXTENSION) + DEFAULT_EXTENSION, + replace=REPLACEMENTS) def loadIndexedImageFile(filename): diff --git a/fsl/data/melodicanalysis.py b/fsl/data/melodicanalysis.py index 9de847d439741b64a980bf34355dcca1693bdbfe..6bb63b7000578ea36c084e1ef9eb2e2d0cfaf5ec 100644 --- a/fsl/data/melodicanalysis.py +++ b/fsl/data/melodicanalysis.py @@ -77,7 +77,7 @@ def isMelodicDir(path): # Must contain an image file called melodic_IC try: fslimage.addExt(op.join(dirname, 'melodic_IC'), mustExist=True) - except ValueError: + except fslimage.PathError: return False # Must contain files called @@ -124,8 +124,8 @@ def getDataFile(meldir): dataFile = op.join(topDir, 'filtered_func_data') - try: return fslimage.addExt(dataFile, mustExist=True) - except ValueError: return None + try: return fslimage.addExt(dataFile, mustExist=True) + except fslimage.PathErrpr: return None def getMeanFile(meldir): diff --git a/fsl/utils/path.py b/fsl/utils/path.py index 408b77c23ba7eed3579c7d4876caa5052dce546a..ef628918c1df60ce98985a69879a73d52a394153 100644 --- a/fsl/utils/path.py +++ b/fsl/utils/path.py @@ -15,12 +15,18 @@ paths. shallowest addExt removeExt + getExt """ import os.path as op +class PathError(Exception): + """``Exception`` class raised by :func:`addExt` and :func:`getExt`. """ + pass + + def deepest(path, suffixes): """Finds the deepest directory which ends with one of the given sequence of suffixes, or returns ``None`` if no directories end @@ -64,7 +70,11 @@ def shallowest(path, suffixes): return None -def addExt(prefix, allowedExts, mustExist=True, defaultExt=None): +def addExt(prefix, + allowedExts, + mustExist=True, + defaultExt=None, + replace=None): """Adds a file extension to the given file ``prefix``. If ``mustExist`` is False, and the file does not already have a @@ -77,17 +87,47 @@ def addExt(prefix, allowedExts, mustExist=True, defaultExt=None): extension. A :exc:`ValueError` is raised if: - No files exist with the given prefix and a supported extension. - - More than one file exists with the given prefix, and a supported - extension. + + - ``replace`` is ``None``, and more than one file exists with the + given prefix, and a supported extension. Otherwise the full file name is returned. - :arg prefix: The file name refix to modify. - :arg mustExist: Whether the file must exist or not. + :arg prefix: The file name prefix to modify. + :arg allowedExts: List of allowed file extensions. + + :arg mustExist: Whether the file must exist or not. + :arg defaultExt: Default file extension to use. + + :arg replace: If multiple files exist with the same ``prefix`` and + supported extensions (e.g. ``file.hdr`` and + ``file.img``), this dictionary can be used to resolve + ambiguities. It must have the structure:: + + { + suffix : [replacement, ...], + ... + } + + If files with ``suffix`` and one of the ``replacement`` + suffixes exists, the ``suffix`` file will + be ignored, and replaced with the ``replacement`` file. + If multiple ``replacement`` files exist alongside the + ``suffix`` file, a ``PathError`` is raised. + + .. note:: The primary use-case of the ``replace`` parameter is to resolve + ambiguity with respect to NIFTI and ANALYSE75 image pairs. By + specifying ``replace={'.hdr' : ['.img'. '.img.gz'}``, the + ``addExt`` function is able to figure out what you mean when you + wish to add an extension to ``file``, and ``file.hdr`` and + either ``file.img`` or ``file.img.gz`` (but not both) exist. """ + if replace is None: + replace = {} + if not mustExist: # the provided file name already @@ -101,31 +141,74 @@ def addExt(prefix, allowedExts, mustExist=True, defaultExt=None): # If the provided prefix already ends with a # supported extension , check to see that it exists if any([prefix.endswith(ext) for ext in allowedExts]): - extended = [prefix] + allPaths = [prefix] # Otherwise, make a bunch of file names, one per # supported extension, and test to see if exactly # one of them exists. else: - extended = [prefix + ext for ext in allowedExts] + allPaths = [prefix + ext for ext in allowedExts] - exists = [op.isfile(e) for e in extended] + exists = [op.isfile(e) for e in allPaths] + nexists = sum(exists) # Could not find any supported file # with the specified prefix - if not any(exists): - raise ValueError( - 'Could not find a supported file with prefix {}'.format(prefix)) + if nexists == 0: + raise PathError('Could not find a supported file ' + 'with prefix {}'.format(prefix)) # Ambiguity! More than one supported - # file with the specified prefix - if sum(exists) > 1: - raise ValueError('More than one file with prefix {}'.format(prefix)) + # file with the specified prefix. + elif nexists > 1: + + # Remove non-existent paths from the + # extended list, get all their + # suffixes, and potential replacements + allPaths = [allPaths[i] for i in range(len(allPaths)) if exists[i]] + suffixes = [getExt(e, allowedExts) for e in allPaths] + replacements = [replace.get(s) for s in suffixes] + hasReplace = [r is not None for r in replacements] + + for p, r in zip(allPaths, replacements): + print ' {} replacements: {}'.format(p, r) + + # If any replacement has been specified + # for any of the existing suffixes, + # see if we have a unique match for + # exactly one existing suffix, the + # one to be ignored/replaced. + if sum(hasReplace) == 1: + + # Make sure there is exactly one potential + # replacement for this suffix. If there's + # more than one (e.g. file.hdr plus both + # file.img and file.img.gz) we can't resolve + # the ambiguity. In this case the code will + # fall through to the raise statement below. + toReplace = allPaths[hasReplace.index(True)] + replacements = replacements[hasReplace.index(True)] + replacements = [prefix + ext for ext in replacements] + replExists = [r in allPaths for r in replacements] + + if sum(replExists) == 1: + + replacedBy = replacements[replExists.index(True)] + allPaths[allPaths.index(toReplace)] = replacedBy + allPaths = list(set(allPaths)) + + exists = [True] * len(allPaths) + + # There's more than one path match - + # we can't resolve the ambiguity + if len(allPaths) > 1: + raise PathError('More than one file with ' + 'prefix {}'.format(prefix)) # Return the full file name of the # supported file that was found extIdx = exists.index(True) - return extended[extIdx] + return allPaths[extIdx] def removeExt(filename, allowedExts): @@ -151,3 +234,32 @@ def removeExt(filename, allowedExts): # and trim it from the file name return filename[:-extLen] + + +def getExt(filename, allowedExts=None): + """Returns the extension from the given file name. + + If ``allowedExts`` is ``None``, this function is equivalent to using:: + + os.path.splitext(filename)[1] + + If ``allowedExts`` is provided, but the file does not end with an allowed + extension, a :exc:`PathError` is raised. + + :arg allowedExts: Allowed/recognised file extensions. + """ + + # If allowedExts is not specified, + # we just use op.splitext + if allowedExts is None: + return op.splitext(filename)[1] + + # Otherwise, try and find a suffix match + extMatches = [filename.endswith(ext) for ext in allowedExts] + + if not any(extMatches): + raise PathError('{} does not end in a supported extension ({})'.format( + filename, ', '.join(allowedExts))) + + extIdx = extMatches.index(True) + return allowedExts[extIdx]