Repository: pytorch/ignite

Issue:
Engine optional in signature of event function
## 🚀 Feature

The idea is to make optional engine argument in handler:
```python
trainer = ... 

@trainer.on(Events.COMPLETED)
def _(): # no need to pass engine
  print("Training is finished")
  print(f"number of iterations {trainer.state.iterations}") # use trainer directly
``` 

PS. Sylvain, I edited a bit the text

Discussion:

Patch:
diff --git a/README.md b/README.md
index 19b51976..33483320 100644
--- a/README.md
+++ b/README.md
@@ -109,7 +109,10 @@ multiple losses and metrics in your training loop.
 
 ## Power of Events & Handlers
 
-The cool thing with handlers is that they offer unparalleled flexibility (compared to say, callbacks). Handlers can be any function (e.g. lambda, simple function, class method etc.) with the correct signature, we only require that the first argument be `engine`. Thus, we do not require to inherit from an interface and override its abstract methods which could unnecessarily bulk up your code and its complexity.
+The cool thing with handlers is that they offer unparalleled flexibility (compared to say, callbacks). Handlers can be 
+any function: e.g. lambda, simple function, class method etc. The first argument can be optionally `engine`, but not necessary. 
+Thus, we do not require to inherit from an interface and override its abstract methods which could unnecessarily bulk 
+up your code and its complexity.
 
 ### Execute any number of functions whenever you wish
 
@@ -119,13 +122,13 @@ Examples
 </summary>
 
 ```python
-trainer.add_event_handler(Events.STARTED, lambda engine: print("Start training"))
+trainer.add_event_handler(Events.STARTED, lambda _: print("Start training"))
 
 # attach handler with args, kwargs
 mydata = [1, 2, 3, 4]
 logger = ...
 
-def on_training_ended(engine, data):
+def on_training_ended(data):
     print("Training is ended. mydata={}".format(data))
     # User can use variables from another scope  
     logger.info("Training is ended")
@@ -152,17 +155,17 @@ Examples
 ```python
 # run the validation every 5 epochs
 @trainer.on(Events.EPOCH_COMPLETED(every=5))
-def run_validation(_):
+def run_validation():
     # run validation
 
 # change some training variable once on 20th epoch
 @trainer.on(Events.EPOCH_STARTED(once=20))
-def change_training_variable(_):
+def change_training_variable():
     # ...
 
 # Trigger handler with customly defined frequency
 @trainer.on(Events.ITERATION_COMPLETED(event_filter=first_x_iters))
-def log_gradients(_):
+def log_gradients():
     # ...
 ```
 
@@ -178,7 +181,7 @@ Examples
 Events can be stacked together to enable multiple calls:
 ```python
 @trainer.on(Events.COMPLETED | Events.EPOCH_COMPLETED(every=10))
-def do_some_validation(engine):
+def run_validation():
     # ...
 ```
 
@@ -258,14 +261,13 @@ F1_mean.attach(engine, "F1")
     variety of handlers to ease the pain of training and validation of
     neural networks!
 -   **ignite.contrib**: The Contrib directory contains additional
-    modules contributed by Ignite users. Modules vary from TBPTT engine,
+    modules that can require extra dependencies. Modules vary from TBPTT engine,
     various optimisation parameter schedulers, logging handlers and a
     metrics module containing many regression metrics
     ([ignite.contrib.metrics.regression](https://github.com/pytorch/ignite/tree/master/ignite/contrib/metrics/regression))!
 
 The code in **ignite.contrib** is not as fully maintained as the core
-part of the library. It may change or be removed at any time without
-notice.
+part of the library.
 
 # Examples
 
diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst
index 43fa1cf8..c935c7c5 100644
--- a/docs/source/concepts.rst
+++ b/docs/source/concepts.rst
@@ -64,8 +64,10 @@ the run:
 
 Complete list of events can be found at :class:`~ignite.engine.Events`.
 
-Thus, user can execute a custom code as an event handler. Let us consider in more detail what happens when
-:meth:`~ignite.engine.Engine.run` is called:
+Thus, user can execute a custom code as an event handler. Handlers can be any function: e.g. lambda, simple function,
+class method etc. The first argument can be optionally `engine`, but not necessary.
+
+Let us consider in more detail what happens when :meth:`~ignite.engine.Engine.run` is called:
 
 .. code-block:: python
 
@@ -98,11 +100,15 @@ Attaching an event handler is simple using method :meth:`~ignite.engine.Engine.a
     @trainer.on(Events.STARTED)
     def on_training_started(engine):
         print("Another message of start training")
+    # or even simpler, use only what you need !
+    @trainer.on(Events.STARTED)
+    def on_training_started():
+        print("Another message of start training")
 
     # attach handler with args, kwargs
     mydata = [1, 2, 3, 4]
 
-    def on_training_ended(engine, data):
+    def on_training_ended(data):
         print("Training is ended. mydata={}".format(data))
 
     trainer.add_event_handler(Events.COMPLETED, on_training_ended, mydata)
@@ -147,12 +153,12 @@ event filtering function:
     trainer = create_supervised_trainer(model, optimizer, loss)
 
     @trainer.on(Events.ITERATION_COMPLETED(every=50))
-    def log_training_loss_every_50_iterations(engine):
+    def log_training_loss_every_50_iterations():
         print("{} / {} : {} - loss: {:.2f}"
-              .format(engine.state.epoch, engine.state.max_epochs, engine.state.iteration, engine.state.output))
+              .format(trainer.state.epoch, trainer.state.max_epochs, trainer.state.iteration, trainer.state.output))
 
     @trainer.on(Events.EPOCH_STARTED(once=25))
-    def do_something_once_on_25_epoch(engine):
+    def do_something_once_on_25_epoch():
         # do something
 
     def custom_event_filter(engine, event):
diff --git a/ignite/contrib/handlers/time_profilers.py b/ignite/contrib/handlers/time_profilers.py
index 9951ce43..e5cf0212 100644
--- a/ignite/contrib/handlers/time_profilers.py
+++ b/ignite/contrib/handlers/time_profilers.py
@@ -96,13 +96,13 @@ class BasicTimeProfiler:
         }
 
         # Setup all other handlers:
-        engine._event_handlers[Events.STARTED].append((self._as_last_started, (), {}))
+        engine._event_handlers[Events.STARTED].append((self._as_last_started, (engine,), {}))
 
         for e, m in zip(self._events, self._fmethods):
-            engine._event_handlers[e].insert(0, (m, (), {}))
+            engine._event_handlers[e].insert(0, (m, (engine,), {}))
 
         for e, m in zip(self._events, self._lmethods):
-            engine._event_handlers[e].append((m, (), {}))
+            engine._event_handlers[e].append((m, (engine,), {}))
 
         # Let's go
         self._event_handlers_timer.reset()
@@ -190,7 +190,7 @@ class BasicTimeProfiler:
             raise TypeError("Argument engine should be ignite.engine.Engine, " "but given {}".format(type(engine)))
 
         if not engine.has_event_handler(self._as_first_started):
-            engine._event_handlers[Events.STARTED].insert(0, (self._as_first_started, (), {}))
+            engine._event_handlers[Events.STARTED].insert(0, (self._as_first_started, (engine,), {}))
 
     @staticmethod
     def _compute_basic_stats(data):
diff --git a/ignite/engine/engine.py b/ignite/engine/engine.py
index bb5a9bf2..b8b93a5d 100644
--- a/ignite/engine/engine.py
+++ b/ignite/engine/engine.py
@@ -137,7 +137,7 @@ class Engine:
         if self._process_function is None:
             raise ValueError("Engine must be given a processing function in order to run.")
 
-        _check_signature(self, process_function, "process_function", None)
+        _check_signature(process_function, "process_function", self, None)
 
     def register_events(self, *event_names: Any, event_to_attr: Optional[dict] = None) -> None:
         """Add events that can be fired.
@@ -214,14 +214,13 @@ class Engine:
             event_name: An event or a list of events to attach the handler. Valid events are
                 from :class:`~ignite.engine.Events` or any `event_name` added by
                 :meth:`~ignite.engine.Engine.register_events`.
-            handler (callable): the callable event handler that should be invoked
+            handler (callable): the callable event handler that should be invoked. No restrictions on its signature.
+                The first argument can be optionally `engine`, the :class:`~ignite.engine.Engine` object, handler is
+                bound to.
             *args: optional args to be passed to `handler`.
             **kwargs: optional keyword args to be passed to `handler`.
 
         Note:
-            The handler function's first argument will be `self`, the :class:`~ignite.engine.Engine` object it
-            was bound to.
-
             Note that other arguments can be passed to the handler in addition to the `*args` and  `**kwargs`
             passed here, for example during :attr:`~ignite.engine.Events.EXCEPTION_RAISED`.
 
@@ -241,10 +240,11 @@ class Engine:
 
             events_list = Events.EPOCH_COMPLETED | Events.COMPLETED
 
-            def execute_validation(engine):
-                # do some validations
+            def execute_something():
+                # do some thing not related to engine
+                pass
 
-            engine.add_event_handler(events_list, execute_validation)
+            engine.add_event_handler(events_list, execute_something)
 
         Note:
             Since v0.3.0, Events become more flexible and allow to pass an event filter to the Engine.
@@ -267,9 +267,12 @@ class Engine:
             raise ValueError("Event {} is not a valid event for this Engine.".format(event_name))
 
         event_args = (Exception(),) if event_name == Events.EXCEPTION_RAISED else ()
-        _check_signature(self, handler, "handler", *(event_args + args), **kwargs)
-
-        self._event_handlers[event_name].append((handler, args, kwargs))
+        try:
+            _check_signature(handler, "handler", self, *(event_args + args), **kwargs)
+            self._event_handlers[event_name].append((handler, (self,) + args, kwargs))
+        except ValueError:
+            _check_signature(handler, "handler", *(event_args + args), **kwargs)
+            self._event_handlers[event_name].append((handler, args, kwargs))
         self.logger.debug("added handler for event %s.", event_name)
 
         return RemovableEventHandle(event_name, handler, self)
@@ -342,6 +345,20 @@ class Engine:
             *args: optional args to be passed to `handler`.
             **kwargs: optional keyword args to be passed to `handler`.
 
+        Example usage:
+
+        .. code-block:: python
+
+            engine = Engine(process_function)
+
+            @engine.on(Events.EPOCH_COMPLETED)
+            def print_epoch():
+                print("Epoch: {}".format(engine.state.epoch))
+
+            @engine.on(Events.EPOCH_COMPLETED | Events.COMPLETED)
+            def execute_something():
+                # do some thing not related to engine
+                pass
         """
 
         def decorator(f: Callable) -> Callable:
@@ -371,7 +388,8 @@ class Engine:
             self.last_event_name = event_name
             for func, args, kwargs in self._event_handlers[event_name]:
                 kwargs.update(event_kwargs)
-                func(self, *(event_args + args), **kwargs)
+                first, others = ((args[0],), args[1:]) if (args and args[0] == self) else ((), args)
+                func(*first, *(event_args + others), **kwargs)
 
     def fire_event(self, event_name: Any) -> None:
         """Execute all the handlers associated with given event.
diff --git a/ignite/engine/events.py b/ignite/engine/events.py
index e0f154bc..8de7b6b0 100644
--- a/ignite/engine/events.py
+++ b/ignite/engine/events.py
@@ -87,7 +87,7 @@ class CallableEventWithFilter:
 
         # check signature:
         if event_filter is not None:
-            _check_signature("engine", event_filter, "event_filter", "event")
+            _check_signature(event_filter, "event_filter", "engine", "event")
 
         return CallableEventWithFilter(self.value, event_filter, self.name)
 
diff --git a/ignite/engine/utils.py b/ignite/engine/utils.py
index d65c43e4..7aa13495 100644
--- a/ignite/engine/utils.py
+++ b/ignite/engine/utils.py
@@ -55,17 +55,16 @@ class ReproducibleBatchSampler(torch.utils.data.sampler.BatchSampler):
         return len(self.batch_sampler)
 
 
-def _check_signature(engine, fn: Callable, fn_description: str, *args, **kwargs) -> None:
-
+def _check_signature(fn: Callable, fn_description: str, *args, **kwargs) -> None:
     signature = inspect.signature(fn)
-    try:
-        signature.bind(engine, *args, **kwargs)
+    try:  # try without engine
+        signature.bind(*args, **kwargs)
     except TypeError as exc:
         fn_params = list(signature.parameters)
         exception_msg = str(exc)
-        passed_params = [engine] + list(args) + list(kwargs)
+        passed_params = list(args) + list(kwargs)
         raise ValueError(
             "Error adding {} '{}': "
-            "takes parameters {} but will be called with {} "
+            "takes parameters {} but will be called with {}"
             "({}).".format(fn, fn_description, fn_params, passed_params, exception_msg)
         )


Test patch:
diff --git a/tests/ignite/engine/test_event_handlers.py b/tests/ignite/engine/test_event_handlers.py
index af98d4cf..1dd1d67e 100644
--- a/tests/ignite/engine/test_event_handlers.py
+++ b/tests/ignite/engine/test_event_handlers.py
@@ -1,6 +1,6 @@
 import gc
 
-from unittest.mock import call, MagicMock
+from unittest.mock import call, MagicMock, create_autospec
 
 import pytest
 from pytest import raises
@@ -35,8 +35,7 @@ def test_add_event_handler_raises_with_invalid_signature():
         pass
 
     engine.add_event_handler(Events.STARTED, handler)
-    with pytest.raises(ValueError):
-        engine.add_event_handler(Events.STARTED, handler, 1)
+    engine.add_event_handler(Events.STARTED, handler, 1)
 
     def handler_with_args(engine, a):
         pass
@@ -51,15 +50,13 @@ def test_add_event_handler_raises_with_invalid_signature():
     engine.add_event_handler(Events.STARTED, handler_with_kwargs, b=2)
     with pytest.raises(ValueError):
         engine.add_event_handler(Events.STARTED, handler_with_kwargs, c=3)
-    with pytest.raises(ValueError):
-        engine.add_event_handler(Events.STARTED, handler_with_kwargs, 1, b=2)
+    engine.add_event_handler(Events.STARTED, handler_with_kwargs, 1, b=2)
 
     def handler_with_args_and_kwargs(engine, a, b=42):
         pass
 
     engine.add_event_handler(Events.STARTED, handler_with_args_and_kwargs, 1, b=2)
-    with pytest.raises(ValueError):
-        engine.add_event_handler(Events.STARTED, handler_with_args_and_kwargs, 1, 2, b=2)
+    engine.add_event_handler(Events.STARTED, handler_with_args_and_kwargs, 1, 2, b=2)
     with pytest.raises(ValueError):
         engine.add_event_handler(Events.STARTED, handler_with_args_and_kwargs, 1, b=2, c=3)
 
@@ -91,9 +88,39 @@ def test_add_event_handler():
     assert completed_counter.count == 15
 
 
+def test_add_event_handler_without_engine():
+    engine = DummyEngine()
+
+    class Counter(object):
+        def __init__(self, count=0):
+            self.count = count
+
+    started_counter = Counter()
+
+    def handle_iteration_started():
+        started_counter.count += 1
+
+    engine.add_event_handler(Events.STARTED, handle_iteration_started)
+
+    completed_counter = Counter()
+
+    def handle_iteration_completed(counter):
+        counter.count += 1
+
+    engine.add_event_handler(Events.COMPLETED, handle_iteration_completed, completed_counter)
+
+    engine.run(15)
+
+    assert started_counter.count == 15
+    assert completed_counter.count == 15
+
+
 def test_adding_multiple_event_handlers():
+    mock_fn_1 = create_autospec(spec=lambda x: None)
+    mock_fn_2 = create_autospec(spec=lambda x: None)
+
     engine = DummyEngine()
-    handlers = [MagicMock(), MagicMock()]
+    handlers = [mock_fn_1, mock_fn_2]
     for handler in handlers:
         engine.add_event_handler(Events.STARTED, handler)
 
@@ -106,7 +133,7 @@ def test_event_removable_handle():
 
     # Removable handle removes event from engine.
     engine = DummyEngine()
-    handler = MagicMock(spec_set=True)
+    handler = create_autospec(spec=lambda x: None)
     assert not hasattr(handler, "_parent")
 
     removable_handle = engine.add_event_handler(Events.STARTED, handler)
@@ -123,7 +150,7 @@ def test_event_removable_handle():
     handler.assert_called_once_with(engine)
 
     # Removable handle can be used as a context manager
-    handler = MagicMock(spec_set=True)
+    handler = create_autospec(spec=lambda x: None)
 
     with engine.add_event_handler(Events.STARTED, handler):
         assert engine.has_event_handler(handler, Events.STARTED)
@@ -186,7 +213,7 @@ def test_events_list_removable_handle():
 
     # Removable handle removes event from engine.
     engine = DummyEngine()
-    handler = MagicMock(spec_set=True)
+    handler = create_autospec(spec=lambda x: None)
     assert not hasattr(handler, "_parent")
 
     events_list = Events.STARTED | Events.COMPLETED
@@ -210,7 +237,7 @@ def test_events_list_removable_handle():
     assert handler.call_count == 2
 
     # Removable handle can be used as a context manager
-    handler = MagicMock(spec_set=True)
+    handler = create_autospec(spec=lambda x: None)
 
     with engine.add_event_handler(events_list, handler):
         for e in events_list:
@@ -227,7 +254,7 @@ def test_events_list_removable_handle():
     assert handler.call_count == 2
 
     # Removeable handle only effects a single event registration
-    handler = MagicMock(spec_set=True)
+    handler = create_autospec(spec=lambda x: None)
 
     other_events_list = Events.EPOCH_STARTED | Events.EPOCH_COMPLETED
 
@@ -248,7 +275,7 @@ def test_events_list_removable_handle():
 
     # Removeable handle is re-enter and re-exitable
 
-    handler = MagicMock(spec_set=True)
+    handler = create_autospec(spec=lambda x: None)
 
     remove = engine.add_event_handler(events_list, handler)
 
@@ -348,7 +375,7 @@ def test_args_and_kwargs_are_passed_to_event():
     args = (1, 2, 3)
     handlers = []
     for event in [Events.STARTED, Events.COMPLETED]:
-        handler = MagicMock(spec_set=True)
+        handler = create_autospec(spec=lambda e, x1, x2, x3, a, b: None)
         engine.add_event_handler(event, handler, *args, **kwargs)
         handlers.append(handler)