Repository: nipy/nipype

Issue:
Issues when exporting workflows
### Summary
During the last OHBM Hackathon, I found two bugs while working on my project: these arise when one tries to export a given workflow using the respective `export()` method (for example assembled combining multiple ones at runtime) to a .py file. I managed to fix them following @satra advices on Mattermost, and I will open a pull request shortly.

### Actual behavior

1. Any node defined with the `fields` parameter, e.g.:
```
inputnode = pe.Node(
   interface=util.IdentityInterface(fields=['dwi','bvecs','bvals']),
   name="inputnode")
```
will be misrepresented when the workflow is exported using `wf.export('wf.py')`, e.g.:
```
workflow_inputnode = Node(IdentityInterface(mandatory_inputs=True), name="workflow_inputnode")
```
This issue can be fixed removing the following line (which skips the `fields` parameter): https://github.com/nipy/nipype/blob/5116ee208205c8050f87fc2a45bb0f35e3dd1c0a/nipype/pipeline/engine/utils.py#L368

2. Once the previous issue is fixed, the generated Python file cannot be executed because of the following error:
```
NameError: name \'stream\' is not defined\n'
```
This is caused by these lines: https://github.com/nipy/nipype/blob/5116ee208205c8050f87fc2a45bb0f35e3dd1c0a/nipype/pipeline/engine/utils.py#L373-L375
To correctly handle strings and lists as parameters, @satra suggested to add an if statement:
```
                     argval = getattr(node.interface, "_%s" % arg)
                     if isinstance(argval, str):
                         filled_args.append(
                             "%s='%s'" % (arg, argval)
                         )
                     else:
                         filled_args.append(
                             "%s=%s" % (arg, argval)
                         )
```

### Expected behavior

It is expected that the `export()` method would be able to handle the `fields` parameter.

### How to replicate the behavior

Any workflow containing an `IdentityInterface` node with the `fields` parameter will replicate the issues.

### Script/Workflow details

NA

### Platform details:

NA

### Execution environment

Choose one
- Container NA
- My python environment inside container NA
- My python environment outside container 3.7.3

[FIX] Issues when exporting workflows
<!--

Pull-request guidelines
-----------------------

1. If you would like to list yourself as a Nipype contributor and your name is not mentioned please modify .zenodo.json file.
2. By submitting this request you acknowledge that your contributions are available under the Apache 2 license.
3. Use a descriptive prefix for your PR: ENH (enhancement), FIX, TST, DOC, STY, REF (refactor), WIP (Work in progress)
4. Run `make check-before-commit` before submitting the PR.

-->
## Summary
<!-- Please reference any related issue and use fixes/close to automatically
close them, if pertinent -->

Fixes #3230 .

## List of changes proposed in this PR (pull-request)
<!-- We suggest using bullets (indicated by * or -) and filled checkboxes [x] here -->

* Removed line that skips the `field` parameter during export:
https://github.com/nipy/nipype/blob/5116ee208205c8050f87fc2a45bb0f35e3dd1c0a/nipype/pipeline/engine/utils.py#L368
* Added an if statement to handle strings and lists during export.

## Acknowledgment

- I acknowledge that this contribution will be available under the Apache 2 license.



Discussion:

Resolving patch:
diff --git a/nipype/pipeline/engine/utils.py b/nipype/pipeline/engine/utils.py
--- a/nipype/pipeline/engine/utils.py
+++ b/nipype/pipeline/engine/utils.py
@@ -364,18 +364,12 @@ def format_node(node, format="python", include_config=False):
         importline = "from %s import %s" % (klass.__module__, klass.__class__.__name__)
         comment = "# Node: %s" % node.fullname
         spec = signature(node.interface.__init__)
-        args = [p.name for p in list(spec.parameters.values())]
-        args = args[1:]
-        if args:
-            filled_args = []
-            for arg in args:
-                if hasattr(node.interface, "_%s" % arg):
-                    filled_args.append(
-                        "%s=%s" % (arg, getattr(node.interface, "_%s" % arg))
-                    )
-            args = ", ".join(filled_args)
-        else:
-            args = ""
+        filled_args = []
+        for param in spec.parameters.values():
+            val = getattr(node.interface, f"_{param.name}", None)
+            if val is not None:
+                filled_args.append(f"{param.name}={val!r}")
+        args = ", ".join(filled_args)
         klass_name = klass.__class__.__name__
         if isinstance(node, MapNode):
             nodedef = '%s = MapNode(%s(%s), iterfield=%s, name="%s")' % (


Test patch:
diff --git a/nipype/pipeline/engine/tests/test_utils.py b/nipype/pipeline/engine/tests/test_utils.py
--- a/nipype/pipeline/engine/tests/test_utils.py
+++ b/nipype/pipeline/engine/tests/test_utils.py
@@ -11,7 +11,12 @@
 from ....interfaces import base as nib
 from ....interfaces import utility as niu
 from .... import config
-from ..utils import clean_working_directory, write_workflow_prov, load_resultfile
+from ..utils import (
+    clean_working_directory,
+    write_workflow_prov,
+    load_resultfile,
+    format_node,
+)
 
 
 class InputSpec(nib.TraitedSpec):
@@ -327,3 +332,11 @@ def test_save_load_resultfile(tmpdir, use_relative):
             )
 
     config.set("execution", "use_relative_paths", old_use_relative)
+
+
+def test_format_node():
+    node = pe.Node(niu.IdentityInterface(fields=["a", "b"]), name="node")
+    serialized = format_node(node)
+    workspace = {"Node": pe.Node}
+    exec("\n".join(serialized), workspace)
+    assert workspace["node"].interface._fields == node.interface._fields