diff --git a/IPython/core/events.py b/IPython/core/events.py index a8591d9..586ab93 100644 --- a/IPython/core/events.py +++ b/IPython/core/events.py @@ -116,6 +116,29 @@ def post_run_cell(): pass @_define_event +def finally_execute(result): + """Always fires after code is executed in response to user/frontend action. + + This includes comm and widget messages and silent execution, as well as user + code cells. + + Parameters + ---------- + result : :class:`~IPython.core.interactiveshell.ExecutionResult` + """ + pass + +@_define_event +def finally_run_cell(result): + """Always fires after user-entered code runs. + + Parameters + ---------- + result : :class:`~IPython.core.interactiveshell.ExecutionResult` + """ + pass + +@_define_event def shell_initialized(ip): """Fires after initialisation of :class:`~IPython.core.interactiveshell.InteractiveShell`. diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 1d537e8..5c8f801 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1879,7 +1879,7 @@ class InteractiveShell(SingletonConfigurable): # This is overridden in TerminalInteractiveShell to show a message about # the %paste magic. def showindentationerror(self): - """Called by run_cell when there's an IndentationError in code entered + """Called by _run_cell when there's an IndentationError in code entered at the prompt. This is overridden in TerminalInteractiveShell to show a message about @@ -2621,6 +2621,29 @@ class InteractiveShell(SingletonConfigurable): ------- result : :class:`ExecutionResult` """ + try: + result = self._run_cell( + raw_cell, store_history, silent, shell_futures) + finally: + self.events.trigger('finally_execute', result) + if not silent: + self.events.trigger('finally_run_cell', result) + return result + + def _run_cell(self, raw_cell, store_history, silent, shell_futures): + """Internal method to run a complete IPython cell. + + Parameters + ---------- + raw_cell : str + store_history : bool + silent : bool + shell_futures : bool + + Returns + ------- + result : :class:`ExecutionResult` + """ result = ExecutionResult() if (not raw_cell) or raw_cell.isspace(): diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 6871ed2..1c78da4 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -263,11 +263,17 @@ class InteractiveShellTestCase(unittest.TestCase): pre_always = mock.Mock() post_explicit = mock.Mock() post_always = mock.Mock() + finally_explicit = mock.Mock() + finally_always = mock.Mock() + all_mocks = [pre_explicit, pre_always, post_explicit, post_always, + finally_explicit,finally_always] ip.events.register('pre_run_cell', pre_explicit) ip.events.register('pre_execute', pre_always) ip.events.register('post_run_cell', post_explicit) ip.events.register('post_execute', post_always) + ip.events.register('finally_run_cell', finally_explicit) + ip.events.register('finally_execute', finally_always) try: ip.run_cell("1", silent=True) @@ -275,17 +281,31 @@ class InteractiveShellTestCase(unittest.TestCase): assert not pre_explicit.called assert post_always.called assert not post_explicit.called + assert finally_always.called + assert not finally_explicit.called # double-check that non-silent exec did what we expected # silent to avoid ip.run_cell("1") assert pre_explicit.called assert post_explicit.called + assert finally_explicit.called + # check that finally hooks are always called + [m.reset_mock() for m in all_mocks] + ip.run_cell("syntax error") + assert pre_always.called + assert pre_explicit.called + assert not post_always.called # because of `SyntaxError` + assert not post_explicit.called + assert finally_explicit.called + assert finally_always.called finally: # remove post-exec ip.events.unregister('pre_run_cell', pre_explicit) ip.events.unregister('pre_execute', pre_always) ip.events.unregister('post_run_cell', post_explicit) ip.events.unregister('post_execute', post_always) + ip.events.unregister('finally_run_cell', finally_explicit) + ip.events.unregister('finally_execute', finally_always) def test_silent_noadvance(self): """run_cell(silent=True) doesn't advance execution_count""" diff --git a/docs/source/config/callbacks.rst b/docs/source/config/callbacks.rst index ad99a6f..7b3b1ed 100644 --- a/docs/source/config/callbacks.rst +++ b/docs/source/config/callbacks.rst @@ -24,11 +24,18 @@ For example:: def post_execute(self): if self.shell.user_ns.get('x', None) != self.last_x: print("x changed!") + + def finally_execute(self, result): + if result.error_before_exec: + print('Error before execution: %s' % result.error_before_exec) + else: + print('Execution result: %s', result.result) def load_ipython_extension(ip): vw = VarWatcher(ip) ip.events.register('pre_execute', vw.pre_execute) ip.events.register('post_execute', vw.post_execute) + ip.events.register('finally_execute', vw.finally_execute) Events @@ -64,16 +71,32 @@ skipping the history/display mechanisms, in which cases ``pre_run_cell`` will no post_run_cell ------------- -``post_run_cell`` runs after interactive execution (e.g. a cell in a notebook). -It can be used to cleanup or notify or perform operations on any side effects produced during execution. -For instance, the inline matplotlib backend uses this event to display any figures created but not explicitly displayed during the course of the cell. - +``post_run_cell`` runs after successful interactive execution (e.g. a cell in a +notebook, but, for example, not when a ``SyntaxError`` was raised). +It can be used to cleanup or notify or perform operations on any side effects +produced during execution. +For instance, the inline matplotlib backend uses this event to display any +figures created but not explicitly displayed during the course of the cell. post_execute ------------ The same as ``pre_execute``, ``post_execute`` is like ``post_run_cell``, -but fires for *all* executions, not just interactive ones. +but fires for *all* successful executions, not just interactive ones. + +finally_run_cell +------------- + +``finally_run_cell`` is like ``post_run_cell``, but fires after *all* executions +(even when, for example, a ``SyntaxError`` was raised). +Additionally, the execution result is provided as an argument. + +finally_execute +------------ + +``finally_execute`` is like ``post_execute``, but fires after *all* executions +(even when, for example, a ``SyntaxError`` was raised). +Additionally, the execution result is provided as an argument. .. seealso:: diff --git a/docs/source/development/execution.rst b/docs/source/development/execution.rst index 20b37e1..d67982b 100644 --- a/docs/source/development/execution.rst +++ b/docs/source/development/execution.rst @@ -6,11 +6,14 @@ Execution semantics in the IPython kernel The execution of user code consists of the following phases: 1. Fire the ``pre_execute`` event. -2. Fire the ``pre_run_cell`` event unless silent is True. +2. Fire the ``pre_run_cell`` event unless silent is ``True``. 3. Execute the ``code`` field, see below for details. 4. If execution succeeds, expressions in ``user_expressions`` are computed. This ensures that any error in the expressions don't affect the main code execution. -5. Fire the post_execute event. +5. Fire the ``post_execute`` event unless the execution failed. +6. Fire the ``post_run_cell`` event unless the execution failed or silent is ``True``. +7. Fire the ``finally_execute`` event. +8. Fire the ``finally_run_cell`` event unless silent is ``True``. .. seealso:: diff --git a/docs/source/whatsnew/pr/finally-event-callbacks.rst b/docs/source/whatsnew/pr/finally-event-callbacks.rst new file mode 100644 index 0000000..180a2f4 --- /dev/null +++ b/docs/source/whatsnew/pr/finally-event-callbacks.rst @@ -0,0 +1,6 @@ +Two new event callbacks have been added: ``finally_execute`` and ``finally_run_cell``. +They work similar to the corresponding *post* callbacks, but are guaranteed to be triggered (even when, for example, a ``SyntaxError`` was raised). +Also, the execution result is provided as an argument for further inspection. + +* `GitHub issue `__ +* `Updated docs `__