Repository: kedro-org/kedro

Issue:
[KED-988] Using functools.update_wrapper for partial functions raises TypeError
## Description

Nodes composed of functions created by `functools.partial` raise a warning, suggesting the use of `functools.update_wrapper`, but using `update_wrapper` on the partial function raises a `TypeError`.

I think this could be fixed by adding `follow_wrapped=False` to `inspect.signature`, which is called in `Node`'s `_validate_inputs` method. I'm happy to open a PR if this seems a reasonable fix.

## Context
I'm running pipelines with node functions that return partial functions. I want to avoid the warning about `update_wrapper`.

## Steps to Reproduce
Run the following minimal code to get the error:
```
from functools import partial, update_wrapper
from kedro.pipeline import, node
import pandas as pd

def partial_foo(bar, df):
    return df.assign(baz=bar)
    
def foo(bar):
    return update_wrapper(partial(partial_foo, bar), partial_foo)

df = pd.DataFrame([{'foo': 'foo', 'bar': 'bar'}, {'foo': 'baz', 'bar': 'brr'}])
my_node = node(foo('hello'), 'data_in', 'data_out')
```

## Expected Result
I should be able to create a `node` with a partial function that is passed to `update_wrapper` without an error.

## Actual Result

When I call `update_wrapper` on the node's partial function, I get the following error:
```
/usr/local/lib/python3.6/site-packages/kedro/pipeline/node.py in _validate_inputs(self, func, inputs)
    515                         str(list(func_args)), str(inputs)
    516                     )
--> 517                 ) from exc
    518 
    519     def _validate_unique_outputs(self):

TypeError: Inputs of function expected ['bar', 'df'], but got data_in
```

This is due to `inspect.signature` getting the signature of the wrapped function, which does not match the wrapper function that is actually called within `Node`.

## Your Environment
* Kedro: 0.15.0
* Python: 3.6.8
* Running in the `python:3.6` Docker image


Discussion:
Thank you for your feedback @cfranklin11! We will investigate the possible solution for the issue and will get back to you. Your suggestion with setting `follow_wrapped=False` may well work, so in the meantime feel free to submit a PR with the proposed change if you would like to.

I've updated the title with our internal ticket number to make it easier to track.

Resolving patch:
diff --git a/kedro/pipeline/node.py b/kedro/pipeline/node.py
--- a/kedro/pipeline/node.py
+++ b/kedro/pipeline/node.py
@@ -510,9 +510,11 @@ def _validate_inputs(self, func, inputs):
         if not inspect.isbuiltin(func):
             args, kwargs = self._process_inputs_for_bind(inputs)
             try:
-                inspect.signature(func).bind(*args, **kwargs)
+                inspect.signature(func, follow_wrapped=False).bind(*args, **kwargs)
             except Exception as exc:
-                func_args = inspect.signature(func).parameters.keys()
+                func_args = inspect.signature(
+                    func, follow_wrapped=False
+                ).parameters.keys()
                 raise TypeError(
                     "Inputs of function expected {}, but got {}".format(
                         str(list(func_args)), str(inputs)


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
@@ -26,7 +26,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import sys
-from functools import partial, wraps
+from functools import partial, update_wrapper, wraps
 from typing import Callable
 
 import pytest
@@ -401,3 +401,9 @@ def test_partial(self):
         assert str(n) == "<partial>([in]) -> [out]"
         assert n.name == "<partial>([in]) -> [out]"
         assert n.short_name == "<Partial>"
+
+    def test_updated_partial(self):
+        n = node(update_wrapper(partial(identity), identity), ["in"], ["out"])
+        assert str(n) == "identity([in]) -> [out]"
+        assert n.name == "identity([in]) -> [out]"
+        assert n.short_name == "Identity"