Repository: pytorch/ignite

Issue:
Bug adding handler in case of decoration + class function + filtered event
## 🐛 Bug description

I would like to report a bug using handler defined by decorated function in a class with filtered event.

The following code reproduces all possible situations to add an handler defined w/wo decoration in a class or not, w/wo engine (or args), using an event w/wo filter

```python
engine = Engine(lambda e, b: b)

# decorator
def decorated(fun):
    @functools.wraps(fun)
    def wrapper(*args, **kwargs):
        return fun(*args, **kwargs)
    return wrapper

# handler as a function
def foo():
    print("foo")

# handler as a decorated function
@decorated
def decorated_foo():
    print("decorated_foo")

# register handler as a function -- OK
engine.add_event_handler(Events.EPOCH_STARTED, foo)
# register handler as a function with filter -- OK
engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo)
# register handler as a decorated function -- OK
engine.add_event_handler(Events.EPOCH_STARTED, decorated_foo)
# register handler as a decorated function with filter -- OK
engine.add_event_handler(Events.EPOCH_STARTED(every=2), decorated_foo)


# handler as a function with engine (here args)
def foo_args(args):
    print("foo_args", args)


# handler as a decorated function with engine 
@decorated
def decorated_foo_args(args):
    print("decorated_foo_args", args)

# register handler as a function with engine  -- OK
engine.add_event_handler(Events.EPOCH_STARTED, foo_args)
# register handler as a function with engine and filter -- OK
engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo_args)
# register handler as a decorated function with engine -- OK
engine.add_event_handler(Events.EPOCH_STARTED, decorated_foo_args)
# register handler as a decorated function with engine and filter -- OK
engine.add_event_handler(Events.EPOCH_STARTED(every=2), decorated_foo_args)

class Foo:
    # handler as a class function (ie method)
    def foo(self):
        print("foo")

    # handler as a decorated method
    @decorated
    def decorated_foo(self):
        print("decorated_foo")

    # handler as a method with engine
    def foo_args(self, args):
        print("foo_args", args)

    # handler as a decorated method with engine
    @decorated
    def decorated_foo_args(self, args):
        print("decorated_foo_args", args)


foo = Foo()

# register handler as a method -- OK
engine.add_event_handler(Events.EPOCH_STARTED, foo.foo)
# register handler as a method with filter -- OK
engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo.foo)
# register handler as a decorated method -- OK
engine.add_event_handler(Events.EPOCH_STARTED, foo.decorated_foo)
# register handler as a decorated method with filter -- OK
engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo.decorated_foo)
# register handler as a method with engine  -- OK
engine.add_event_handler(Events.EPOCH_STARTED, foo.foo_args)
# register handler as a method with engine and filter -- OK
engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo.foo_args)
# register handler as a decorated method with engine -- OK
engine.add_event_handler(Events.EPOCH_STARTED, foo.decorated_foo_args)

# register handler as a decorated method with engine and filter -- FAILED
engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo.decorated_foo_args)

engine.run([0])
```

The error is
```
Error adding <function Foo.decorated_foo_args at 0x1229b6af0> 'handler': takes parameters ['self', 'args'] but will be called with [](missing a required argument: 'self').
```

Why ? 

First, a handler defined with a filtered event is wrapped with decoration. See https://github.com/sdesrozis/ignite/blob/93be57aa3f71ce601391d59096c3b430c4d9487b/ignite/engine/engine.py#L198. Note that `functools.wraps` is used to fit the signature of the related handler.

The failed case is decorated method with engine. So, I guess `functools.wraps` works perfectly and catch `self` and `engine` as arguments. But the signature checking search (using `inspect.signature`) fails because missing `self`... 

See signature checking
https://github.com/pytorch/ignite/blob/0de7156bb284bd01d788252469a3b386f10abbd7/ignite/engine/utils.py#L5

I think this is related to `follow_wrapped=True` argument of `inspect.signature`.

## Environment

 - PyTorch Version (e.g., 1.4): 1.5
 - Ignite Version (e.g., 0.3.0): 0.4
 - OS (e.g., Linux): MacOS
 - How you installed Ignite (`conda`, `pip`, source): Honda
 - Python version: 3.7
 - Any other relevant information:



Discussion:
vfdev-5: @sdesrozis can we add some tests for that ?

Patch:
diff --git a/ignite/engine/utils.py b/ignite/engine/utils.py
index 41c2924f..a09c0f3f 100644
--- a/ignite/engine/utils.py
+++ b/ignite/engine/utils.py
@@ -3,7 +3,11 @@ from typing import Callable
 
 
 def _check_signature(fn: Callable, fn_description: str, *args, **kwargs) -> None:
-    signature = inspect.signature(fn)
+    # if handler with filter, check the handler rather than the decorator
+    if hasattr(fn, "_parent"):
+        signature = inspect.signature(fn._parent())
+    else:
+        signature = inspect.signature(fn)
     try:  # try without engine
         signature.bind(*args, **kwargs)
     except TypeError as exc:


Test patch:
diff --git a/tests/ignite/engine/test_event_handlers.py b/tests/ignite/engine/test_event_handlers.py
index fa1a1ec6..5b8abbb3 100644
--- a/tests/ignite/engine/test_event_handlers.py
+++ b/tests/ignite/engine/test_event_handlers.py
@@ -1,3 +1,4 @@
+import functools
 import gc
 from unittest.mock import MagicMock, call, create_autospec
 
@@ -471,3 +472,78 @@ def test_custom_exception_handler():
 
     # only one call from _run_once_over_data, since the exception is swallowed
     assert len(counter.exceptions) == 1 and counter.exceptions[0] == value_error
+
+
+def test_event_handlers_with_decoration():
+
+    engine = Engine(lambda e, b: b)
+
+    def decorated(fun):
+        @functools.wraps(fun)
+        def wrapper(*args, **kwargs):
+            return fun(*args, **kwargs)
+
+        return wrapper
+
+    values = []
+
+    def foo():
+        values.append("foo")
+
+    @decorated
+    def decorated_foo():
+        values.append("decorated_foo")
+
+    engine.add_event_handler(Events.EPOCH_STARTED, foo)
+    engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo)
+    engine.add_event_handler(Events.EPOCH_STARTED, decorated_foo)
+    engine.add_event_handler(Events.EPOCH_STARTED(every=2), decorated_foo)
+
+    def foo_args(e):
+        values.append("foo_args")
+        values.append(e.state.iteration)
+
+    @decorated
+    def decorated_foo_args(e):
+        values.append("decorated_foo_args")
+        values.append(e.state.iteration)
+
+    engine.add_event_handler(Events.EPOCH_STARTED, foo_args)
+    engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo_args)
+    engine.add_event_handler(Events.EPOCH_STARTED, decorated_foo_args)
+    engine.add_event_handler(Events.EPOCH_STARTED(every=2), decorated_foo_args)
+
+    class Foo:
+        def __init__(self):
+            self.values = []
+
+        def foo(self):
+            self.values.append("foo")
+
+        @decorated
+        def decorated_foo(self):
+            self.values.append("decorated_foo")
+
+        def foo_args(self, e):
+            self.values.append("foo_args")
+            self.values.append(e.state.iteration)
+
+        @decorated
+        def decorated_foo_args(self, e):
+            self.values.append("decorated_foo_args")
+            self.values.append(e.state.iteration)
+
+    foo = Foo()
+
+    engine.add_event_handler(Events.EPOCH_STARTED, foo.foo)
+    engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo.foo)
+    engine.add_event_handler(Events.EPOCH_STARTED, foo.decorated_foo)
+    engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo.decorated_foo)
+    engine.add_event_handler(Events.EPOCH_STARTED, foo.foo_args)
+    engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo.foo_args)
+    engine.add_event_handler(Events.EPOCH_STARTED, foo.decorated_foo_args)
+    engine.add_event_handler(Events.EPOCH_STARTED(every=2), foo.decorated_foo_args)
+
+    engine.run([0], max_epochs=2)
+
+    assert values == foo.values