Repository: pytorch/ignite

Issue:
Failed to remove handler attached on filtered event
## 🐛 Bug description

```python
from ignite.engine import Engine, Events

e = Engine(lambda e, b: None)
assert len(e._event_handlers[Events.ITERATION_COMPLETED]) == 0

handle = e.add_event_handler(Events.ITERATION_COMPLETED(every=3), lambda _: print("Hello"))
assert len(e._event_handlers[Events.ITERATION_COMPLETED]) == 1

handle.remove()
assert len(e._event_handlers[Events.ITERATION_COMPLETED]) == 0, e._event_handlers[Events.ITERATION_COMPLETED]
```
Output:
```
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Input In [8], in <cell line: 11>()
      8 assert len(e._event_handlers[Events.ITERATION_COMPLETED]) == 1
     10 handle.remove()
---> 11 assert len(e._event_handlers[Events.ITERATION_COMPLETED]) == 0, e._event_handlers[Events.ITERATION_COMPLETED]

AssertionError: [(<function <lambda> at 0x132ab23a0>, (<ignite.engine.engine.Engine object at 0x13330ed00>,), {})]
> 
```

## Environment

- PyTorch Version (e.g., 1.4):
- Ignite Version (e.g., 0.3.0): 0.4.9
- OS (e.g., Linux):
- How you installed Ignite (`conda`, `pip`, source):
- Python version:
- Any other relevant information:



Discussion:
The problem rises when we construct the handle with the wrapping handler but in `_compare_handlers` we compare it with the parent of registered handlers.

https://github.com/pytorch/ignite/blob/c12ad57e26aa8ec2b191ff4bc0e01e97ef24f8f7/ignite/engine/engine.py#L307-L309

https://github.com/pytorch/ignite/blob/c12ad57e26aa8ec2b191ff4bc0e01e97ef24f8f7/ignite/engine/engine.py#L327

https://github.com/pytorch/ignite/blob/c12ad57e26aa8ec2b191ff4bc0e01e97ef24f8f7/ignite/engine/engine.py#L351-L353

Resolving patch:
diff --git a/ignite/engine/engine.py b/ignite/engine/engine.py
--- a/ignite/engine/engine.py
+++ b/ignite/engine/engine.py
@@ -373,7 +373,7 @@ def remove_event_handler(self, handler: Callable, event_name: Any) -> None:
         self._event_handlers[event_name] = new_event_handlers
 
     def on(self, event_name: Any, *args: Any, **kwargs: Any) -> Callable:
-        """Decorator shortcut for add_event_handler.
+        """Decorator shortcut for :meth:`~ignite.engine.engine.Engine.add_event_handler`.
 
         Args:
             event_name: An event to attach the handler to. Valid events are from :class:`~ignite.engine.events.Events`
diff --git a/ignite/engine/events.py b/ignite/engine/events.py
--- a/ignite/engine/events.py
+++ b/ignite/engine/events.py
@@ -474,6 +474,14 @@ def remove(self) -> None:
         if handler is None or engine is None:
             return
 
+        if hasattr(handler, "_parent"):
+            handler = handler._parent()  # type: ignore[attr-defined]
+            if handler is None:
+                raise RuntimeError(
+                    "Internal error! Please fill an issue on https://github.com/pytorch/ignite/issues "
+                    "if encounter this error. Thank you!"
+                )
+
         if isinstance(self.event_name, EventsList):
             for e in self.event_name:
                 if engine.has_event_handler(handler, e):


Test patch:
diff --git a/tests/ignite/engine/test_custom_events.py b/tests/ignite/engine/test_custom_events.py
--- a/tests/ignite/engine/test_custom_events.py
+++ b/tests/ignite/engine/test_custom_events.py
@@ -218,8 +218,7 @@ def bar(e):
     engine.add_event_handler(Events.EPOCH_COMPLETED(every=3), bar)
     assert engine.has_event_handler(bar)
     assert engine.has_event_handler(bar, Events.EPOCH_COMPLETED)
-
-    engine.has_event_handler(bar, Events.EPOCH_COMPLETED(every=3))
+    assert engine.has_event_handler(bar, Events.EPOCH_COMPLETED(every=3))
 
 
 def test_remove_event_handler_on_callable_events():
@@ -258,15 +257,9 @@ def _test(event_name, event_attr, every, true_num_calls):
 
         engine = Engine(lambda e, b: b)
 
-        counter = [
-            0,
-        ]
-        counter_every = [
-            0,
-        ]
-        num_calls = [
-            0,
-        ]
+        counter = [0]
+        counter_every = [0]
+        num_calls = [0]
 
         @engine.on(event_name(every=every))
         def assert_every(engine):
@@ -309,12 +302,8 @@ def _test(event_name, event_attr):
         engine = Engine(lambda e, b: b)
 
         once = 2
-        counter = [
-            0,
-        ]
-        num_calls = [
-            0,
-        ]
+        counter = [0]
+        num_calls = [0]
 
         @engine.on(event_name(once=once))
         def assert_once(engine):
@@ -350,9 +339,7 @@ def _test(event_name, event_attr, true_num_calls):
 
         engine = Engine(lambda e, b: b)
 
-        num_calls = [
-            0,
-        ]
+        num_calls = [0]
 
         @engine.on(event_name(event_filter=custom_event_filter))
         def assert_on_special_event(engine):
@@ -381,9 +368,7 @@ def custom_event_filter(engine, event):
 
     # Check bad behaviour
     engine = Engine(lambda e, b: b)
-    counter = [
-        0,
-    ]
+    counter = [0]
 
     # Modify events
     Events.ITERATION_STARTED(event_filter=custom_event_filter)
@@ -433,9 +418,7 @@ def update_fn(engine, batch):
         engine = Engine(update_fn)
         engine.register_events(*CustomEvents, event_to_attr=event_to_attr)
 
-        num_calls = [
-            0,
-        ]
+        num_calls = [0]
 
         @engine.on(event_name(event_filter=custom_event_filter))
         def assert_on_special_event(engine):
diff --git a/tests/ignite/engine/test_event_handlers.py b/tests/ignite/engine/test_event_handlers.py
--- a/tests/ignite/engine/test_event_handlers.py
+++ b/tests/ignite/engine/test_event_handlers.py
@@ -129,73 +129,90 @@ def test_adding_multiple_event_handlers():
         handler.assert_called_once_with(engine)
 
 
-def test_event_removable_handle():
+@pytest.mark.parametrize(
+    "event1, event2",
+    [
+        (Events.STARTED, Events.COMPLETED),
+        (Events.EPOCH_STARTED, Events.EPOCH_COMPLETED),
+        (Events.ITERATION_STARTED, Events.ITERATION_COMPLETED),
+        (Events.ITERATION_STARTED(every=2), Events.ITERATION_COMPLETED(every=2)),
+    ],
+)
+def test_event_removable_handle(event1, event2):
 
     # Removable handle removes event from engine.
-    engine = DummyEngine()
+    engine = Engine(lambda e, b: None)
     handler = create_autospec(spec=lambda x: None)
     assert not hasattr(handler, "_parent")
 
-    removable_handle = engine.add_event_handler(Events.STARTED, handler)
-    assert engine.has_event_handler(handler, Events.STARTED)
+    removable_handle = engine.add_event_handler(event1, handler)
+    assert engine.has_event_handler(handler, event1)
 
-    engine.run(1)
-    handler.assert_called_once_with(engine)
+    engine.run([1, 2])
+    handler.assert_any_call(engine)
+    num_calls = handler.call_count
 
     removable_handle.remove()
-    assert not engine.has_event_handler(handler, Events.STARTED)
+    assert not engine.has_event_handler(handler, event1)
 
     # Second engine pass does not fire handle again.
-    engine.run(1)
-    handler.assert_called_once_with(engine)
+    engine.run([1, 2])
+    # Assert that handler wasn't call
+    assert handler.call_count == num_calls
 
     # Removable handle can be used as a context manager
     handler = create_autospec(spec=lambda x: None)
 
-    with engine.add_event_handler(Events.STARTED, handler):
-        assert engine.has_event_handler(handler, Events.STARTED)
-        engine.run(1)
+    with engine.add_event_handler(event1, handler):
+        assert engine.has_event_handler(handler, event1)
+        engine.run([1, 2])
 
-    assert not engine.has_event_handler(handler, Events.STARTED)
-    handler.assert_called_once_with(engine)
+    assert not engine.has_event_handler(handler, event1)
+    handler.assert_any_call(engine)
+    num_calls = handler.call_count
 
-    engine.run(1)
-    handler.assert_called_once_with(engine)
+    engine.run([1, 2])
+    # Assert that handler wasn't call
+    assert handler.call_count == num_calls
 
     # Removeable handle only effects a single event registration
     handler = MagicMock(spec_set=True)
 
-    with engine.add_event_handler(Events.STARTED, handler):
-        with engine.add_event_handler(Events.COMPLETED, handler):
-            assert engine.has_event_handler(handler, Events.STARTED)
-            assert engine.has_event_handler(handler, Events.COMPLETED)
-        assert engine.has_event_handler(handler, Events.STARTED)
-        assert not engine.has_event_handler(handler, Events.COMPLETED)
-    assert not engine.has_event_handler(handler, Events.STARTED)
-    assert not engine.has_event_handler(handler, Events.COMPLETED)
+    with engine.add_event_handler(event1, handler):
+        with engine.add_event_handler(event2, handler):
+            assert engine.has_event_handler(handler, event1)
+            assert engine.has_event_handler(handler, event2)
+        assert engine.has_event_handler(handler, event1)
+        assert not engine.has_event_handler(handler, event2)
+    assert not engine.has_event_handler(handler, event1)
+    assert not engine.has_event_handler(handler, event2)
 
     # Removeable handle is re-enter and re-exitable
 
     handler = MagicMock(spec_set=True)
 
-    remove = engine.add_event_handler(Events.STARTED, handler)
+    remove = engine.add_event_handler(event1, handler)
 
     with remove:
         with remove:
-            assert engine.has_event_handler(handler, Events.STARTED)
-        assert not engine.has_event_handler(handler, Events.STARTED)
-    assert not engine.has_event_handler(handler, Events.STARTED)
+            assert engine.has_event_handler(handler, event1)
+        assert not engine.has_event_handler(handler, event1)
+    assert not engine.has_event_handler(handler, event1)
 
     # Removeable handle is a weakref, does not keep engine or event alive
     def _add_in_closure():
-        _engine = DummyEngine()
+        _engine = Engine(lambda e, b: None)
 
         def _handler(_):
             pass
 
-        _handle = _engine.add_event_handler(Events.STARTED, _handler)
+        _handle = _engine.add_event_handler(event1, _handler)
         assert _handle.engine() is _engine
-        assert _handle.handler() is _handler
+
+        if event1.filter is None:
+            assert _handle.handler() is _handler
+        else:
+            assert _handle.handler()._parent() is _handler
 
         return _handle