Repository: pytorch/ignite

Issue:
Handler without engine raises error on filtered events
## 🐛 Bug description

The following code does not work:
```python
import torch
print(torch.__version__)
import ignite
print(ignite.__version__)

from ignite.engine import Engine, Events, State

def f4():
    print("f4")

    
e = Engine(lambda e, b: None)
e.state = State(iteration=0, epoch=0, max_epochs=10)

e.add_event_handler(Events.ITERATION_COMPLETED(every=2), f4)
e.fire_event(Events.ITERATION_COMPLETED)
e.fire_event(Events.ITERATION_COMPLETED)
```
Output
```

TypeErrorTraceback (most recent call last)
<ipython-input-13-832efed482dd> in <module>
     14 
     15 e.add_event_handler(Events.ITERATION_COMPLETED(every=2), f4)
---> 16 e.fire_event(Events.ITERATION_COMPLETED)
     17 e.fire_event(Events.ITERATION_COMPLETED)

/opt/conda/lib/python3.7/site-packages/ignite/engine/engine.py in fire_event(self, event_name)
    414 
    415         """
--> 416         return self._fire_event(event_name)
    417 
    418     def terminate(self) -> None:

/opt/conda/lib/python3.7/site-packages/ignite/engine/engine.py in _fire_event(self, event_name, *event_args, **event_kwargs)
    391                 kwargs.update(event_kwargs)
    392                 first, others = ((args[0],), args[1:]) if (args and args[0] == self) else ((), args)
--> 393                 func(*first, *(event_args + others), **kwargs)
    394 
    395     def fire_event(self, event_name: Any) -> None:

/opt/conda/lib/python3.7/site-packages/ignite/engine/engine.py in wrapper(engine, *args, **kwargs)
    203             event = engine.state.get_event_attrib_value(event_name)
    204             if event_filter(engine, event):
--> 205                 return handler(engine, *args, **kwargs)
    206 
    207         # setup input handler as parent to make has_event_handler work

TypeError: f4() takes 0 positional arguments but 1 was given
```


## Environment

 - PyTorch Version (e.g., 1.4): 1.6.0.dev20200411+cu101
 - Ignite Version (e.g., 0.3.0): 0.4.0.dev20200412


@sdesrozis could you please take a look. Thanks !

Discussion:
vfdev-5: @sdesrozis please, add tests of the bug case
sdesrozis: > @sdesrozis please, add tests of the bug case

Sorry, I didn't commit the test !
vfdev-5: @sdesrozis flake is failed: https://travis-ci.org/github/pytorch/ignite/jobs/674841603#L238

Patch:
diff --git a/ignite/engine/engine.py b/ignite/engine/engine.py
index 83106dc5..b8a457f9 100644
--- a/ignite/engine/engine.py
+++ b/ignite/engine/engine.py
@@ -5,6 +5,7 @@ from collections.abc import Mapping
 import weakref
 import random
 import warnings
+import functools
 from typing import Union, Optional, Callable, Iterable, Iterator, Any, Tuple, List
 
 import torch
@@ -197,12 +198,14 @@ class Engine:
             if event_to_attr and e in event_to_attr:
                 State.event_to_attr[e] = event_to_attr[e]
 
-    @staticmethod
-    def _handler_wrapper(handler: Callable, event_name: Any, event_filter: Callable) -> Callable:
-        def wrapper(engine: Engine, *args, **kwargs) -> Any:
-            event = engine.state.get_event_attrib_value(event_name)
-            if event_filter(engine, event):
-                return handler(engine, *args, **kwargs)
+    def _handler_wrapper(self, handler: Callable, event_name: Any, event_filter: Callable) -> Callable:
+        # signature of the following wrapper will be inspected during registering to check if engine is necessary
+        # we have to build a wrapper with relevant signature : solution is functools.wrapsgit s
+        @functools.wraps(handler)
+        def wrapper(*args, **kwargs) -> Any:
+            event = self.state.get_event_attrib_value(event_name)
+            if event_filter(self, event):
+                return handler(*args, **kwargs)
 
         # setup input handler as parent to make has_event_handler work
         wrapper._parent = weakref.ref(handler)
@@ -261,7 +264,7 @@ class Engine:
             and event_name.filter != CallableEventWithFilter.default_event_filter
         ):
             event_filter = event_name.filter
-            handler = Engine._handler_wrapper(handler, event_name, event_filter)
+            handler = self._handler_wrapper(handler, event_name, event_filter)
 
         if event_name not in self._allowed_events:
             self.logger.error("attempt to add event handler to an invalid event %s.", event_name)


Test patch:
diff --git a/tests/ignite/engine/test_custom_events.py b/tests/ignite/engine/test_custom_events.py
index 42098a2c..33ef860f 100644
--- a/tests/ignite/engine/test_custom_events.py
+++ b/tests/ignite/engine/test_custom_events.py
@@ -229,11 +229,20 @@ def _test_every_event_filter_with_engine(device="cpu"):
             assert counter_every[0] == getattr(engine.state, event_attr)
             num_calls[0] += 1
 
+        @engine.on(event_name(every=every))
+        def assert_every_no_engine():
+            assert getattr(engine.state, event_attr) % every == 0
+            assert counter_every[0] == getattr(engine.state, event_attr)
+
         @engine.on(event_name)
         def assert_(engine):
             counter[0] += 1
             assert getattr(engine.state, event_attr) == counter[0]
 
+        @engine.on(event_name)
+        def assert_no_engine():
+            assert getattr(engine.state, event_attr) == counter[0]
+
         engine.run(data, max_epochs=5)
 
         assert num_calls[0] == true_num_calls