Skip to content

Commit 4b86d97

Browse files
committed
Merge pull request #551 from mwaskom/enh/function_imports
ENH Increased flexibility in Function interface
2 parents 1bb69be + 5ad3eaf commit 4b86d97

File tree

5 files changed

+101
-14
lines changed

5 files changed

+101
-14
lines changed

CHANGES

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ Next release
99
* ENH: Minor improvements to FSL's FUGUE and FLIRT interfaces
1010
* ENH: Added optional dilation of parcels in cmtk.Parcellate
1111
* ENH: Interpolation mode added to afni.Resample
12+
* ENH: Function interface can accept a list of strings containing import statements
13+
that allow external functions to run without their imports defined in the
14+
function body
1215

1316
* FIX: SpecifyModel works with 3D files correctly now.
1417

doc/users/function_interface.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ be imported within the function itself::
7171
Without explicitly importing Nibabel in the body of the function, this
7272
would fail.
7373

74+
Alternatively, it is possible to provide a list of strings corresponding
75+
to the imports needed to execute a function as a parameter of the `Function`
76+
constructor. This allows for the use of external functions that do not
77+
import all external definitions inside the function body.
78+
7479
Hello World - Function interface in a workflow
7580
----------------------------------------------
7681

nipype/interfaces/tests/test_utility.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import shutil
55
from tempfile import mkdtemp
66

7-
from nipype.testing import assert_equal, assert_true
7+
import numpy as np
8+
from nipype.testing import assert_equal, assert_true, assert_raises
89
from nipype.interfaces import utility
910
import nipype.pipeline.engine as pe
1011

@@ -66,3 +67,49 @@ def increment_array(in_array):
6667
# Clean up
6768
os.chdir(origdir)
6869
shutil.rmtree(tempdir)
70+
71+
72+
def make_random_array(size):
73+
74+
return np.random.randn(size, size)
75+
76+
77+
def should_fail():
78+
79+
tempdir = os.path.realpath(mkdtemp())
80+
origdir = os.getcwd()
81+
os.chdir(tempdir)
82+
83+
node = pe.Node(utility.Function(input_names=["size"],
84+
output_names=["random_array"],
85+
function=make_random_array),
86+
name="should_fail")
87+
try:
88+
node.inputs.size = 10
89+
node.run()
90+
finally:
91+
os.chdir(origdir)
92+
shutil.rmtree(tempdir)
93+
94+
95+
assert_raises(NameError, should_fail)
96+
97+
98+
def test_function_with_imports():
99+
100+
tempdir = os.path.realpath(mkdtemp())
101+
origdir = os.getcwd()
102+
os.chdir(tempdir)
103+
104+
node = pe.Node(utility.Function(input_names=["size"],
105+
output_names=["random_array"],
106+
function=make_random_array,
107+
imports=["import numpy as np"]),
108+
name="should_not_fail")
109+
print node.inputs.function_str
110+
try:
111+
node.inputs.size = 10
112+
node.run()
113+
finally:
114+
os.chdir(origdir)
115+
shutil.rmtree(tempdir)

nipype/interfaces/utility.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
33
import os
44
import re
5+
from cPickle import dumps, loads
56
import numpy as np
67
import nibabel as nb
78

@@ -11,7 +12,7 @@
1112
InputMultiPath, BaseInterface, BaseInterfaceInputSpec)
1213
from nipype.interfaces.io import IOBase, add_traits
1314
from nipype.testing import assert_equal
14-
from nipype.utils.misc import getsource, create_function_from_source, dumps
15+
from nipype.utils.misc import getsource, create_function_from_source
1516

1617

1718
class IdentityInterface(IOBase):
@@ -335,7 +336,8 @@ class Function(IOBase):
335336
input_spec = FunctionInputSpec
336337
output_spec = DynamicTraitedSpec
337338

338-
def __init__(self, input_names, output_names, function=None, **inputs):
339+
def __init__(self, input_names, output_names, function=None, imports=None,
340+
**inputs):
339341
"""
340342
341343
Parameters
@@ -344,7 +346,15 @@ def __init__(self, input_names, output_names, function=None, **inputs):
344346
input_names: single str or list
345347
names corresponding to function inputs
346348
output_names: single str or list
347-
names corresponding to function outputs. has to match the number of outputs
349+
names corresponding to function outputs.
350+
has to match the number of outputs
351+
function : callable
352+
callable python object. must be able to execute in an
353+
isolated namespace (possibly in concert with the ``imports``
354+
parameter)
355+
imports : list of strings
356+
list of import statements that allow the function to execute
357+
in an otherwise empty namespace
348358
"""
349359

350360
super(Function, self).__init__(**inputs)
@@ -354,15 +364,18 @@ def __init__(self, input_names, output_names, function=None, **inputs):
354364
self.inputs.function_str = getsource(function)
355365
except IOError:
356366
raise Exception('Interface Function does not accept ' \
357-
'function objects defined interactively in a python session')
367+
'function objects defined interactively ' \
368+
'in a python session')
358369
elif isinstance(function, str):
359370
self.inputs.function_str = dumps(function)
360371
else:
361372
raise Exception('Unknown type of function')
362-
self.inputs.on_trait_change(self._set_function_string, 'function_str')
373+
self.inputs.on_trait_change(self._set_function_string,
374+
'function_str')
363375
self._input_names = filename_to_list(input_names)
364376
self._output_names = filename_to_list(output_names)
365377
add_traits(self.inputs, [name for name in self._input_names])
378+
self.imports = imports
366379
self._out = {}
367380
for name in self._output_names:
368381
self._out[name] = None
@@ -373,7 +386,8 @@ def _set_function_string(self, obj, name, old, new):
373386
function_source = getsource(new)
374387
elif isinstance(new, str):
375388
function_source = dumps(new)
376-
self.inputs.trait_set(trait_change_notify=False, **{'%s' % name: function_source})
389+
self.inputs.trait_set(trait_change_notify=False,
390+
**{'%s' % name: function_source})
377391

378392
def _add_output_traits(self, base):
379393
undefined_traits = {}
@@ -384,7 +398,8 @@ def _add_output_traits(self, base):
384398
return base
385399

386400
def _run_interface(self, runtime):
387-
function_handle = create_function_from_source(self.inputs.function_str)
401+
function_handle = create_function_from_source(self.inputs.function_str,
402+
self.imports)
388403

389404
args = {}
390405
for name in self._input_names:

nipype/utils/misc.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,38 @@ def getsource(function):
4646
src = dumps(dedent(inspect.getsource(function)))
4747
return src
4848

49-
def create_function_from_source(function_source):
49+
def create_function_from_source(function_source, imports=None):
5050
"""Return a function object from a function source
51+
52+
Parameters
53+
----------
54+
function_source : pickled string
55+
string in pickled form defining a function
56+
imports : list of strings
57+
list of import statements in string form that allow the function
58+
to be executed in an otherwise empty namespace
5159
"""
5260
ns = {}
61+
import_keys = []
5362
try:
63+
if imports is not None:
64+
for statement in imports:
65+
exec statement in ns
66+
import_keys = ns.keys()
67+
5468
exec loads(function_source) in ns
69+
5570
except Exception, msg:
5671
msg = str(msg) + '\nError executing function:\n %s\n'%function_source
57-
msg += '\n'.join( ["Functions in connection strings have to be standalone.",
58-
"They cannot be declared either interactively or inside",
59-
"another function or inline in the connect string. Any",
60-
"imports should be done inside the function"
72+
msg += '\n'.join(["Functions in connection strings have to be standalone.",
73+
"They cannot be declared either interactively or inside",
74+
"another function or inline in the connect string. Any",
75+
"imports should be done inside the function"
6176
])
6277
raise RuntimeError(msg)
63-
funcname = [name for name in ns.keys() if name != '__builtins__'][0]
78+
ns_funcs = list(set(ns) - set(import_keys + ['__builtins__']))
79+
assert len(ns_funcs) == 1, "Function or inputs are ill-defined"
80+
funcname = ns_funcs[0]
6481
func = ns[funcname]
6582
return func
6683

0 commit comments

Comments
 (0)