From 5bf216bc8bc5f4508486896540235109ad124951 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Thu, 17 Nov 2016 16:14:47 +0000
Subject: [PATCH] imcp/immv moved into their own module. They now have an
 option to honour the default file extensions (i.e. the FSLOUTPUTTYPE) - this
 is used by the fslpy_immv/imcp command line scripts.

---
 bin/fslpy_imcp    |  37 +++------
 bin/fslpy_immv    |  43 ++++-------
 fsl/data/image.py |  45 +++++------
 fsl/utils/imcp.py | 193 ++++++++++++++++++++++++++++++++++++++++++++++
 fsl/utils/path.py | 143 ++++------------------------------
 5 files changed, 256 insertions(+), 205 deletions(-)
 create mode 100644 fsl/utils/imcp.py

diff --git a/bin/fslpy_imcp b/bin/fslpy_imcp
index 85fa9a561..eb4441bdc 100755
--- a/bin/fslpy_imcp
+++ b/bin/fslpy_imcp
@@ -11,20 +11,8 @@ from __future__ import print_function
 import os.path        as op
 import                   sys
 import fsl.utils.path as fslpath
-
-
-SUPPORTED_EXTENSIONS = [
-    '.nii', '.nii.gz',
-    '.hdr', '.hdr.gz',
-    '.img', '.img.gz',
-    '.mnc', '.mnc.gz'
-]
-
-
-FILE_GROUPS = [
-    ('.img',    '.hdr'),
-    ('.img.gz', '.hdr.gz')
-]
+import fsl.utils.imcp as imcp
+import fsl.data.image as fslimage
 
 
 usage = """Usage:
@@ -37,35 +25,32 @@ Copy images from <file1> to <file2>, or copy all <file>s to <directory>
 NB: filenames can be basenames or include an extension.
 
 Recognised file extensions: {}
-""".format(', '.join(SUPPORTED_EXTENSIONS))
+""".format(', '.join(fslimage.ALLOWED_EXTENSIONS))
 
 
-def main():
+def main(argv=None):
     """Parses CLI arguments (see the usage string), and calls the
-    fsl.utils.path.imcp function on each input.
+    :func:`fsl.utils.imcp.imcp` function on each input.
     """
 
-    if len(sys.argv) < 2:
+    if len(argv) < 2:
         print(usage)
         sys.exit(1)
 
-    srcs = sys.argv[1:-1]
-    dest = sys.argv[  -1]
+    srcs = argv[:-1]
+    dest = argv[ -1]
 
     if len(srcs) > 1 and not op.isdir(dest):
         print(usage)
         sys.exit(1)
 
     srcs = fslpath.removeDuplicates(srcs,
-                                    allowedExts=SUPPORTED_EXTENSIONS,
-                                    fileGroups=FILE_GROUPS)
+                                    allowedExts=fslimage.ALLOWED_EXTENSIONS,
+                                    fileGroups=fslimage.FILE_GROUPS)
 
     for src in srcs:
         try:
-            fslpath.imcp(src,
-                         dest,
-                         allowedExts=SUPPORTED_EXTENSIONS,
-                         fileGroups=FILE_GROUPS) 
+            imcp.imcp(src, dest, useDefaultExt=True) 
             
         except Exception as e:
             print(e)
diff --git a/bin/fslpy_immv b/bin/fslpy_immv
index f1f1baf08..0b5a5ae54 100755
--- a/bin/fslpy_immv
+++ b/bin/fslpy_immv
@@ -11,21 +11,8 @@ from __future__ import print_function
 import os.path        as op
 import                   sys
 import fsl.utils.path as fslpath
-
-
-SUPPORTED_EXTENSIONS = [
-    '.nii', '.nii.gz',
-    '.hdr', '.hdr.gz',
-    '.img', '.img.gz',
-    '.mnc', '.mnc.gz'
-]
-
-
-
-FILE_GROUPS = [
-    ('.img',    '.hdr'),
-    ('.img.gz', '.hdr.gz')
-]
+import fsl.utils.imcp as imcp
+import fsl.data.image as fslimage
 
 
 usage = """Usage:
@@ -38,35 +25,35 @@ Moves images from <file1> to <file2>, or move all <file>s to <directory>
 NB: filenames can be basenames or include an extension.
 
 Recognised file extensions: {}
-""".format(', '.join(SUPPORTED_EXTENSIONS))
+""".format(', '.join(fslimage.ALLOWED_EXTENSIONS))
 
 
-def main():
+def main(argv=None):
     """Parses CLI arguments (see the usage string), and calls the
-    fsl.utils.path.immv function on each input.
-    """ 
+    fsl.utils.imcp.immv function on each input.
+    """
+
+    if argv is None:
+        argv = sys.argv[1:]
 
-    if len(sys.argv) < 2:
+    if len(argv) < 2:
         print(usage)
         sys.exit(1)
 
-    srcs = sys.argv[1:-1]
-    dest = sys.argv[  -1]
+    srcs = argv[:-1]
+    dest = argv[ -1]
 
     if len(srcs) > 1 and not op.isdir(dest):
         print(usage)
         sys.exit(1)
 
     srcs = fslpath.removeDuplicates(srcs,
-                                    allowedExts=SUPPORTED_EXTENSIONS,
-                                    fileGroups=FILE_GROUPS) 
+                                    allowedExts=fslimage.ALLOWED_EXTENSIONS,
+                                    fileGroups=fslimage.FILE_GROUPS) 
 
     for src in srcs:
         try:
-            fslpath.immv(src,
-                         dest,
-                         allowedExts=SUPPORTED_EXTENSIONS,
-                         fileGroups=FILE_GROUPS)
+            imcp.immv(src, dest, useDefaultExt=True, move=True)
             
         except Exception as e:
             print(e)
diff --git a/fsl/data/image.py b/fsl/data/image.py
index c81acf89f..1ff4ee748 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -25,9 +25,10 @@ and file names:
    :nosignatures:
 
    looksLikeImage
-   removeExt
+   addExt 
    splitExt
-   addExt
+   getExt
+   removeExt
    defaultExt
    loadIndexedImageFile
 """
@@ -789,36 +790,36 @@ def looksLikeImage(filename, allowedExts=None):
     return any([filename.endswith(ext) for ext in allowedExts])
 
 
-def removeExt(filename):
-    """Removes the extension from the given file name. Returns the filename
-    unmodified if it does not have a supported extension.
-
-    See :func:`~fsl.utils.path.removeExt`.
-
-    :arg filename: The file name to strip.
+def addExt(prefix, mustExist=True):
+    """Adds a file extension to the given file ``prefix``.  See
+    :func:`~fsl.utils.path.addExt`.
     """
-    return fslpath.removeExt(filename, ALLOWED_EXTENSIONS)
+    return fslpath.addExt(prefix,
+                          ALLOWED_EXTENSIONS,
+                          mustExist,
+                          defaultExt(),
+                          fileGroups=FILE_GROUPS)
 
 
 def splitExt(filename):
-    """Splits the base name and extension for the given ``filename``.
-
-    See :func:`~fsl.utils.path.splitExt`.
+    """Splits the base name and extension for the given ``filename``.  See
+    :func:`~fsl.utils.path.splitExt`.
     """
-
     return fslpath.splitExt(filename, ALLOWED_EXTENSIONS)
 
 
-def addExt(prefix, mustExist=True):
-    """Adds a file extension to the given file ``prefix``.
+def getExt(filename):
+    """Gets the extension for the given file name.  See
+    :func:`~fsl.utils.path.getExt`.
+    """
+    return fslpath.getExt(filename, ALLOWED_EXTENSIONS)
+
 
-    See :func:`~fsl.utils.path.addExt`.
+def removeExt(filename):
+    """Removes the extension from the given file name. See
+    :func:`~fsl.utils.path.removeExt`.
     """
-    return fslpath.addExt(prefix,
-                          ALLOWED_EXTENSIONS,
-                          mustExist,
-                          defaultExt(),
-                          fileGroups=FILE_GROUPS)
+    return fslpath.removeExt(filename, ALLOWED_EXTENSIONS)
 
 
 def defaultExt():
diff --git a/fsl/utils/imcp.py b/fsl/utils/imcp.py
new file mode 100644
index 000000000..47a509025
--- /dev/null
+++ b/fsl/utils/imcp.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python
+#
+# imcp.py - Functions for moving/copying NIFTI image files.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""
+
+.. autosummary::
+   :nosignatures:
+
+   imcp
+   immv
+"""
+
+
+import                   os
+import os.path        as op
+import                   shutil
+
+import fsl.utils.path as fslpath
+import fsl.data.image as fslimage
+
+
+def imcp(src,
+         dest,
+         overwrite=False,
+         useDefaultExt=False,
+         move=False):
+    """Copy the given ``src`` file to destination ``dest``.
+
+    A :class:`.fsl.utils.path.PathError` is raised if anything goes wrong.
+
+    :arg src:           Path to copy. If ``allowedExts`` is provided,
+                        the file extension can be omitted.
+    
+    :arg dest:          Destination path. Can be an incomplete file
+                        specification (i.e. without the extension), or a 
+                        directory. 
+
+    :arg overwrite:     If ``True`` this function will overwrite files that 
+                        already exist. Defaults to ``False``.
+
+    :arg useDefaultExt: Defaults to ``False``. If ``True``, the destination
+                        file type will be set according to the default 
+                        extension, specified by
+                        :func:`~fsl.data.image.defaultExt`.
+    
+    :arg move:          If ``True``, the files are moved, instead of being
+                        copied. See :func:`immv`.
+    """
+
+    import nibabel as nib
+
+    if op.isdir(dest):
+        dest = op.join(dest, op.basename(src))
+
+    srcBase,  srcExt  = fslimage.splitExt(src)
+    destBase, destExt = fslimage.splitExt(dest)
+
+    # src was specified without an
+    # extension, or the specified
+    # src does not have an allowed
+    # extension. 
+    if srcExt == '':
+
+        # Try to resolve the specified src
+        # path - if src does not exist, or
+        # does not have an allowed extension,
+        # addExt will raise an error
+        src = fslimage.addExt(src, mustExist=True)
+
+        # We've resolved src to a 
+        # full filename - split it 
+        # again to get its extension
+        srcBase, srcExt = fslimage.splitExt(src)
+
+    # Figure out the destination file
+    # extension/type. If useDefaultExt
+    # is True, we use the default
+    # extension. Otherwise, if no
+    # destination file extension is
+    # provided, we use the source
+    # extension.
+    if   useDefaultExt: destExt = fslimage.defaultExt()
+    elif destExt == '': destExt = srcExt
+
+    # Resolve any file group differences
+    # e.g. we don't care if the src is
+    # specified as file.hdr, and the dest
+    # is specified as file.img - if src
+    # and dest are part of the same file
+    # group, we replace the dest extension
+    # with the src extension.
+    if srcExt != destExt:
+        for group in fslimage.FILE_GROUPS:
+            if srcExt in group and destExt in group:
+                destExt = srcExt
+                break
+
+    dest = destBase + destExt
+
+    # Give up if we don't have permission. 
+    if          not os.access(op.dirname(dest), os.W_OK | os.X_OK):
+        raise fslpath.PathError('imcp error - cannot write to {}'.format(dest))
+    
+    if move and not os.access(op.dirname(src),  os.W_OK | os.X_OK):
+        raise fslpath.PathError('imcp error - cannot move from {}'.format(src))
+
+    # If the source file type does not
+    # match the destination file type,
+    # we need to perform a conversion.
+    #
+    # This is expensive in terms of io
+    # and cpu, but programmatically
+    # very easy - nibabel does all the
+    # hard work.
+    if srcExt != destExt:
+
+        if not overwrite and op.exists(dest):
+            raise fslpath.PathError('imcp error - destination already '
+                                    'exists ({})'.format(dest))
+         
+        img = nib.load(src)
+        nib.save(img, dest)
+
+        if move:
+            os.remove(src)
+
+        return
+
+    # Otherwise we do a file copy. This
+    # is actually more complicated than
+    # conveting the file type due to
+    # hdr/img pairs ...
+    # 
+    # If the source is part of a file group,
+    # e.g. src.img/src.hdr), we need to copy
+    # the whole set of files. So here we
+    # build a list of source files that need
+    # to be copied/moved. The getFileGroup
+    # function returns all other files that
+    # are associated with this file (i.e.
+    # part of the same group).
+    #
+    # We store the sources as separate
+    # (base, ext) tuples, so we don't
+    # have to re-split when creating
+    # destination paths.
+    copySrcs = fslpath.getFileGroup(src,
+                                    fslimage.ALLOWED_EXTENSIONS,
+                                    fslimage.FILE_GROUPS,
+                                    fullPaths=False)
+    
+    copySrcs = [(srcBase, e) for e in copySrcs]
+
+    # Note that these additional files 
+    # do not have to exist, e.g.
+    # imcp('blah.img', ...)  will still
+    # work if there is no blah.hdr
+    copySrcs = [(b, e) for (b, e) in copySrcs if op.exists(b + e)]
+
+    # Build a list of destinations for each
+    # copy source - we build this list in
+    # advance, so we can fail if any of the
+    # destinations already exist. We also
+    # re-combine the source bases/extensions.
+    copyDests = [destBase + e for (b, e) in copySrcs]
+    copySrcs  = [b        + e for (b, e) in copySrcs]
+
+    # Fail if any of the destination 
+    # paths already exist
+    if not overwrite and any([op.exists(d) for d in copyDests]):
+        raise fslpath.PathError('imcp error - a destination path already '
+                                'exists ({})'.format(', '.join(copyDests)))
+ 
+    # Do the copy/move
+    for src, dest in zip(copySrcs, copyDests):
+        if move: shutil.move(src, dest)
+        else:    shutil.copy(src, dest)
+
+
+def immv(src,
+         dest,
+         overwrite=False,
+         useDefaultExt=True):
+    """Move the specified ``src`` to the specified ``dest``. See :func:`imcp`.
+    """
+    imcp(src,
+         dest,
+         overwrite=overwrite,
+         useDefaultExt=useDefaultExt,
+         move=True)
diff --git a/fsl/utils/path.py b/fsl/utils/path.py
index 464c0b892..cbd9fd8bd 100644
--- a/fsl/utils/path.py
+++ b/fsl/utils/path.py
@@ -18,13 +18,10 @@ paths.
    getExt
    splitExt
    getFileGroup
-   imcp
-   immv
 """
 
 
 import os.path as op
-import            shutil
 
 
 class PathError(Exception):
@@ -231,23 +228,23 @@ def removeDuplicates(paths, allowedExts=None, fileGroups=None):
         001.img
         002.hdr
         002.img
-        003.img
+        003.hdr
         003.img
 
     And you call ``removeDuplicates`` like so::
 
-         paths        = ['001.img', '001.hdr',
-                         '002.img', '002.hdr',
-                         '003.img', '003.hdr']
+         paths       = ['001.img', '001.hdr',
+                        '002.img', '002.hdr',
+                        '003.img', '003.hdr']
     
-         allowedExts = ['.img', '.hdr']
-         fileGroups  = [('.img',    '.hdr')]
+         allowedExts = ['.img',  '.hdr']
+         fileGroups  = [('.img', '.hdr')]
 
          removeDuplicates(paths, allowedExts, fileGroups)
 
     The returned list will be::
 
-         ['001.img', '003.img', '003.img']
+         ['001.img', '002.img', '003.img']
 
     :arg paths:       List of paths to reduce.
 
@@ -290,16 +287,17 @@ def getFileGroup(path, allowedExts=None, fileGroups=None, fullPaths=True):
     return ``file.img`` (i.e. the file which matches the first extension in
     the group).
 
-    Similarly, if you call the :func:`imcp` or :func:`immv` functions with the
-    above parameters, both ``file.img`` and ``file.hdr`` will be moved.
+    Similarly, if you call the :func:`.imcp.imcp` or :func:`.imcp.immv`
+    functions with the above parameters, both ``file.img`` and ``file.hdr``
+    will be moved.
 
     .. note:: The primary use-case of file groups is to resolve ambiguity with
               respect to NIFTI and ANALYSE75 image pairs. By specifying
               ``fileGroups=[('.img', '.hdr'), ('.img.gz', '.hdr.gz')]``, the
-              :func:`addExt`, :func:`immv` and :func:`imcp` functions are able
-              to figure out what you mean when you specify ``file``, and both
-              ``file.hdr`` and ``file.img`` (or ``file.hdr.gz`` and
-              ``file.img.gz``) exist.
+              :func:`addExt`, :func:`.imcp.immv` and :func:`.imcp.imcp`
+              functions are able to figure out what you mean when you specify
+              ``file``, and both ``file.hdr`` and ``file.img`` (or
+              ``file.hdr.gz`` and ``file.img.gz``) exist.
     
     :arg path:        Path to the file. Must contain the file extension.
     
@@ -342,116 +340,3 @@ def getFileGroup(path, allowedExts=None, fileGroups=None, fullPaths=True):
     else:
         if fullPaths: return matchedGroupFiles[0]
         else:         return matchedGroups[    0]
-
-
-def imcp(src,
-         dest,
-         allowedExts=None,
-         fileGroups=None,
-         overwrite=False,
-         move=False):
-    """Copy the given ``src`` file to destination ``dest``.
-
-    :arg src:         Path to copy. If ``allowedExts`` is provided,
-                      the file extension can be omitted.
-    
-    :arg dest:        Destination path. Can be an incomplete file
-                      specification (i.e. without the extension), or a 
-                      directory. 
-    
-    :arg allowedExts: Allowed/recognised file extensions.
-    
-    :arg fileGroups:  Recognised file groups - see the :func:`getFileGroup`
-                      documentation.
-
-    :arg overwrite:   If ``True`` this function will overwrite files that 
-                      already exist. Defaults to ``False``.
-    
-    :arg move:        If ``True``, the files are moved, instead of being
-                      copied.
-    """
-
-    base, ext = splitExt(src, allowedExts)
-    destIsDir = op.isdir(dest)
-
-    # If dest has been specified 
-    # as a file name, we don't 
-    # care about its extension.
-    if not destIsDir:
-        dest = removeExt(dest, allowedExts)
-
-    # src was specified without an
-    # extension, or the specitifed
-    # src does not have an allowed
-    # extension. 
-    if ext == '':
-
-        # Try to resolve the specified src
-        # path - if src does not exist, or
-        # does not have an allowed extension,
-        # addExt will raise an error
-        src = addExt(src,
-                     allowedExts,
-                     mustExist=True,
-                     fileGroups=fileGroups)
-
-        # We've resolved src to a 
-        # full filename - split it 
-        # again to get its extension
-        base, ext = splitExt(src, allowedExts)
-
-    # If the source is part of a file group,
-    # e.g. src.img/src.hdr, we want to copy
-    # the whole set of files. So here we
-    # build a list of source files that need
-    # to be copied/moved. The getFileGroup
-    # function returns all other files that
-    # are associated with this file (i.e.
-    # part of the same group).
-    #
-    # We store the sources as separate
-    # (base, ext) tuples, so we don't
-    # have to re-split when creating
-    # destination paths.
-    copySrcs = getFileGroup(src, allowedExts, fileGroups, fullPaths=False)
-    copySrcs = [(base, e) for e in copySrcs]
-
-    # Note that these additional files 
-    # do not have to exist, e.g.
-    # imcp('blah.img', ...)  will still
-    # work if there is no blah.hdr
-    copySrcs = [(b, e) for (b, e) in copySrcs if op.exists(b + e)]
-
-    # Build a list of destinations for each
-    # copy source - we build this list in
-    # advance, so we can fail if any of the
-    # destinations already exist.
-    copyDests = []
-    for i, (base, ext) in enumerate(copySrcs):
-
-        # We'll also take this opportunity 
-        # to re-combine the source paths
-        copySrcs[i] = base + ext
-
-        basename = op.basename(base)
-
-        if destIsDir: copyDests.append(op.join(dest, basename + ext))
-        else:         copyDests.append(dest + ext)
-
-    # Fail if any of the destination 
-    # paths already exist
-    if not overwrite and any([op.exists(d) for d in copyDests]):
-        raise PathError('imcp error - a destination path already '
-                        'exists ({})'.format(', '.join(copyDests)))
- 
-    # Do the copy/move
-    for src, dest in zip(copySrcs, copyDests):
-
-        if move: shutil.move(src, dest)
-        else:    shutil.copy(src, dest)
-
-
-def immv(src, dest, allowedExts=None, fileGroups=None, overwrite=False):
-    """Move the specified ``src`` to the specified ``dest``. See :func:`imcp`.
-    """
-    imcp(src, dest, allowedExts, fileGroups, overwrite, move=True)
-- 
GitLab