Commit 577cf431 authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

Cleaned up/documented wrapperutils

parent cde39b46
......@@ -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)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment