diff --git a/doc/users/saving_workflows.rst b/doc/users/saving_workflows.rst index c97751eead..33d1e8a118 100644 --- a/doc/users/saving_workflows.rst +++ b/doc/users/saving_workflows.rst @@ -55,7 +55,7 @@ This will create a file "outputtestsave.py" with the following content: from nipype.pipeline.engine import Workflow, Node, MapNode from nipype.interfaces.utility import IdentityInterface from nipype.interfaces.utility import Function - from nipype.utils.misc import getsource + from nipype.utils.functions import getsource from nipype.interfaces.fsl.preprocess import BET from nipype.interfaces.fsl.utils import ImageMaths # Functions diff --git a/nipype/interfaces/utility/wrappers.py b/nipype/interfaces/utility/wrappers.py index 4de11d7ea8..6885d7218e 100644 --- a/nipype/interfaces/utility/wrappers.py +++ b/nipype/interfaces/utility/wrappers.py @@ -24,7 +24,7 @@ BaseInterfaceInputSpec, get_max_resources_used) from ..io import IOBase, add_traits from ...utils.filemanip import filename_to_list -from ...utils.misc import getsource, create_function_from_source +from ...utils.functions import getsource, create_function_from_source logger = logging.getLogger('interface') if runtime_profile: diff --git a/nipype/pipeline/engine/utils.py b/nipype/pipeline/engine/utils.py index 25b12ab607..fe8228c8ac 100644 --- a/nipype/pipeline/engine/utils.py +++ b/nipype/pipeline/engine/utils.py @@ -31,7 +31,8 @@ from ...utils.filemanip import (fname_presuffix, FileNotFoundError, to_str, filename_to_list, get_related_files) -from ...utils.misc import create_function_from_source, str2bool +from ...utils.misc import str2bool +from ...utils.functions import create_function_from_source from ...interfaces.base import (CommandLine, isdefined, Undefined, InterfaceResult) from ...interfaces.utility import IdentityInterface @@ -100,7 +101,7 @@ def _write_inputs(node): lines[-1] = lines[-1].replace(' %s(' % funcname, ' %s_1(' % funcname) funcname = '%s_1' % funcname - lines.append('from nipype.utils.misc import getsource') + lines.append('from nipype.utils.functions import getsource') lines.append("%s.inputs.%s = getsource(%s)" % (nodename, key, funcname)) diff --git a/nipype/pipeline/engine/workflows.py b/nipype/pipeline/engine/workflows.py index f30ed50051..14c4920a72 100644 --- a/nipype/pipeline/engine/workflows.py +++ b/nipype/pipeline/engine/workflows.py @@ -36,8 +36,8 @@ from ... import config, logging -from ...utils.misc import (unflatten, package_check, str2bool, - getsource, create_function_from_source) +from ...utils.misc import (unflatten, package_check, str2bool) +from ...utils.functions import (getsource, create_function_from_source) from ...interfaces.base import (traits, InputMultiPath, CommandLine, Undefined, TraitedSpec, DynamicTraitedSpec, Bunch, InterfaceResult, md5, Interface, diff --git a/nipype/utils/functions.py b/nipype/utils/functions.py new file mode 100644 index 0000000000..aa72d85009 --- /dev/null +++ b/nipype/utils/functions.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +Handles custom functions used in Function interface. Future imports +are avoided to keep namespace as clear as possible. +""" +from builtins import next, str +from future.utils import raise_from +import inspect +from textwrap import dedent + +def getsource(function): + """Returns the source code of a function""" + return dedent(inspect.getsource(function)) + + +def create_function_from_source(function_source, imports=None): + """Return a function object from a function source + + Parameters + ---------- + function_source : unicode string + unicode string defining a function + imports : list of strings + list of import statements in string form that allow the function + to be executed in an otherwise empty namespace + """ + ns = {} + import_keys = [] + + try: + if imports is not None: + for statement in imports: + exec(statement, ns) + import_keys = list(ns.keys()) + exec(function_source, ns) + + except Exception as e: + msg = 'Error executing function\n{}\n'.format(function_source) + msg += ("Functions in connection strings have to be standalone. " + "They cannot be declared either interactively or inside " + "another function or inline in the connect string. Any " + "imports should be done inside the function.") + raise_from(RuntimeError(msg), e) + ns_funcs = list(set(ns) - set(import_keys + ['__builtins__'])) + assert len(ns_funcs) == 1, "Function or inputs are ill-defined" + func = ns[ns_funcs[0]] + return func diff --git a/nipype/utils/misc.py b/nipype/utils/misc.py index 552e24c435..095e6b88f3 100644 --- a/nipype/utils/misc.py +++ b/nipype/utils/misc.py @@ -3,7 +3,7 @@ # vi: set ft=python sts=4 ts=4 sw=4 et: """Miscellaneous utility functions """ -from __future__ import print_function, division, unicode_literals, absolute_import +from __future__ import print_function, unicode_literals, division, absolute_import from future import standard_library standard_library.install_aliases() from builtins import next, str @@ -66,47 +66,6 @@ def trim(docstring, marker=None): return '\n'.join(trimmed) -def getsource(function): - """Returns the source code of a function""" - src = dedent(inspect.getsource(function)) - return src - - -def create_function_from_source(function_source, imports=None): - """Return a function object from a function source - - Parameters - ---------- - function_source : pickled string - string in pickled form defining a function - imports : list of strings - list of import statements in string form that allow the function - to be executed in an otherwise empty namespace - """ - ns = {} - import_keys = [] - try: - if imports is not None: - for statement in imports: - exec(statement, ns) - import_keys = list(ns.keys()) - exec(function_source, ns) - - except Exception as e: - msg = '\nError executing function:\n %s\n' % function_source - msg += '\n'.join(["Functions in connection strings have to be standalone.", - "They cannot be declared either interactively or inside", - "another function or inline in the connect string. Any", - "imports should be done inside the function" - ]) - raise_from(RuntimeError(msg), e) - ns_funcs = list(set(ns) - set(import_keys + ['__builtins__'])) - assert len(ns_funcs) == 1, "Function or inputs are ill-defined" - funcname = ns_funcs[0] - func = ns[funcname] - return func - - def find_indices(condition): "Return the indices where ravel(condition) is true" res, = np.nonzero(np.ravel(condition)) diff --git a/nipype/utils/tests/test_functions.py b/nipype/utils/tests/test_functions.py new file mode 100644 index 0000000000..1d9b9dac7a --- /dev/null +++ b/nipype/utils/tests/test_functions.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +import sys +import pytest +from nipype.utils.functions import (getsource, create_function_from_source) + +def _func1(x): + return x**3 + +def test_func_to_str(): + + def func1(x): + return x**2 + + # Should be ok with both functions! + for f in _func1, func1: + f_src = getsource(f) + f_recreated = create_function_from_source(f_src) + assert f(2.3) == f_recreated(2.3) + +def test_func_to_str_err(): + bad_src = "obbledygobbledygook" + with pytest.raises(RuntimeError): create_function_from_source(bad_src) + +def _print_statement(): + try: + exec('print ""') + return True + except SyntaxError: + return False + +def test_func_string(): + def is_string(): + return isinstance('string', str) + + wrapped_func = create_function_from_source(getsource(is_string)) + assert is_string() == wrapped_func() + +@pytest.mark.skipif(sys.version_info[0] > 2, reason="breaks python 3") +def test_func_print_py2(): + wrapped_func = create_function_from_source(getsource(_print_statement)) + assert wrapped_func() diff --git a/nipype/utils/tests/test_misc.py b/nipype/utils/tests/test_misc.py index f2780a584f..1685fd645e 100644 --- a/nipype/utils/tests/test_misc.py +++ b/nipype/utils/tests/test_misc.py @@ -8,9 +8,8 @@ import pytest -from nipype.utils.misc import (container_to_string, getsource, - create_function_from_source, str2bool, flatten, - unflatten) +from nipype.utils.misc import (container_to_string, str2bool, + flatten, unflatten) def test_cont_to_str(): @@ -35,26 +34,6 @@ def test_cont_to_str(): assert (container_to_string(123) == '123') -def _func1(x): - return x**3 - - -def test_func_to_str(): - - def func1(x): - return x**2 - - # Should be ok with both functions! - for f in _func1, func1: - f_src = getsource(f) - f_recreated = create_function_from_source(f_src) - assert f(2.3) == f_recreated(2.3) - -def test_func_to_str_err(): - bad_src = "obbledygobbledygook" - with pytest.raises(RuntimeError): create_function_from_source(bad_src) - - @pytest.mark.parametrize("string, expected", [ ("yes", True), ("true", True), ("t", True), ("1", True), ("no", False), ("false", False), ("n", False), ("f", False), ("0", False)