Repository: nipype/pydra

Issue:
`Workflow` constructor raises `TypeError` when `input_spec` is an instance of `SpecInfo`
The documentation of the `Workflow` constructor says that `input_spec` can be a list of strings, a `SpecInfo`, or a `BaseSpec` instance:

https://github.com/nipype/pydra/blob/219c72170984f013eeb1fdc3d87f1d4a58d0dfab/pydra/engine/core.py#L807

However, when providing a `SpecInfo` I get a `TypeError: unhashable type: 'SpecInfo'`.

Looking at the code, it seems like this case isn't handled properly such that we end up iterating over the `SpecInfo` instance (line 860), triggering the error described above:

https://github.com/nipype/pydra/blob/219c72170984f013eeb1fdc3d87f1d4a58d0dfab/pydra/engine/core.py#L843-L863

 It *could* be that `SpecInfo` was supposed to inherit from `BaseSpec`, but it doesn't seem to be the case atm:

https://github.com/nipype/pydra/blob/219c72170984f013eeb1fdc3d87f1d4a58d0dfab/pydra/engine/specs.py#L65

Discussion:

Patch:
diff --git a/pydra/engine/core.py b/pydra/engine/core.py
index 42b78054..b66549e2 100644
--- a/pydra/engine/core.py
+++ b/pydra/engine/core.py
@@ -843,6 +843,12 @@ class Workflow(TaskBase):
         if input_spec:
             if isinstance(input_spec, BaseSpec):
                 self.input_spec = input_spec
+            elif isinstance(input_spec, SpecInfo):
+                if not any([x == BaseSpec for x in input_spec.bases]):
+                    raise ValueError(
+                        "Provided SpecInfo must have BaseSpec as it's base."
+                    )
+                self.input_spec = input_spec
             else:
                 self.input_spec = SpecInfo(
                     name="Inputs",


Test patch:
diff --git a/pydra/engine/tests/test_workflow.py b/pydra/engine/tests/test_workflow.py
index 17d3d6b3..4b021d48 100644
--- a/pydra/engine/tests/test_workflow.py
+++ b/pydra/engine/tests/test_workflow.py
@@ -30,6 +30,7 @@ from .utils import (
 from ..submitter import Submitter
 from ..core import Workflow
 from ... import mark
+from ..specs import SpecInfo, BaseSpec, ShellSpec
 
 
 def test_wf_no_input_spec():
@@ -37,6 +38,36 @@ def test_wf_no_input_spec():
         Workflow(name="workflow")
 
 
+def test_wf_specinfo_input_spec():
+    input_spec = SpecInfo(
+        name="Input",
+        fields=[
+            ("a", str, "", {"mandatory": True}),
+            ("b", dict, {"foo": 1, "bar": False}, {"mandatory": False}),
+        ],
+        bases=(BaseSpec,),
+    )
+    wf = Workflow(
+        name="workflow",
+        input_spec=input_spec,
+    )
+    for x in ["a", "b"]:
+        assert hasattr(wf.inputs, x)
+    assert wf.inputs.a == ""
+    assert wf.inputs.b == {"foo": 1, "bar": False}
+    bad_input_spec = SpecInfo(
+        name="Input",
+        fields=[
+            ("a", str, {"mandatory": True}),
+        ],
+        bases=(ShellSpec,),
+    )
+    with pytest.raises(
+        ValueError, match="Provided SpecInfo must have BaseSpec as it's base."
+    ):
+        Workflow(name="workflow", input_spec=bad_input_spec)
+
+
 def test_wf_name_conflict1():
     """raise error when workflow name conflicts with a class attribute or method"""
     with pytest.raises(ValueError) as excinfo1: