Repository: nipy/nipype

Issue:
Function interface crash when upgrading nipype
### Summary
The function interface crashes, in a code that was working in an older nipype installation (version details below). From the error, it seems I am doing something wrong when importing other functions inside of the function interface, but I don't know what it is exactly or how to fix it.

The function itself is intended to convert html into pdf and send it as an email attachment.

### Actual behavior
The function below does not run, and this is the crash information:
```
170601-20:23:45,389 workflow INFO:
	 Traceback (most recent call last):
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/pipeline/plugins/multiproc.py", line 52, in run_node
    result['result'] = node.run(updatehash=updatehash)
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/pipeline/engine/nodes.py", line 372, in run
    self._run_interface()
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/pipeline/engine/nodes.py", line 482, in _run_interface
    self._result = self._run_command(execute)
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/pipeline/engine/nodes.py", line 613, in _run_command
    result = self._interface.run()
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/interfaces/base.py", line 1081, in run
    runtime = self._run_wrapper(runtime)
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/interfaces/base.py", line 1029, in _run_wrapper
    runtime = self._run_interface(runtime)
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/interfaces/utility/wrappers.py", line 146, in _run_interface
    self.imports)
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/utils/misc.py", line 102, in create_function_from_source
    raise_from(RuntimeError(msg), e)
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/future/utils/__init__.py", line 454, in raise_from
    raise e
RuntimeError: 
Error executing function:
 
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
Interface Function failed to run. 

170601-20:23:46,403 workflow INFO:
Traceback (most recent call last):
	 ***********************************
  File "/home/myanezl2/workspace/c3nl/QA_matlab/QA_Matlab.py", line 192, in <module>
170601-20:23:46,403 workflow ERROR:
	 could not run node: QA.QA_email
170601-20:23:46,404 workflow INFO:
	 crashfile: /home/myanezl2/workspace/c3nl/QA_matlab/crash-20170601-202344-myanezl2-QA_email-98e03d45-765c-4bec-87c5-c4e253113c83.pklz
170601-20:23:46,404 workflow INFO:
	 ***********************************
    QA.run('MultiProc', plugin_args={'n_procs': 2})
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/pipeline/engine/workflows.py", line 590, in run
    runner.run(execgraph, updatehash=updatehash, config=self.config)
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/pipeline/plugins/base.py", line 279, in run
    report_nodes_not_run(notrun)
  File "/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype/pipeline/plugins/base.py", line 101, in report_nodes_not_run
    raise RuntimeError(('Workflow did not execute cleanly. '
RuntimeError: Workflow did not execute cleanly. Check log for details

Process finished with exit code 1
```
### Expected behavior
This function was working in this previous installation:
print(nipype.__version__)
0.11.0

print(nipype.get_info())
{'nibabel_version': '2.0.2', 'networkx_version': '1.10', 'numpy_version': '1.11.0', 'sys_platform': 'linux2', 'sys_version': '2.7.11 |Anaconda 2.4.1 (64-bit)| (default, Dec  6 2015, 18:08:32) \n[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]', 'commit_source': '(none found)', 'commit_hash': '<not found>', 'pkg_path': '/share/apps/anaconda-3-2.4.1/lib/python2.7/site-packages/nipype', 'sys_executable': '/share/apps/anaconda-3-2.4.1/bin/python', 'traits_version': '4.5.0', 'scipy_version': '0.17.0'}

### Script/Workflow details
```
def gen_qa_email(in_directory, empty_variable, in_date):
	import os
	import smtplib
	import pdfkit
	from email.mime.application import MIMEApplication
	from email.mime.multipart import MIMEMultipart
	from email.mime.text import MIMEText
	from email.mime.image import MIMEImage

	""" Convert html to pdf"""
	pdf_file = pdfkit.from_file(in_directory + '/QA_results.html', False)

	""" Create the email message container from the input args."""
	recipients = ['email@email.com']
	msg = MIMEMultipart('alternative')
	msg['Subject'] = 'QA' + in_date
	msg['From'] = 'email@email.com'
	msg['To'] = ", ".join(recipients)

	""" Attach parts into message container"""
	pdf = MIMEApplication(pdf_file, 'pdf')
	pdf.add_header('Content-Disposition', 'attachment', filename= 'QA.pdf')
	msg.attach(pdf)

	try:
		smtpserver = smtplib.SMTP("smtp.gmail.com", 587)
		smtpserver.ehlo()
		smtpserver.starttls()
		smtpserver.ehlo
		smtpserver.login(''email@email.com']', 'password')
		smtpserver.sendmail(''email@email.com'], recipients, msg.as_string())
		smtpserver.quit()
	except smtplib.SMTPAuthenticationError as e:
		print "Unable to send message: %s" % e
	return None

QA_email = Node(name='QA_email', interface=Function(input_names=['in_directory', 'empty_variable', 'in_date'], output_names=[], function=gen_qa_email))
QA_email.inputs.in_date = experiment_date
```
### Platform details:

print(nipype.__version__)
0.13.1

print(nipype.get_info())
{'nibabel_version': '2.1.0', 'sys_executable': '/apps/software/anaconda/4.3.1/bin/python', 'networkx_version': '1.11', 'numpy_version': '1.11.3', 'sys_platform': 'linux2', 'sys_version': '2.7.13 |Anaconda 4.3.1 (64-bit)| (default, Dec 20 2016, 23:09:15) \n[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]', 'commit_source': u'installation', 'commit_hash': u'8946bcab9', 'pkg_path': '/apps/software/anaconda/4.3.1/lib/python2.7/site-packages/nipype', 'nipype_version': u'0.13.1', 'traits_version': '4.6.0', 'scipy_version': '0.18.1'}


Discussion:

Patch:
diff --git a/doc/users/saving_workflows.rst b/doc/users/saving_workflows.rst
index c97751eea..33d1e8a11 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 4de11d7ea..6885d7218 100644
--- a/nipype/interfaces/utility/wrappers.py
+++ b/nipype/interfaces/utility/wrappers.py
@@ -24,7 +24,7 @@ from ..base import (traits, DynamicTraitedSpec, Undefined, isdefined, runtime_pr
                     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 25b12ab60..fe8228c8a 100644
--- a/nipype/pipeline/engine/utils.py
+++ b/nipype/pipeline/engine/utils.py
@@ -31,7 +31,8 @@ import networkx as nx
 
 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 f30ed5005..14c4920a7 100644
--- a/nipype/pipeline/engine/workflows.py
+++ b/nipype/pipeline/engine/workflows.py
@@ -36,8 +36,8 @@ import networkx as nx
 
 
 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 000000000..aa72d8500
--- /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 552e24c43..095e6b88f 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))


Test patch:
diff --git a/nipype/utils/tests/test_functions.py b/nipype/utils/tests/test_functions.py
new file mode 100644
index 000000000..1d9b9dac7
--- /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 f2780a584..1685fd645 100644
--- a/nipype/utils/tests/test_misc.py
+++ b/nipype/utils/tests/test_misc.py
@@ -8,9 +8,8 @@ from builtins import next
 
 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)