diff --git a/fsl/wrappers/wrapperutils.py b/fsl/wrappers/wrapperutils.py index bab95060920e1a2cc2ea390fac167dd4589f8400..5bf162825142d7134bcc29fd9ecadd8eb9ad9954 100644 --- a/fsl/wrappers/wrapperutils.py +++ b/fsl/wrappers/wrapperutils.py @@ -67,7 +67,6 @@ def _unwrap(func): return func - SHOW_IF_TRUE = object() """Constant to be used in the ``valmap`` passed to the :func:`applyArgStyle` function. @@ -165,46 +164,52 @@ def applyArgStyle(style, argmap=None, valmap=None, **kwargs): def required(*reqargs): - """Decorator which makes sure that all specified keyword arguments are - present before calling the decorated function. + """Decorator which makes sure that all specified arguments are present + before calling the decorated function. Arguments which are not present + will result in an :exc:`AssertionError`. Use as follows:: + + @required('foo') + def funcWhichRequires_foo(**kwargs): + foo = kwargs['foo'] """ def decorator(func): def wrapper(*args, **kwargs): - kwargs = kwargs.copy() - kwargs.update(argsToKwargs(func, args)) + kwargs = argsToKwargs(func, args, kwargs) for reqarg in reqargs: assert reqarg in kwargs return func(**kwargs) - - wrapper = _update_wrapper(wrapper, func) - - # If this is a bound method, make - # sure that the instance is set on - # the wrapper function - this is - # needed by _FileOrThing decorators. - if hasattr(func, '__self__'): - wrapper.__self__ = func.__self__ - - return wrapper - + return _update_wrapper(wrapper, func) return decorator -def argsToKwargs(func, args): +def argsToKwargs(func, args, kwargs=None): """Given a function, and a sequence of positional arguments destined for that function, converts the positional arguments into a dict of keyword arguments. Used by the :class:`_FileOrThing` class. + + :arg func: Function which will accept ``args`` as positionals. + + :arg args: Tuple of positional arguments to be passed to ``func``. + + :arg kwargs: Optional. If provided, assumed to be keyword arguments + to be passed to ``func``. The ``args`` are merged into + ``kwargs``. A :exc:`ValueError` is raised if one of + ``args`` is already present in ``kwargs``. """ + # Remove any decorators + # from the function func = _unwrap(func) - # getargspec is the only way to get the names - # of positional arguments in Python 2.x. + # getargspec is the only way to + # get the names of positional + # arguments in Python 2.x. if sys.version_info[0] < 3: argnames = inspect.getargspec(func).args - # getargspec is deprecated in python 3.x + # But getargspec is deprecated + # in python 3.x else: # getfullargspec is deprecated in @@ -213,8 +218,12 @@ def argsToKwargs(func, args): warnings.filterwarnings('ignore', category=DeprecationWarning) argnames = inspect.getfullargspec(func).args - kwargs = collections.OrderedDict() + if kwargs is None: kwargs = dict() + else: kwargs = dict(kwargs) + for name, val in zip(argnames, args): + if name in kwargs: + raise ValueError('Argument {} repeated'.format(name)) kwargs[name] = val return kwargs @@ -236,6 +245,7 @@ class _FileOrThing(object): :func:`fileOrImage` and :func:`fileOrArray` decorator functions for more details. + These decorators are intended for functions which wrap a command-line tool, i.e. where some inputs/outputs need to be specified as file names. @@ -263,10 +273,11 @@ class _FileOrThing(object): Functions decorated with a ``_FileOrThing`` decorator will always return a - tuple, where the first element is the function's actual return value. The - remainder of the tuple will contain any arguments that were given the - special ``LOAD`` value. ``None`` is returned for any ``LOAD`` arguments - corresponded to output files that were not generated by the function. + ``dict``-like object, where the function's actual return value is + accessible via an attribute called `output`. All output arguments with a + value of ``LOAD`` will be present as dictionary entries, with the keyword + argument names used as keys. Any ``LOAD``ed output arguments which were not + generated by the function will not be present in the dictionary. **Example** @@ -291,69 +302,108 @@ class _FileOrThing(object): if output is not None: np.savetxt(output, atoc) + return 'Done' + Because we have decorated the ``concat`` function with :func:`fileToArray`, it can be called with either file names, or Numpy arrays:: + # All arguments are passed through # unmodified - the output will be - # saved to a file called atoc.mat + # saved to a file called atoc.mat. concat('atob.txt', 'btoc.txt', 'atoc.mat') - # The output is returned as a numpy - # array (in a tuple with the concat - # function's return value) - atoc = concat('atob.txt', 'btoc.txt', LOAD)[1] + # The function's return value + # is accessed via an attribute called + # "output" on the dict + assert concat('atob.txt', 'btoc.txt', 'atoc.mat').output == 'Done' + + # Outputs to be loaded into memory + # are returned in a dictionary, + # with argument names as keys. + atoc = concat('atob.txt', 'btoc.txt', LOAD)['atoc'] - # The inputs are saved to temporary - # files, and those file names are - # passed to the concat function. - atoc = concat(np.diag([2, 2, 2, 0]), np.diag([3, 3, 3, 3]), LOAD)[1] + # In-memory inputs are saved to + # temporary files, and those file + # names are passed to the concat + # function. + atoc = concat(np.diag([2, 2, 2, 0]), + np.diag([3, 3, 3, 3]), LOAD)['atoc'] + + + **Using with other decorators** """ - def __init__(self, prepareThing, loadThing, *things): + class _Results(dict): + """A custom ``dict`` type used to return outputs from a function + decorated with ``_FileOrThing``. All outputs are stored as dictionary + items, with the argument name as key, and the output object (the + "thing") as value. + + The decorated function's actual return value is accessible via the + :meth:`output` property. + """ + def __init__(self, output): + self.__output = output + + @property + def output(self): + """Access the return value of the decorated function. """ + return self.__output + + + def __init__(self, prepIn, prepOut, load, *things): """Initialise a ``_FileOrThing`` decorator. - :arg prepareThing: Function which - :arg loadThing: Function which is called for arguments that - were set to :data:`LOAD`. + :arg prepIn: Function which returns a file name to be used in + place of an input argument. - :arg things: - """ - self.__prepareThing = prepareThing - self.__loadThing = loadThing - self.__things = things + :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. - def __call__(self, func): - """Creates and returns the real decorator function. """ + :arg things: Names of all arguments which will be handled by + this ``_FileOrThing`` decorator. + + The ``prepIn`` and ``prepOut`` functions must accept the following + positional arguments: + + - A directory in which all temporary input/output files should be + stored - isFOT = isinstance(getattr(func, '__self__', None), _FileOrThing) - wrapper = functools.partial(self.__wrapper, func, isFOT) + - The name of the keyword argument to be processed + + - The argument value that was passed in + """ + self.__prepIn = prepIn + self.__prepOut = prepOut + self.__load = load + self.__things = things - # TODO - wrapper = _update_wrapper(wrapper, func) - wrapper.__self__ = self - return wrapper + def __call__(self, func): + """Creates and returns the decorated function. """ + wrapper = functools.partial(self.__wrapper, func) + return _update_wrapper(wrapper, func) - def __wrapper(self, func, isFileOrThing, *args, **kwargs): - """Function which wraps ``func``, ensuring that any arguments of + def __wrapper(self, func, *args, **kwargs): + """Function which calls ``func``, ensuring that any arguments of type ``Thing`` are saved to temporary files, and any arguments with the value :data:`LOAD` are loaded and returned. - :arg func: The func being wrapped. + :arg func: The function being wrapped. - :arg isFileOrThing: Set to ``True`` if ``func`` is a wrapper metho - of another ``_FileOrThing`` instance. In this case, - the output arguments will be flattenedinto a single - tuple. + All other arguments are passed through to ``func``. """ - kwargs = kwargs.copy() - kwargs.update(argsToKwargs(func, args)) + # Turn all positionals into keywords + kwargs = argsToKwargs(func, args, kwargs) # Create a tempdir to store any temporary # input/output things, but don't change @@ -361,57 +411,76 @@ class _FileOrThing(object): # function may be relative. with tempdir.tempdir(changeto=False) as td: - kwargs, infiles, outfiles = self.__prepareThings(td, kwargs) + # Replace any things with file names. + # Also get a list of LOAD outputs + kwargs, outfiles = self.__prepareArgs(td, kwargs) # Call the function result = func(**kwargs) - # Load the output things that - outthings = [] - for of in outfiles: - # were specified as LOAD + # make a _Reults object to store + # the output. If we are decorating + # another _FileOrThing, the + # results will get merged together + # into a single _Results dict. + if not isinstance(result, _FileOrThing._Results): + result = _FileOrThing._Results(result) - # output file didn't get created - if not op.exists(of): - ot = None + # Load the LOADed outputs + for oname, ofile in outfiles.items(): - # load the thing - else: - ot = self.__loadThing(of) + if not op.exists(ofile): oval = None + else: oval = self.__load(ofile) - outthings.append(ot) + result[oname] = oval - if isFileOrThing: - things = result[1:] - result = result[0] - return tuple([result] + list(things) + outthings) - else: - return tuple([result] + outthings) + return result - def __prepareThings(self, workdir, kwargs): - """ + def __prepareArgs(self, workdir, kwargs): + """Prepares all input and output arguments to be passed to the + decorated function. Any arguments with a value of :data:`LOAD` are + passed to the ``prepOut`` function specified at :meth:`__init__`. + All other arguments are passed through the ``prepIn`` function. + + :arg workdir: Directory in which all temporary files should be stored. + + :arg kwargs: Keyword arguments to be passed to the decorated function. + + :returns: A tuple containing: + + - An updated copy of ``kwargs``, ready to be passed + into the function + + - A dictionary of ``{ name : filename }`` mappings, + for all arguments with a value of ``LOAD``. """ kwargs = dict(kwargs) - infiles = [] - outfiles = [] + outfiles = dict() - for tname in self.__things: + for name in self.__things: - tval = kwargs.get(tname, None) + val = kwargs.get(name, None) - if tval is None: + if val is None: continue - tval, infile, outfile = self.__prepareThing(workdir, tname, tval) + if val == LOAD: + + outfile = self.__prepOut(workdir, name, val) + + if outfile is not None: + kwargs[ name] = outfile + outfiles[name] = outfile + else: - if infile is not None: infiles .append(infile) - if outfile is not None: outfiles.append(outfile) + infile = self.__prepIn(workdir, name, val) - kwargs[tname] = tval + if infile is not None: + kwargs[name] = infile - return kwargs, infiles, outfiles + return kwargs, outfiles def fileOrImage(*imgargs): @@ -420,54 +489,32 @@ def fileOrImage(*imgargs): image objects. """ - def prepareArg(workdir, name, val): + def prepIn(workdir, name, val): - newval = val - infile = None - outfile = None + infile = None - # This is an input image which has - # been specified as an in-memory - # nibabel image. if the image has - # a backing file, replace the image - # object with the file name. - # Otherwise, save the image out to - # a temporary file, and replace the - # image with the file name. if isinstance(val, nib.nifti1.Nifti1Image): - imgfile = val.get_filename() + infile = val.get_filename() # in-memory image - we have # to save it out to a file - if imgfile is None: - - hd, imgfile = tempfile.mkstemp(fslimage.defaultExt()) - + if infile is None: + hd, infile = tempfile.mkstemp(fslimage.defaultExt()) os.close(hd) - val.to_filename(imgfile) - infile = imgfile - - # replace the image with its - # file name - newval = imgfile + val.to_filename(infile) - # This is an output image, and the - # caller has requested that it be - # returned from the function call - # as an in-memory image. - elif val == LOAD: - newval = op.join(workdir, '{}.nii.gz'.format(name)) - outfile = newval + return infile - return newval, infile, outfile + def prepOut(workdir, name, val): + return op.join(workdir, '{}.nii.gz'.format(name)) - def loadImage(path): + def load(path): # create an independent in-memory # copy of the image file img = nib.load(path) return nib.nifti1.Nifti1Image(img.get_data(), None, img.header) - return _FileOrThing(prepareArg, loadImage, *imgargs) + return _FileOrThing(prepIn, prepOut, load, *imgargs) def fileOrArray(*arrargs): @@ -475,32 +522,20 @@ def fileOrArray(*arrargs): to text files, and output files can be loaded and returned as Numpy arrays. """ - def prepareArg(workdir, name, val): + def prepIn(workdir, name, val): - newval = val - infile = None - outfile = None + infile = None - # Input has been provided as a numpy - # array - save it to a file, and - # replace the argument with the file - # name if isinstance(val, np.ndarray): - - hd, arrfile = tempfile.mkstemp('.txt') - + hd, infile = tempfile.mkstemp('.txt') os.close(hd) + np.savetxt(infile, val, fmt='%0.18f') - np.savetxt(arrfile, val, fmt='%0.18f') - newval = arrfile + return infile - # This is an output, and the caller has - # requested that it be returned from the - # function call as an in-memory array. - elif val == LOAD: - newval = op.join(workdir, '{}.txt'.format(name)) - outfile = newval + def prepOut(workdir, name, val): + return op.join(workdir, '{}.txt'.format(name)) - return newval, infile, outfile + load = np.loadtxt - return _FileOrThing(prepareArg, np.loadtxt, *arrargs) + return _FileOrThing(prepIn, prepOut, load, *arrargs)