Repository: nipype/pydra

Issue:
Task crashes when function returns None
in both cases the named outputs should be set to `None`.

```
import pydra
import typing as ty
import os


@pydra.mark.task
@pydra.mark.annotate({"return": {"b": ty.Any}})
def test_multiout(val, val2):
    return None


if __name__ == "__main__":
    cache_dir = os.path.join(os.getcwd(), 'cache3')
    task = test_multiout(name="mo2",
                         val=[0, 1, 2],
                         val2=[4, 5, 6],
                         cache_dir=cache_dir)
    with pydra.Submitter(plugin="cf", n_procs=2) as sub:
        sub(runnable=task)
    results = task.result()
    print(results)
```

this should also be checked for multiple outputs.

```
@pydra.mark.task
@pydra.mark.annotate({"return": {"a": ty.Any, "b": ty.Any}})
def test_multiout(val, val2):
    return None

```

Comments:

Patch:
diff --git a/.azure-pipelines/windows.yml b/.azure-pipelines/windows.yml
index f2907a5..6365a53 100644
--- a/.azure-pipelines/windows.yml
+++ b/.azure-pipelines/windows.yml
@@ -35,4 +35,9 @@ jobs:
     - script: |
         pytest -vs -n auto --cov pydra --cov-config .coveragerc --cov-report xml:cov.xml --doctest-modules pydra
       displayName: 'Pytest tests'
-
+    - script: |
+        python -m pip install codecov
+        codecov --file cov.xml
+      displayName: 'Upload To Codecov'
+      env:
+        CODECOV_TOKEN: $(CODECOV_TOKEN)
diff --git a/README.md b/README.md
index 44f2bda..17b5c8c 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ Feature list:
 and Dask (this is an experimental implementation with limited testing)
 
 [[API Documentation](https://nipype.github.io/pydra/)]
+[[PyCon 2020 Poster](https://docs.google.com/presentation/d/10tS2I34rS0G9qz6v29qVd77OUydjP_FdBklrgAGmYSw/edit?usp=sharing)]
 
 ### Tutorial
 This tutorial will walk you through the main concepts of Pydra!
diff --git a/pydra/engine/task.py b/pydra/engine/task.py
index 8cb088a..d8aaa60 100644
--- a/pydra/engine/task.py
+++ b/pydra/engine/task.py
@@ -199,19 +199,21 @@ class FunctionTask(TaskBase):
         del inputs["_func"]
         self.output_ = None
         output = cp.loads(self.inputs._func)(**inputs)
-        if output is not None:
-            output_names = [el[0] for el in self.output_spec.fields]
-            self.output_ = {}
-            if len(output_names) > 1:
-                if len(output_names) == len(output):
+        output_names = [el[0] for el in self.output_spec.fields]
+        if output is None:
+            self.output_ = dict((nm, None) for nm in output_names)
+        else:
+            if len(output_names) == 1:
+                # if only one element in the fields, everything should be returned together
+                self.output_ = {output_names[0]: output}
+            else:
+                if isinstance(output, tuple) and len(output_names) == len(output):
                     self.output_ = dict(zip(output_names, output))
                 else:
                     raise Exception(
                         f"expected {len(self.output_spec.fields)} elements, "
-                        f"but {len(output)} were returned"
+                        f"but {output} were returned"
                     )
-            else:  # if only one element in the fields, everything should be returned together
-                self.output_[output_names[0]] = output
 
 
 class ShellCommandTask(TaskBase):


Test patch:
diff --git a/pydra/engine/tests/test_task.py b/pydra/engine/tests/test_task.py
index f7f4182..eaa21f9 100644
--- a/pydra/engine/tests/test_task.py
+++ b/pydra/engine/tests/test_task.py
@@ -309,6 +309,31 @@ def test_exception_func():
     assert pytest.raises(Exception, bad_funk)
 
 
+def test_result_none_1():
+    """ checking if None is properly returned as the result"""
+
+    @mark.task
+    def fun_none(x):
+        return None
+
+    task = fun_none(name="none", x=3)
+    res = task()
+    assert res.output.out is None
+
+
+def test_result_none_2():
+    """ checking if None is properly set for all outputs """
+
+    @mark.task
+    def fun_none(x) -> (ty.Any, ty.Any):
+        return None
+
+    task = fun_none(name="none", x=3)
+    res = task()
+    assert res.output.out1 is None
+    assert res.output.out2 is None
+
+
 def test_audit_prov(tmpdir):
     @mark.task
     def testfunc(a: int, b: float = 0.1) -> ty.NamedTuple("Output", [("out", float)]):