Skip to content

fix: python 2 Function interfaces recompatibility #2093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 12, 2017
Merged
2 changes: 1 addition & 1 deletion doc/users/saving_workflows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion nipype/interfaces/utility/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions nipype/pipeline/engine/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions nipype/pipeline/engine/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions nipype/utils/functions.py
Original file line number Diff line number Diff line change
@@ -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
43 changes: 1 addition & 42 deletions nipype/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
41 changes: 41 additions & 0 deletions nipype/utils/tests/test_functions.py
Original file line number Diff line number Diff line change
@@ -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()
25 changes: 2 additions & 23 deletions nipype/utils/tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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)
Expand Down