Skip to content

Commit

Permalink
Add support for "finally" event callbacks.
Browse files Browse the repository at this point in the history
  • Loading branch information
fniephaus committed Sep 13, 2017
1 parent 502cfdf commit 48a1a5a
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 7 deletions.
23 changes: 23 additions & 0 deletions IPython/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,29 @@ def post_run_cell():
"""Fires after user-entered code runs."""
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`.
Expand Down
33 changes: 33 additions & 0 deletions IPython/core/interactiveshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -2598,6 +2598,39 @@ def safe_run_module(self, mod_name, where):
warn('Unknown failure executing module: <%s>' % mod_name)

def run_cell(self, raw_cell, store_history=False, silent=False, shell_futures=True):
"""Run a complete IPython cell and trigger finally hooks.
Parameters
----------
raw_cell : str
The code (including IPython code such as %magic functions) to run.
store_history : bool
If True, the raw and translated cell will be stored in IPython's
history. For user code calling back into IPython's machinery, this
should be set to False.
silent : bool
If True, avoid side-effects, such as implicit displayhooks and
and logging. silent=True forces store_history=False.
shell_futures : bool
If True, the code will share future statements with the interactive
shell. It will both be affected by previous __future__ imports, and
any __future__ imports in the code will affect the shell. If False,
__future__ imports are not shared in either direction.
Returns
-------
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=False, silent=False, shell_futures=True):
"""Run a complete IPython cell.
Parameters
Expand Down
20 changes: 20 additions & 0 deletions IPython/core/tests/test_interactiveshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,29 +263,49 @@ def test_silent_postexec(self):
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)
assert pre_always.called
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"""
Expand Down
33 changes: 28 additions & 5 deletions docs/source/config/callbacks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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::
Expand Down
7 changes: 5 additions & 2 deletions docs/source/development/execution.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down
6 changes: 6 additions & 0 deletions docs/source/whatsnew/pr/finally-event-callbacks.rst
Original file line number Diff line number Diff line change
@@ -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 guranteed 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 <https://github.com/ipython/ipython/issues/10774>`__
* `Updated docs <http://ipython.readthedocs.io/en/stable/config/callbacks.html?highlight=finally>`__

0 comments on commit 48a1a5a

Please sign in to comment.