Repository: kedro-org/kedro

Issue:
better error messages when wrong number of inputs is passed
## Description

When passing the wrong number of inputs into a kedro node it can be very confusing to where this happened, a more customized message that tells the user more information about the node would be quite helpful.  Some things to consider including would be; the name of the node, inputs, outputs, tags, and function name. 

## Context

Today I ran into an issue where I had scaffolded a new pipeline with many nodes before running.  And saw this error.  It took me a bit longer than it should have to figure out where I had gone wrong.

```
TypeError: Inputs of function expected [], but got a_raw_<dataset>
```

## Context
Why is this change important to you? How would you use it? How can it benefit other users?


## Possible Implementation

The error occurred in [kedro/pipeline/node.py#L553](https://github.com/quantumblacklabs/kedro/blob/master/kedro/pipeline/node.py#L553), It would be quite helpful to throw a custom error and include as much information about the node as is reasonable in this error message.

## Alternately

A custom error message is not required, but more information would be greatly appreciated.




Discussion:

Resolving patch:
diff --git a/kedro/pipeline/node.py b/kedro/pipeline/node.py
--- a/kedro/pipeline/node.py
+++ b/kedro/pipeline/node.py
@@ -197,20 +197,14 @@ def __call__(self, **kwargs) -> Dict[str, Any]:
         return self.run(inputs=kwargs)
 
     @property
-    def _func_name(self):
-        if hasattr(self._func, "__name__"):
-            return self._func.__name__
-
-        name = repr(self._func)
-        if "functools.partial" in name:
+    def _func_name(self) -> str:
+        name = _get_readable_func_name(self._func)
+        if name == "<partial>":
             warn(
-                "The node producing outputs `{}` is made from a `partial` function. "
-                "Partial functions do not have a `__name__` attribute: consider using "
-                "`functools.update_wrapper` for better log messages.".format(
-                    self.outputs
-                )
+                f"The node producing outputs `{self.outputs}` is made from a `partial` function. "
+                f"Partial functions do not have a `__name__` attribute: consider using "
+                f"`functools.update_wrapper` for better log messages."
             )
-            name = "<partial>"
         return name
 
     @property
@@ -550,10 +544,11 @@ def _validate_inputs(self, func, inputs):
                 func_args = inspect.signature(
                     func, follow_wrapped=False
                 ).parameters.keys()
+                func_name = _get_readable_func_name(func)
+
                 raise TypeError(
-                    "Inputs of function expected {}, but got {}".format(
-                        str(list(func_args)), str(inputs)
-                    )
+                    f"Inputs of '{func_name}' function expected {list(func_args)}, "
+                    f"but got {inputs}"
                 ) from exc
 
     def _validate_unique_outputs(self):
@@ -696,3 +691,20 @@ def _to_list(element: Union[None, str, Iterable[str], Dict[str, str]]) -> List:
     if isinstance(element, dict):
         return sorted(element.values())
     return list(element)
+
+
+def _get_readable_func_name(func: Callable) -> str:
+    """Get a user-friendly readable name of the function provided.
+
+    Returns:
+        str: readable name of the provided callable func.
+    """
+
+    if hasattr(func, "__name__"):
+        return func.__name__
+
+    name = repr(func)
+    if "functools.partial" in name:
+        name = "<partial>"
+
+    return name


Test patch:
diff --git a/tests/pipeline/test_node.py b/tests/pipeline/test_node.py
--- a/tests/pipeline/test_node.py
+++ b/tests/pipeline/test_node.py
@@ -332,20 +332,42 @@ def dummy_func_args(**kwargs):
     return dummy_func_args, "A", "B"
 
 
+lambda_identity = lambda input1: input1  # noqa: disable=E731
+
+
+def lambda_inconsistent_input_size():
+    return lambda_identity, ["A", "B"], "C"
+
+
+partial_identity = partial(identity)
+
+
+def partial_inconsistent_input_size():
+    return partial_identity, ["A", "B"], "C"
+
+
 @pytest.mark.parametrize(
     "func, expected",
     [
         (
             inconsistent_input_size,
-            r"Inputs of function expected \[\'input1\'\], but got \[\'A\', \'B\'\]",
+            r"Inputs of 'identity' function expected \[\'input1\'\], but got \[\'A\', \'B\'\]",
         ),
         (
             inconsistent_input_args,
-            r"Inputs of function expected \[\'args\'\], but got {\'a\': \'A\'}",
+            r"Inputs of 'dummy_func_args' function expected \[\'args\'\], but got {\'a\': \'A\'}",
         ),
         (
             inconsistent_input_kwargs,
-            r"Inputs of function expected \[\'kwargs\'\], but got A",
+            r"Inputs of 'dummy_func_args' function expected \[\'kwargs\'\], but got A",
+        ),
+        (
+            lambda_inconsistent_input_size,
+            r"Inputs of '<lambda>' function expected \[\'input1\'\], but got \[\'A\', \'B\'\]",
+        ),
+        (
+            partial_inconsistent_input_size,
+            r"Inputs of '<partial>' function expected \[\'input1\'\], but got \[\'A\', \'B\'\]",
         ),
     ],
 )