From 547512adc6b845d1adfea7b9630a261ff0a96c9a Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauldmccarthy@gmail.com>
Date: Thu, 5 Jul 2018 14:46:09 +0100
Subject: [PATCH] ENH: fileOrThing decorator now accepts an "outprefix"
 argument, which can be used as a wildcard - all files which begin with
 outprefix can be specified as keyword arguments and LOADed.

---
 fsl/wrappers/wrapperutils.py | 172 +++++++++++++++++++++++++++--------
 1 file changed, 135 insertions(+), 37 deletions(-)

diff --git a/fsl/wrappers/wrapperutils.py b/fsl/wrappers/wrapperutils.py
index cfeed50ff..0ce8c09d5 100644
--- a/fsl/wrappers/wrapperutils.py
+++ b/fsl/wrappers/wrapperutils.py
@@ -88,7 +88,11 @@ and returned::
 import os.path as op
 import            os
 import            sys
+import            glob
+import            shutil
+import            fnmatch
 import            inspect
+import            logging
 import            tempfile
 import            warnings
 import            functools
@@ -103,6 +107,9 @@ import fsl.utils.run     as run
 import fsl.data.image    as fslimage
 
 
+log = logging.getLogger(__name__)
+
+
 def _update_wrapper(wrapper, wrapped, *args, **kwargs):
     """Replacement for the built-in ``functools.update_wrapper``. This
     implementation ensures that the wrapper function has an attribute
@@ -497,25 +504,31 @@ class _FileOrThing(object):
             return self.__output
 
 
-    def __init__(self, func, prepIn, prepOut, load, *things):
+    def __init__(self, func, prepIn, prepOut, load, *things, outprefix=None):
         """Initialise a ``_FileOrThing`` decorator.
 
-        :arg func:    The function to be decorated.
+        :arg func:      The function to be decorated.
+
+        :arg prepIn:    Function which returns a file name to be used in
+                        place of an input argument.
 
-        :arg prepIn:  Function which returns a file name to be used in
-                      place of an input argument.
+        :arg prepOut:   Function which generates a file name to use for
+                        arguments that were set to :data:`LOAD`.
 
-        :arg prepOut: Function which generates a file name to use for
-                      arguments that were set to :data:`LOAD`.
+        :arg load:      Function which is called to load items for arguments
+                        that were set to :data:`LOAD`. Must accept a file path
+                        as its sole argument.
 
-        :arg load:    Function which is called to load items for arguments
-                      that were set to :data:`LOAD`. Must accept a file path
-                      as its sole argument.
+        :arg things:    Names of all arguments which will be handled by
+                        this ``_FileOrThing`` decorator. If not provided,
+                        *all* arguments passed to the function will be
+                        handled.
 
-        :arg things:  Names of all arguments which will be handled by
-                      this ``_FileOrThing`` decorator. If not provided,
-                      *all* arguments passed to the function will be
-                      handled.
+        :arg outprefix: The name of a positional or keyword argument to the
+                        function, which specifies an output file name prefix.
+                        All other arguments which begin with this prefix (
+                        more specifically, which begin with ``[prefix]_``)
+                        may be interpreted as things to load.
 
         The ``prepIn`` and ``prepOut`` functions must accept the following
         positional arguments:
@@ -527,11 +540,12 @@ class _FileOrThing(object):
 
           - The argument value that was passed in
         """
-        self.__func    = func
-        self.__prepIn  = prepIn
-        self.__prepOut = prepOut
-        self.__load    = load
-        self.__things  = things
+        self.__func      = func
+        self.__prepIn    = prepIn
+        self.__prepOut   = prepOut
+        self.__load      = load
+        self.__things    = things
+        self.__outprefix = outprefix
 
 
     def __call__(self, *args, **kwargs):
@@ -551,15 +565,17 @@ class _FileOrThing(object):
         # function may be relative.
         with tempdir.tempdir(changeto=False) as td:
 
+            log.debug('Redirecting LOADed outputs to %s', td)
+
             # Replace any things with file names.
             # Also get a list of LOAD outputs
-            args, kwargs, outfiles = self.__prepareArgs(
-                td, argnames, args, kwargs)
+            args = self.__prepareArgs(td, argnames, args, kwargs)
+            args, kwargs, prefix, outfiles, prefixedFiles = args
 
             # Call the function
             result = func(*args, **kwargs)
 
-            # make a _Reults object to store
+            # make a _Results object to store
             # the output. If we are decorating
             # another _FileOrThing, the
             # results will get merged together
@@ -570,11 +586,42 @@ class _FileOrThing(object):
             # Load the LOADed outputs
             for oname, ofile in outfiles.items():
 
-                if not op.exists(ofile): oval = None
-                else:                    oval = self.__load(ofile)
+                log.debug('Loading output %s: %s', oname, ofile)
+
+                if op.exists(ofile): oval = self.__load(ofile)
+                else:                oval = None
 
                 result[oname] = oval
 
+            # Load or move output-prefixed files
+            if prefix is not None:
+
+                prefixDir   = op.abspath(op.dirname(prefix))
+                prefix      = op.basename(prefix)
+                allPrefixed = glob.glob(op.join(td, '{}_*'.format(prefix)))
+
+                for filename in allPrefixed:
+
+                    basename = op.basename(filename)
+                    for argname in prefixedFiles:
+                        if fnmatch.fnmatch(basename, '{}*'.format(argname)):
+
+                            log.debug('Loading prefixed output %s: %s',
+                                      argname, filename)
+
+                            fval = self.__load(filename)
+
+                            if argname in result: result[argname].append(fval)
+                            else:                 result[argname] = [fval]
+                            break
+
+                    # if file did not match any pattern,
+                    # move it into real prefix
+                    else:
+                        log.debug('Moving prefixed output %s into %s',
+                                  filename, prefixDir)
+                        shutil.move(filename, prefixDir)
+
             return result
 
 
@@ -597,34 +644,85 @@ class _FileOrThing(object):
 
                         - An updated copy of ``kwargs``.
 
+                        - The output file prefix that was actually passed in
+                          (it is subsequently modified so that prefixed outputs
+                          are redirected to a temporary location). All prefixed
+                          outputs that are not ``LOAD``ed should be moved into
+                          this directory. ``None`` if there is no output
+                          prefix.
+
                         - A dictionary of ``{ name : filename }`` mappings,
                           for all arguments with a value of ``LOAD``.
+
+                        - A list   ``[ filename ]`` paths, for all
+                          output-prefix arguments with a value of ``LOAD``.
         """
 
-        outfiles = dict()
+        # These containers keep track
+        # of output files which are to
+        # be loaded into memory
+        outfiles      = dict()
+        prefixedFiles = []
 
         allargs  = {k : v for k, v in zip(argnames, args)}
         allargs.update(kwargs)
 
+        # Has an output prefix been specified?
+        prefix     = allargs.get(self.__outprefix, None)
+        realPrefix = None
+
+        # If so, replace it with a new output
+        # prefix which will redirect all output
+        # to the temp dir.
+        #
+        # Importantly, here we assume that the
+        # underlying function (and hence the
+        # underlying command-line tool) will
+        # accept an output prefix which contains
+        # a directory path.
+        if prefix is not None:
+            realPrefix                = prefix
+            prefix                    = op.basename(prefix)
+            allargs[self.__outprefix] = op.join(workdir, prefix)
+
         if len(self.__things) > 0: things = self.__things
         else:                      things = allargs.keys()
 
-        for name in things:
+        for name, val in list(allargs.items()):
 
-            val = allargs.get(name, None)
+            # is this argument referring
+            # to a prefixd output?
+            isprefixed = (prefix is not None and
+                          name.startswith(prefix))
 
-            if val is None:
+            if not (isprefixed or name in things):
                 continue
 
-            if val is LOAD:
+            # Prefixed output files may only
+            # be given a value of LOAD
+            if isprefixed and val is not LOAD:
+                raise ValueError('Cannot specify name of prefixed file - the '
+                                 'name is  defined by the output prefix')
 
-                outfile = self.__prepOut(workdir, name, val)
+            if val is LOAD:
 
-                if outfile is not None:
-                    allargs[ name] = outfile
+                # this argument refers to an output
+                # that is generated from the output
+                # prefix argument, and doesn't map
+                # directly to an argument of the
+                # function. So we don't pass it
+                # through.
+                if isprefixed:
+                    prefixedFiles.append(name)
+                    allargs.pop(name)
+
+                # regular output-file argument
+                else:
+                    outfile = self.__prepOut(workdir, name, val)
                     outfiles[name] = outfile
-            else:
+                    allargs[ name] = outfile
 
+            else:
                 infile = self.__prepIn(workdir, name, val)
 
                 if infile is not None:
@@ -633,10 +731,10 @@ class _FileOrThing(object):
         args   = [allargs.pop(k) for k in argnames]
         kwargs = allargs
 
-        return args, kwargs, outfiles
+        return args, kwargs, realPrefix, outfiles, prefixedFiles
 
 
-def fileOrImage(*imgargs):
+def fileOrImage(*args, **kwargs):
     """Decorator which can be used to ensure that any NIfTI images are saved
     to file, and output images can be loaded and returned as ``nibabel``
     image objects or :class:`.Image` objects.
@@ -693,7 +791,7 @@ def fileOrImage(*imgargs):
             raise RuntimeError('Cannot handle type: {}'.format(intypes))
 
     def decorator(func):
-        fot = _FileOrThing(func, prepIn, prepOut, load, *imgargs)
+        fot = _FileOrThing(func, prepIn, prepOut, load, *args, **kwargs)
 
         def wrapper(*args, **kwargs):
             result = fot(*args, **kwargs)
@@ -705,7 +803,7 @@ def fileOrImage(*imgargs):
     return decorator
 
 
-def fileOrArray(*arrargs):
+def fileOrArray(*args, **kwargs):
     """Decorator which can be used to ensure that any Numpy arrays are saved
     to text files, and output files can be loaded and returned as Numpy arrays.
     """
@@ -727,7 +825,7 @@ def fileOrArray(*arrargs):
     load = np.loadtxt
 
     def decorator(func):
-        fot = _FileOrThing(func, prepIn, prepOut, load, *arrargs)
+        fot = _FileOrThing(func, prepIn, prepOut, load, *args, **kwargs)
 
         def wrapper(*args, **kwargs):
             return fot(*args, **kwargs)
-- 
GitLab