Repository: PrefectHQ/prefect

Issue:
Raise an error if `flow.run()` called without required parameters
Related to #743, we should raise an error if users don't provide required parameters when calling flow.run(). We already raise an error for unexpected parameters. 


Discussion:
:+1: just be careful not to get rid of the error inside of the `Parameter.run` method itself (https://github.com/PrefectHQ/prefect/issues/21).
Good call. I was thinking of only implementing this in `flow.run()` itself, since it's a convenience function, but not in the main FlowRunner pipeline.

Resolving patch:
diff --git a/src/prefect/client/client.py b/src/prefect/client/client.py
--- a/src/prefect/client/client.py
+++ b/src/prefect/client/client.py
@@ -318,7 +318,7 @@ def deploy(
         Raises:
             - ClientError: if the deploy failed
         """
-        required_parameters = flow.parameters(only_required=True)
+        required_parameters = {p for p in flow.parameters() if p.required}
         if flow.schedule is not None and required_parameters:
             raise ClientError(
                 "Flows with required parameters can not be scheduled automatically."
diff --git a/src/prefect/core/flow.py b/src/prefect/core/flow.py
--- a/src/prefect/core/flow.py
+++ b/src/prefect/core/flow.py
@@ -370,28 +370,14 @@ def terminal_tasks(self) -> Set[Task]:
         """
         return set(t for t in self.tasks if not self.edges_from(t))
 
-    @cache
-    def parameters(
-        self, names_only: bool = False, only_required: bool = False
-    ) -> Set[Union[str, Parameter]]:
+    def parameters(self) -> Set[Parameter]:
         """
-        Get details about any Parameters in this flow.
-
-        Args:
-            - names_only (bool, optional): Whether or not to only return
-            parameter names
-            - only_required (bool, optional): Whether or not to only get
-            required parameters; defaults to `False`
+        Returns any parameters of the flow.
 
         Returns:
-            - dict: of `{task.name: task}` for all tasks in the flow which are
-            Parameters
+            - set: a set of any Parameters in this flow
         """
-        return {
-            t.name if names_only else t
-            for t in self.tasks
-            if isinstance(t, Parameter) and (t.required if only_required else True)
-        }
+        return {p for p in self.tasks if isinstance(p, Parameter)}
 
     def reference_tasks(self) -> Set[Task]:
         """
@@ -971,27 +957,36 @@ def run(
         if runner_cls is None:
             runner_cls = prefect.engine.get_default_flow_runner_class()
 
+        # build parameters from passed dictionary and also kwargs
         parameters = parameters or {}
+        for p in self.parameters():
+            if p.name in kwargs:
+                parameters[p.name] = kwargs.pop(p.name)
+
+        # check for parameters that don't match the flow
         unknown_params = [
-            p for p in parameters if p not in self.parameters(names_only=True)
+            p for p in parameters if p not in {fp.name for fp in self.parameters()}
         ]
         if unknown_params:
             fmt_params = ", ".join(unknown_params)
-            raise TypeError(
+            raise ValueError(
                 "Flow.run received the following unexpected parameters: {}".format(
                     fmt_params
                 )
             )
 
-        passed_parameters = {}
-        for p in self.parameters(names_only=True):
-            if p in kwargs:
-                passed_parameters[p] = kwargs.pop(p)
-            elif p in parameters:
-                passed_parameters[p] = parameters[p]
+        # check for parameters that are required by the flow, but weren't passed
+        missing_params = [p.name for p in self.parameters() if p.name not in parameters]
+        if missing_params:
+            fmt_params = ", ".join(missing_params)
+            raise ValueError(
+                "Flow.run did not receive the following required parameters: {}".format(
+                    fmt_params
+                )
+            )
 
         state = self._run_on_schedule(
-            parameters=passed_parameters, runner_cls=runner_cls, **kwargs
+            parameters=parameters, runner_cls=runner_cls, **kwargs
         )
 
         # state always should return a dict of tasks. If it's NoResult (meaning the run was


Test patch:
diff --git a/tests/core/test_flow.py b/tests/core/test_flow.py
--- a/tests/core/test_flow.py
+++ b/tests/core/test_flow.py
@@ -749,13 +749,23 @@ def run(self):
         f.add_task(x)
         f.add_task(t)
 
-    with pytest.raises(TypeError):
+    # errors because of the invalid parameter
+    with pytest.raises(ValueError):
         state = f.run(parameters=dict(x=10, y=3, z=9))
 
+    # errors because the parameter is passed to FlowRunner.run() as an invalid kwarg
     with pytest.raises(TypeError):
         state = f.run(x=10, y=3, z=9)
 
 
+def test_flow_raises_for_missing_required_parameters():
+    with Flow(name="test") as f:
+        f.add_task(Parameter("x"))
+
+    with pytest.raises(ValueError):
+        f.run()
+
+
 def test_validate_cycles():
     f = Flow(name="test")
     t1 = Task()
@@ -1074,26 +1084,6 @@ def test_cache_terminal_tasks(self):
         f.add_edge(t2, t3)
         assert f.terminal_tasks() == set([t3])
 
-    def test_cache_parameters(self):
-        f = Flow(name="test")
-        t1 = Parameter("t1")
-        t2 = Task()
-        t3 = Task()
-        f.add_edge(t1, t2)
-
-        f.parameters()
-
-        # check that cache holds result
-        key = ("parameters", ())
-        assert f._cache[key] == {t1}
-
-        # check that cache is read
-        f._cache[key] = 1
-        assert f.parameters() == 1
-
-        f.add_edge(t2, t3)
-        assert f.parameters() == {t1}
-
     def test_cache_all_upstream_edges(self):
         f = Flow(name="test")
         t1 = Task()
diff --git a/tests/flows/test_parameter.py b/tests/flows/test_parameter.py
--- a/tests/flows/test_parameter.py
+++ b/tests/flows/test_parameter.py
@@ -50,7 +50,7 @@ def test_create_required_parameter():
 def test_raise_error_if_two_parameters_have_same_name():
     f = Flow(name="test")
     f.add_task(Parameter("x"))
-    assert "x" in f.parameters(names_only=True)
+    assert "x" in {p.name for p in f.parameters()}
     with pytest.raises(ValueError):
         f.add_task(Parameter("x"))
 
@@ -63,6 +63,3 @@ def test_flow_parameters():
     f.add_task(y)
 
     assert f.parameters() == {x, y}
-    assert f.parameters(only_required=True) == {x}
-    assert f.parameters(names_only=True) == {"x", "y"}
-    assert f.parameters(names_only=True, only_required=True) == {"x"}
diff --git a/tests/serialization/test_flows.py b/tests/serialization/test_flows.py
--- a/tests/serialization/test_flows.py
+++ b/tests/serialization/test_flows.py
@@ -111,7 +111,7 @@ def test_deserialize_with_parameters_key():
     f.add_task(x)
 
     f2 = FlowSchema().load(FlowSchema().dump(f))
-    assert f2.parameters(names_only=True) == f.parameters(names_only=True)
+    assert {p.name for p in f2.parameters()} == {p.name for p in f.parameters()}
     f_params = {(p.name, p.required, p.default) for p in f.parameters()}
     f2_params = {(p.name, p.required, p.default) for p in f2.parameters()}
     assert f_params == f2_params