Repository: nipy/nipype

Issue:
Import error for `nipype.interfaces.dipy.base`
### Summary

`import nipype.interfaces.dipy` generates an error for me, with Python 3.9 and 3.8, Dipy 4.12 (latest release) and latest release or current master of nipype.

### Actual behavior

Running the above import generates the error `TypeError: 'NoneType' object is not iterable` generated from `dipy_to_nipype_interface` at: https://github.com/nipy/nipype/blob/f756b71117493caea093bac79d576207d8f96482/nipype/interfaces/dipy/base.py#L200
This function is executed during import in [`interfaces.dipy.preprocess`](https://github.com/nipy/nipype/blob/f756b71117493caea093bac79d576207d8f96482/nipype/interfaces/dipy/preprocess.py#L27). It seems this works correctly for several workflows (LabelsBundles, MedianOtsu, RecoBundles, LocalFiberTrackingPAM, PFTrackingPAM) but crashes for [GibbsRingingFlow](https://github.com/dipy/dipy/blob/61a6f14015b8cd58b48501e1ef6fc853e55b6807/dipy/workflows/denoise.py#L306) because `default_values` is None.

### How to replicate the behavior
- create a fresh venv
- pip install the latest dipy and nipype packages
- open python and run `import nipype.interfaces.dipy`

### Expected behavior

A working import. This seems an easy fix, if others can reproduce the issue I would be happy to make a PR replacing https://github.com/nipy/nipype/blob/f756b71117493caea093bac79d576207d8f96482/nipype/interfaces/dipy/base.py#L192 with:

```python
    default_values = inspect.getfullargspec(flow.run).defaults or []
```

### Platform details:

<!-- Please run the following code from your shell and place the output between the triple ticks, below.
python -c "import nipype; from pprint import pprint; pprint(nipype.get_info())"
-->

```
{'commit_hash': 'f756b7111',
 'commit_source': 'repository',
 'networkx_version': '2.6.3',
 'nibabel_version': '3.2.1',
 'nipype_version': '1.7.1-dev+gf756b7111',
 'numpy_version': '1.21.4',
 'pkg_path': '/home/.../basedir/nipype/nipype',
 'scipy_version': '1.7.2',
 'sys_executable': '/home/.../basedir/venv/bin/python',
 'sys_platform': 'linux',
 'sys_version': '3.9.9 (main, Nov 16 2021, 03:08:02) \n[GCC 9.3.0]',
 'traits_version': '6.3.2'}
```
(Error is the same for Python 3.8)

### Execution environment

- My python environment outside container



Discussion:
Please also report upstream to dipy. We need to make these imports more resilient to API breakages.

cc @skoudoro 
On closer inspection I realize the bug is actually a bit more complicated. The `run()` method on `GibbsRingingFlow` does have default arguments, but `inspect.getfullargspec()` breaks here because of a decorator that was added a few months ago (https://github.com/dipy/dipy/blob/61a6f14015b8cd58b48501e1ef6fc853e55b6807/dipy/workflows/denoise.py#L311). So it is not really an API change from dipy.


Thank you for pinging me. It is strange that the decorator does not keep the same function signature. I need to improve the new decorator management in Nipype.

Otherwise, it will break each time we deprecate an argument. I have a quite busy period but I will have a deeper look into it for sure. 
I suppose I should use `inspect.signature()` instead of `inspect.getfullargspec()`

Resolving patch:
diff --git a/nipype/info.py b/nipype/info.py
--- a/nipype/info.py
+++ b/nipype/info.py
@@ -162,7 +162,7 @@ def get_nipype_gitversion():
 EXTRA_REQUIRES = {
     "data": ["datalad"],
     "doc": [
-        "dipy!=1.4.1",
+        "dipy",
         "ipython",
         "matplotlib",
         "nbsphinx",
@@ -172,7 +172,7 @@ def get_nipype_gitversion():
         "sphinxcontrib-napoleon",
     ],
     "duecredit": ["duecredit"],
-    "nipy": ["nitime", "nilearn", "dipy!=1.4.1", "nipy", "matplotlib"],
+    "nipy": ["nitime", "nilearn", "dipy", "nipy", "matplotlib"],
     "profiler": ["psutil>=5.0"],
     "pybids": ["pybids>=0.7.0"],
     "specs": ["black"],
diff --git a/nipype/interfaces/dipy/base.py b/nipype/interfaces/dipy/base.py
--- a/nipype/interfaces/dipy/base.py
+++ b/nipype/interfaces/dipy/base.py
@@ -27,13 +27,13 @@
 
 
 def no_dipy():
-    """Check if dipy is available"""
+    """Check if dipy is available."""
     global HAVE_DIPY
     return not HAVE_DIPY
 
 
 def dipy_version():
-    """Check dipy version"""
+    """Check dipy version."""
     if no_dipy():
         return None
 
@@ -41,9 +41,7 @@ def dipy_version():
 
 
 class DipyBaseInterface(LibraryBaseInterface):
-    """
-    A base interface for py:mod:`dipy` computations
-    """
+    """A base interface for py:mod:`dipy` computations."""
 
     _pkg = "dipy"
 
@@ -57,9 +55,7 @@ class DipyBaseInterfaceInputSpec(BaseInterfaceInputSpec):
 
 
 class DipyDiffusionInterface(DipyBaseInterface):
-    """
-    A base interface for py:mod:`dipy` computations
-    """
+    """A base interface for py:mod:`dipy` computations."""
 
     input_spec = DipyBaseInterfaceInputSpec
 
@@ -90,6 +86,24 @@ def _gen_filename(self, name, ext=None):
         return out_prefix + "_" + name + ext
 
 
+def get_default_args(func):
+    """Return optional arguments of a function.
+
+    Parameters
+    ----------
+    func: callable
+
+    Returns
+    -------
+    dict
+
+    """
+    signature = inspect.signature(func)
+    return {k: v.default for k, v in signature.parameters.items()
+            if v.default is not inspect.Parameter.empty
+            }
+
+
 def convert_to_traits_type(dipy_type, is_file=False):
     """Convert DIPY type to Traits type."""
     dipy_type = dipy_type.lower()
@@ -189,7 +203,7 @@ def dipy_to_nipype_interface(cls_name, dipy_flow, BaseClass=DipyBaseInterface):
     parser = IntrospectiveArgumentParser()
     flow = dipy_flow()
     parser.add_workflow(flow)
-    default_values = inspect.getfullargspec(flow.run).defaults
+    default_values = list(get_default_args(flow.run).values())
     optional_params = [
         args + (val,) for args, val in zip(parser.optional_parameters, default_values)
     ]


Test patch:
diff --git a/nipype/interfaces/dipy/tests/test_base.py b/nipype/interfaces/dipy/tests/test_base.py
--- a/nipype/interfaces/dipy/tests/test_base.py
+++ b/nipype/interfaces/dipy/tests/test_base.py
@@ -1,4 +1,5 @@
 import pytest
+from packaging.version import Version
 from collections import namedtuple
 from ...base import traits, File, TraitedSpec, BaseInterfaceInputSpec
 from ..base import (
@@ -8,6 +9,8 @@
     DipyBaseInterface,
     no_dipy,
     get_dipy_workflows,
+    get_default_args,
+    dipy_version
 )
 
 
@@ -112,6 +115,32 @@ def test_create_interface_specs():
     assert "out_params" in current_params.keys()
 
 
+@pytest.mark.skipif(no_dipy() or Version(dipy_version()) < Version("1.4"),
+                    reason="DIPY >=1.4 required")
+def test_get_default_args():
+    from dipy.utils.deprecator import deprecated_params
+
+    def test(dummy=11, x=3):
+        return dummy, x
+
+    @deprecated_params('x', None, '0.3', '0.5', alternative='test2.y')
+    def test2(dummy=11, x=3):
+        return dummy, x
+
+    @deprecated_params(['dummy', 'x'], None, '0.3', alternative='test2.y')
+    def test3(dummy=11, x=3):
+        return dummy, x
+
+    @deprecated_params(['dummy', 'x'], None, '0.3', '0.5',
+                       alternative='test2.y')
+    def test4(dummy=11, x=3):
+        return dummy, x
+
+    expected_res = {'dummy': 11, 'x': 3}
+    for func in [test, test2, test3, test4]:
+        assert get_default_args(func) == expected_res
+
+
 @pytest.mark.skipif(no_dipy(), reason="DIPY is not installed")
 def test_dipy_to_nipype_interface():
     from dipy.workflows.workflow import Workflow
@@ -178,3 +207,4 @@ def test_get_dipy_workflows():
     test_convert_to_traits_type()
     test_create_interface_specs()
     test_dipy_to_nipype_interface()
+    test_get_default_args()