Skip to content

Commit 1a74013

Browse files
authored
Merge pull request #2150 from mgxd/fix/py2functions
ref: isolate function handling from __future__ imports
2 parents 847acfc + f709804 commit 1a74013

File tree

8 files changed

+99
-71
lines changed

8 files changed

+99
-71
lines changed

doc/users/saving_workflows.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ This will create a file "outputtestsave.py" with the following content:
5555
from nipype.pipeline.engine import Workflow, Node, MapNode
5656
from nipype.interfaces.utility import IdentityInterface
5757
from nipype.interfaces.utility import Function
58-
from nipype.utils.misc import getsource
58+
from nipype.utils.functions import getsource
5959
from nipype.interfaces.fsl.preprocess import BET
6060
from nipype.interfaces.fsl.utils import ImageMaths
6161
# Functions

nipype/interfaces/utility/wrappers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
BaseInterfaceInputSpec)
2525
from ..io import IOBase, add_traits
2626
from ...utils.filemanip import filename_to_list
27-
from ...utils.misc import getsource, create_function_from_source
27+
from ...utils.functions import getsource, create_function_from_source
2828

2929
logger = logging.getLogger('interface')
3030

nipype/pipeline/engine/utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929

3030
from ...utils.filemanip import (fname_presuffix, FileNotFoundError, to_str,
3131
filename_to_list, get_related_files)
32-
from ...utils.misc import create_function_from_source, str2bool
32+
from ...utils.misc import str2bool
33+
from ...utils.functions import create_function_from_source
3334
from ...interfaces.base import (CommandLine, isdefined, Undefined,
3435
InterfaceResult)
3536
from ...interfaces.utility import IdentityInterface
@@ -99,7 +100,7 @@ def _write_inputs(node):
99100
lines[-1] = lines[-1].replace(' %s(' % funcname,
100101
' %s_1(' % funcname)
101102
funcname = '%s_1' % funcname
102-
lines.append('from nipype.utils.misc import getsource')
103+
lines.append('from nipype.utils.functions import getsource')
103104
lines.append("%s.inputs.%s = getsource(%s)" % (nodename,
104105
key,
105106
funcname))

nipype/pipeline/engine/workflows.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@
3636

3737

3838
from ... import config, logging
39-
from ...utils.misc import (unflatten, str2bool,
40-
getsource, create_function_from_source)
39+
40+
from ...utils.misc import (unflatten, str2bool)
41+
from ...utils.functions import (getsource, create_function_from_source)
4142
from ...interfaces.base import (traits, InputMultiPath, CommandLine,
4243
Undefined, TraitedSpec, DynamicTraitedSpec,
4344
Bunch, InterfaceResult, md5, Interface,

nipype/utils/functions.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Handles custom functions used in Function interface. Future imports
4+
are avoided to keep namespace as clear as possible.
5+
"""
6+
from builtins import next, str
7+
from future.utils import raise_from
8+
import inspect
9+
from textwrap import dedent
10+
11+
def getsource(function):
12+
"""Returns the source code of a function"""
13+
return dedent(inspect.getsource(function))
14+
15+
16+
def create_function_from_source(function_source, imports=None):
17+
"""Return a function object from a function source
18+
19+
Parameters
20+
----------
21+
function_source : unicode string
22+
unicode string defining a function
23+
imports : list of strings
24+
list of import statements in string form that allow the function
25+
to be executed in an otherwise empty namespace
26+
"""
27+
ns = {}
28+
import_keys = []
29+
30+
try:
31+
if imports is not None:
32+
for statement in imports:
33+
exec(statement, ns)
34+
import_keys = list(ns.keys())
35+
exec(function_source, ns)
36+
37+
except Exception as e:
38+
msg = 'Error executing function\n{}\n'.format(function_source)
39+
msg += ("Functions in connection strings have to be standalone. "
40+
"They cannot be declared either interactively or inside "
41+
"another function or inline in the connect string. Any "
42+
"imports should be done inside the function.")
43+
raise_from(RuntimeError(msg), e)
44+
ns_funcs = list(set(ns) - set(import_keys + ['__builtins__']))
45+
assert len(ns_funcs) == 1, "Function or inputs are ill-defined"
46+
func = ns[ns_funcs[0]]
47+
return func

nipype/utils/misc.py

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# vi: set ft=python sts=4 ts=4 sw=4 et:
44
"""Miscellaneous utility functions
55
"""
6-
from __future__ import print_function, division, unicode_literals, absolute_import
6+
from __future__ import print_function, unicode_literals, division, absolute_import
77
from future import standard_library
88
standard_library.install_aliases()
99
from builtins import next, str
@@ -66,47 +66,6 @@ def trim(docstring, marker=None):
6666
return '\n'.join(trimmed)
6767

6868

69-
def getsource(function):
70-
"""Returns the source code of a function"""
71-
src = dedent(inspect.getsource(function))
72-
return src
73-
74-
75-
def create_function_from_source(function_source, imports=None):
76-
"""Return a function object from a function source
77-
78-
Parameters
79-
----------
80-
function_source : pickled string
81-
string in pickled form defining a function
82-
imports : list of strings
83-
list of import statements in string form that allow the function
84-
to be executed in an otherwise empty namespace
85-
"""
86-
ns = {}
87-
import_keys = []
88-
try:
89-
if imports is not None:
90-
for statement in imports:
91-
exec(statement, ns)
92-
import_keys = list(ns.keys())
93-
exec(function_source, ns)
94-
95-
except Exception as e:
96-
msg = '\nError executing function:\n %s\n' % function_source
97-
msg += '\n'.join(["Functions in connection strings have to be standalone.",
98-
"They cannot be declared either interactively or inside",
99-
"another function or inline in the connect string. Any",
100-
"imports should be done inside the function"
101-
])
102-
raise_from(RuntimeError(msg), e)
103-
ns_funcs = list(set(ns) - set(import_keys + ['__builtins__']))
104-
assert len(ns_funcs) == 1, "Function or inputs are ill-defined"
105-
funcname = ns_funcs[0]
106-
func = ns[funcname]
107-
return func
108-
109-
11069
def find_indices(condition):
11170
"Return the indices where ravel(condition) is true"
11271
res, = np.nonzero(np.ravel(condition))

nipype/utils/tests/test_functions.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# -*- coding: utf-8 -*-
2+
import sys
3+
import pytest
4+
from nipype.utils.functions import (getsource, create_function_from_source)
5+
6+
def _func1(x):
7+
return x**3
8+
9+
def test_func_to_str():
10+
11+
def func1(x):
12+
return x**2
13+
14+
# Should be ok with both functions!
15+
for f in _func1, func1:
16+
f_src = getsource(f)
17+
f_recreated = create_function_from_source(f_src)
18+
assert f(2.3) == f_recreated(2.3)
19+
20+
def test_func_to_str_err():
21+
bad_src = "obbledygobbledygook"
22+
with pytest.raises(RuntimeError): create_function_from_source(bad_src)
23+
24+
def _print_statement():
25+
try:
26+
exec('print ""')
27+
return True
28+
except SyntaxError:
29+
return False
30+
31+
def test_func_string():
32+
def is_string():
33+
return isinstance('string', str)
34+
35+
wrapped_func = create_function_from_source(getsource(is_string))
36+
assert is_string() == wrapped_func()
37+
38+
@pytest.mark.skipif(sys.version_info[0] > 2, reason="breaks python 3")
39+
def test_func_print_py2():
40+
wrapped_func = create_function_from_source(getsource(_print_statement))
41+
assert wrapped_func()

nipype/utils/tests/test_misc.py

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88

99
import pytest
1010

11-
from nipype.utils.misc import (container_to_string, getsource,
12-
create_function_from_source, str2bool, flatten,
13-
unflatten)
11+
from nipype.utils.misc import (container_to_string, str2bool,
12+
flatten, unflatten)
1413

1514

1615
def test_cont_to_str():
@@ -35,26 +34,6 @@ def test_cont_to_str():
3534
assert (container_to_string(123) == '123')
3635

3736

38-
def _func1(x):
39-
return x**3
40-
41-
42-
def test_func_to_str():
43-
44-
def func1(x):
45-
return x**2
46-
47-
# Should be ok with both functions!
48-
for f in _func1, func1:
49-
f_src = getsource(f)
50-
f_recreated = create_function_from_source(f_src)
51-
assert f(2.3) == f_recreated(2.3)
52-
53-
def test_func_to_str_err():
54-
bad_src = "obbledygobbledygook"
55-
with pytest.raises(RuntimeError): create_function_from_source(bad_src)
56-
57-
5837
@pytest.mark.parametrize("string, expected", [
5938
("yes", True), ("true", True), ("t", True), ("1", True),
6039
("no", False), ("false", False), ("n", False), ("f", False), ("0", False)

0 commit comments

Comments
 (0)