Repository: nipype/pydra

Issue:
template_update does not take type of value into account
the `template_update` function cannot simply check for equality of objects.

https://github.com/nipype/pydra/blob/master/pydra/engine/helpers_file.py#L508

<summary>
Code 
<details>
```
import numpy as np
fft = pydra.mark.annotate({'a': np.ndarray, 'return': float})(np.fft.fft)
fft = pydra.mark.task(fft)()
foo(a=np.ones((100)))
```
</details>
</summary>

<summary> 
Error
<details>
```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-34-af82eb0ea503> in <module>
      2 fft = pydra.mark.annotate({'a': np.ndarray, 'return': float})(np.fft.fft)
      3 fft = pydra.mark.task(fft)()
----> 4 foo(a=np.ones((100)))

~/pydra/engine/core.py in __call__(self, submitter, plugin, rerun, **kwargs)
    354                     "TODO: linear workflow execution - assign submitter or plugin for now"
    355                 )
--> 356             res = self._run(rerun=rerun, **kwargs)
    357         return res
    358 

~/pydra/engine/core.py in _run(self, rerun, **kwargs)
    378             orig_inputs = attr.asdict(self.inputs)
    379             map_copyfiles = copyfile_input(self.inputs, self.output_dir)
--> 380             modified_inputs = template_update(self.inputs, map_copyfiles)
    381             if modified_inputs is not None:
    382                 self.inputs = attr.evolve(self.inputs, **modified_inputs)

~/pydra/engine/helpers_file.py in template_update(inputs, map_copyfiles)
    506                     "{fld.name} should be a string"
    507                 )
--> 508     return {k: v for k, v in dict_.items() if getattr(inputs, k) != v}
    509 
    510 

~/pydra/engine/helpers_file.py in <dictcomp>(.0)
    506                     "{fld.name} should be a string"
    507                 )
--> 508     return {k: v for k, v in dict_.items() if getattr(inputs, k) != v}
    509 
    510 

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()```
</details>
</summary>


Discussion:

Patch:
diff --git a/pydra/engine/core.py b/pydra/engine/core.py
index 5e3e8efe..e630588a 100644
--- a/pydra/engine/core.py
+++ b/pydra/engine/core.py
@@ -381,7 +381,7 @@ class TaskBase:
             orig_inputs = attr.asdict(self.inputs)
             map_copyfiles = copyfile_input(self.inputs, self.output_dir)
             modified_inputs = template_update(self.inputs, map_copyfiles)
-            if modified_inputs is not None:
+            if modified_inputs:
                 self.inputs = attr.evolve(self.inputs, **modified_inputs)
             self.audit.start_audit(odir)
             result = Result(output=None, runtime=None, errored=False)
diff --git a/pydra/engine/helpers_file.py b/pydra/engine/helpers_file.py
index 7c2098b1..2afac73a 100644
--- a/pydra/engine/helpers_file.py
+++ b/pydra/engine/helpers_file.py
@@ -505,7 +505,7 @@ def template_update(inputs, map_copyfiles=None):
                     f"output_file_template metadata for "
                     "{fld.name} should be a string"
                 )
-    return {k: v for k, v in dict_.items() if getattr(inputs, k) != v}
+    return {k: v for k, v in dict_.items() if getattr(inputs, k) is not v}
 
 
 def is_local_file(f):
diff --git a/pydra/engine/task.py b/pydra/engine/task.py
index 4872445a..aa7fff5a 100644
--- a/pydra/engine/task.py
+++ b/pydra/engine/task.py
@@ -176,15 +176,20 @@ class FunctionTask(TaskBase):
                     )
                 else:
                     if not isinstance(return_info, tuple):
-                        return_info = (return_info,)
-                    output_spec = SpecInfo(
-                        name="Output",
-                        fields=[
-                            ("out{}".format(n + 1), t)
-                            for n, t in enumerate(return_info)
-                        ],
-                        bases=(BaseSpec,),
-                    )
+                        output_spec = SpecInfo(
+                            name="Output",
+                            fields=[("out", return_info)],
+                            bases=(BaseSpec,),
+                        )
+                    else:
+                        output_spec = SpecInfo(
+                            name="Output",
+                            fields=[
+                                ("out{}".format(n + 1), t)
+                                for n, t in enumerate(return_info)
+                            ],
+                            bases=(BaseSpec,),
+                        )
         elif "return" in func.__annotations__:
             raise NotImplementedError("Branch not implemented")
         self.output_spec = output_spec


Test patch:
diff --git a/pydra/engine/tests/test_task.py b/pydra/engine/tests/test_task.py
index 2d6a15e7..e624a39c 100644
--- a/pydra/engine/tests/test_task.py
+++ b/pydra/engine/tests/test_task.py
@@ -27,6 +27,17 @@ def test_output():
     assert res.output.out == 5
 
 
+def test_numpy():
+    """ checking if mark.task works for numpy functions"""
+    np = pytest.importorskip("numpy")
+    fft = mark.annotate({"a": np.ndarray, "return": float})(np.fft.fft)
+    fft = mark.task(fft)()
+    arr = np.array([[1, 10], [2, 20]])
+    fft.inputs.a = arr
+    res = fft()
+    assert np.allclose(np.fft.fft(arr), res.output.out)
+
+
 @pytest.mark.xfail(reason="cp.dumps(func) depends on the system/setup, TODO!!")
 def test_checksum():
     nn = funaddtwo(a=3)
@@ -38,7 +49,9 @@ def test_checksum():
 
 def test_annotated_func():
     @mark.task
-    def testfunc(a: int, b: float = 0.1) -> ty.NamedTuple("Output", [("out1", float)]):
+    def testfunc(
+        a: int, b: float = 0.1
+    ) -> ty.NamedTuple("Output", [("out_out", float)]):
         return a + b
 
     funky = testfunc(a=1)
@@ -48,14 +61,14 @@ def test_annotated_func():
     assert getattr(funky.inputs, "a") == 1
     assert getattr(funky.inputs, "b") == 0.1
     assert getattr(funky.inputs, "_func") is not None
-    assert set(funky.output_names) == set(["out1"])
+    assert set(funky.output_names) == set(["out_out"])
     # assert funky.inputs.hash == '17772c3aec9540a8dd3e187eecd2301a09c9a25c6e371ddd86e31e3a1ecfeefa'
     assert funky.__class__.__name__ + "_" + funky.inputs.hash == funky.checksum
 
     result = funky()
     assert hasattr(result, "output")
-    assert hasattr(result.output, "out1")
-    assert result.output.out1 == 1.1
+    assert hasattr(result.output, "out_out")
+    assert result.output.out_out == 1.1
 
     assert os.path.exists(funky.cache_dir / funky.checksum / "_result.pklz")
     funky.result()  # should not recompute
@@ -64,7 +77,7 @@ def test_annotated_func():
     assert funky.result() is None
     funky()
     result = funky.result()
-    assert result.output.out1 == 2.1
+    assert result.output.out_out == 2.1
 
     help = funky.help(returnhelp=True)
     assert help == [
@@ -74,7 +87,7 @@ def test_annotated_func():
         "- b: float (default: 0.1)",
         "- _func: str",
         "Output Parameters:",
-        "- out1: float",
+        "- out_out: float",
     ]
 
 
@@ -150,13 +163,13 @@ def test_halfannotated_func():
     assert getattr(funky.inputs, "a") == 10
     assert getattr(funky.inputs, "b") == 20
     assert getattr(funky.inputs, "_func") is not None
-    assert set(funky.output_names) == set(["out1"])
+    assert set(funky.output_names) == set(["out"])
     assert funky.__class__.__name__ + "_" + funky.inputs.hash == funky.checksum
 
     result = funky()
     assert hasattr(result, "output")
-    assert hasattr(result.output, "out1")
-    assert result.output.out1 == 30
+    assert hasattr(result.output, "out")
+    assert result.output.out == 30
 
     assert os.path.exists(funky.cache_dir / funky.checksum / "_result.pklz")
 
@@ -165,7 +178,7 @@ def test_halfannotated_func():
     assert funky.result() is None
     funky()
     result = funky.result()
-    assert result.output.out1 == 31
+    assert result.output.out == 31
     help = funky.help(returnhelp=True)
 
     assert help == [
@@ -175,7 +188,7 @@ def test_halfannotated_func():
         "- b: _empty",
         "- _func: str",
         "Output Parameters:",
-        "- out1: int",
+        "- out: int",
     ]
 
 
diff --git a/pydra/mark/tests/test_functions.py b/pydra/mark/tests/test_functions.py
index ae963ebe..31c764ea 100644
--- a/pydra/mark/tests/test_functions.py
+++ b/pydra/mark/tests/test_functions.py
@@ -87,6 +87,16 @@ def test_return_annotated_task():
     assert res.output.squared == 4.0
 
 
+def test_return_halfannotated_annotated_task():
+    @task
+    @annotate({"in_val": float, "return": float})
+    def square(in_val):
+        return in_val ** 2
+
+    res = square(in_val=2.0)()
+    assert res.output.out == 4.0
+
+
 def test_return_annotated_task_multiple_output():
     @task
     @annotate({"in_val": float, "return": {"squared": float, "cubed": float}})