Repository: fair-workflows/fairworkflows

Issue:
Restrict fair workflows to only consist of is_fairstep-decorated functions
Defining a fair workflow using `@is_fairworkflow` decorator should throw an error if there are functions in it that are not decorated with `@is_fairstep`

Discussion:

Patch:
diff --git a/fairworkflows/fairworkflow.py b/fairworkflows/fairworkflow.py
index 540498c..b0af8ab 100644
--- a/fairworkflows/fairworkflow.py
+++ b/fairworkflows/fairworkflow.py
@@ -536,9 +536,7 @@ def is_fairworkflow(label: str = None, is_pplan_plan: bool = True):
         num_params = len(inspect.signature(func).parameters)
         empty_args = ([inspect.Parameter.empty()] * num_params)
         promise = scheduled_workflow(*empty_args)
-        if not isinstance(func(*empty_args), PromisedObject):
-            raise TypeError("The workflow does not return a 'promise'. Did you use the "
-                            "is_fairstep decorator on all the steps?")
+        _validate_decorated_function(func, empty_args)
 
         # Description of workflow is the raw function code
         description = inspect.getsource(func)
@@ -547,3 +545,22 @@ def is_fairworkflow(label: str = None, is_pplan_plan: bool = True):
             is_pplan_plan=is_pplan_plan, derived_from=None)
         return promise
     return _modify_function
+
+
+def _validate_decorated_function(func, empty_args):
+    """
+    Validate that a function decorated with is_fairworkflow actually consists of steps that are
+    decorated with is_fairstep. Call the function using empty arguments to test. NB: This won't
+    catch all edgecases of users misusing the is_fairworkflow decorator, but at least will
+    provide more useful error messages in frequently occurring cases.
+    """
+    try:
+        result = func(*empty_args)
+    except TypeError as e:
+        raise TypeError("Marking the function as workflow with `is_fairworkflow` decorator "
+                        "failed. "
+                        "Did you use the is_fairstep decorator on all the steps? "
+                        f"Detailed error message: {e}")
+    if not isinstance(result, PromisedObject):
+        raise TypeError("The workflow does not return a 'promise'. Did you use the "
+                        "is_fairstep decorator on all the steps?")


Test patch:
diff --git a/tests/test_fairworkflow.py b/tests/test_fairworkflow.py
index 442bb27..da3a526 100644
--- a/tests/test_fairworkflow.py
+++ b/tests/test_fairworkflow.py
@@ -395,3 +395,24 @@ class TestFairWorkflow:
                 """
                 return return_value(in1)
         assert "The workflow does not return a 'promise'" in str(e.value)
+
+    def test_workflow_mixed_decorated_steps(self):
+        def add(a: float, b: float) -> float:
+            """Adding up numbers. NB: no is_fairstep decorator!"""
+            return a + b
+
+        @is_fairstep(label='Subtraction')
+        def sub(a: float, b: float) -> float:
+            """Subtracting numbers."""
+            return a - b
+
+        with pytest.raises(TypeError) as e:
+            @is_fairworkflow(label='My Workflow')
+            def my_workflow(in1, in2):
+                """
+                A simple addition, subtraction workflow
+                """
+                return add(in1, sub(in2, in2))
+        assert ("Marking the function as workflow with `is_fairworkflow` decorator failed. "
+                in str(e.value))
+        assert "unsupported operand type(s)" in str(e.value)