##// END OF EJS Templates
Ensure post event callbacks are always called....
Fabio Niephaus -
Show More
@@ -0,0 +1,6 b''
1 The *post* event callbacks are now always called, even when the execution failed
2 (for example because of a ``SyntaxError``).
3 Additionally, the execution result object is now made available in both *pre*
4 and *post* event callbacks in a backward compatible manner.
5
6 * `Related GitHub issue <https://github.com/ipython/ipython/issues/10774>`__
@@ -13,6 +13,27 b' events and the arguments which will be passed to them.'
13 13 This API is experimental in IPython 2.0, and may be revised in future versions.
14 14 """
15 15
16 from functools import wraps
17 try:
18 from inspect import getfullargspec
19 except:
20 from inspect import getargspec as getfullargspec # for Python2 compatibility.
21
22 # original function -> wrapper function mapping
23 compatibility_wrapper_functions = {}
24
25 def _compatibility_wrapper_for(function):
26 """Returns a wrapper for a function without args that accepts any args."""
27 if len(getfullargspec(function).args) > 0:
28 raise TypeError('%s cannot have arguments' % function)
29 if function in compatibility_wrapper_functions:
30 return compatibility_wrapper_functions[function]
31 @wraps(function)
32 def wrapper(*args, **kwargs):
33 function()
34 compatibility_wrapper_functions[function] = wrapper
35 return wrapper
36
16 37 class EventManager(object):
17 38 """Manage a collection of events and a sequence of callbacks for each.
18 39
@@ -56,11 +77,24 b' class EventManager(object):'
56 77 """
57 78 if not callable(function):
58 79 raise TypeError('Need a callable, got %r' % function)
59 self.callbacks[event].append(function)
80
81 callback_proto = available_events.get(event)
82 if (callable(callback_proto) and
83 len(getfullargspec(callback_proto).args) > 0 and
84 len(getfullargspec(function).args) == 0):
85 # `callback_proto` requires args but `function` does not, so a
86 # compatibility wrapper is needed.
87 self.callbacks[event].append(_compatibility_wrapper_for(function))
88 else:
89 self.callbacks[event].append(function)
60 90
61 91 def unregister(self, event, function):
62 92 """Remove a callback from the given event."""
63 self.callbacks[event].remove(function)
93 wrapper = compatibility_wrapper_functions.get(function)
94 if wrapper:
95 self.callbacks[event].remove(wrapper)
96 else:
97 self.callbacks[event].remove(function)
64 98
65 99 def trigger(self, event, *args, **kwargs):
66 100 """Call callbacks for ``event``.
@@ -90,34 +124,33 b' def _define_event(callback_proto):'
90 124 # ------------------------------------------------------------------------------
91 125
92 126 @_define_event
93 def pre_execute():
127 def pre_execute(result):
94 128 """Fires before code is executed in response to user/frontend action.
95 129
96 130 This includes comm and widget messages and silent execution, as well as user
97 code cells."""
98 pass
131 code cells.
99 132
100 @_define_event
101 def pre_run_cell():
102 """Fires before user-entered code runs."""
133 Parameters
134 ----------
135 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
136 The object which will be returned as the execution result.
137 """
103 138 pass
104 139
105 140 @_define_event
106 def post_execute():
107 """Fires after code is executed in response to user/frontend action.
108
109 This includes comm and widget messages and silent execution, as well as user
110 code cells."""
111 pass
141 def pre_run_cell(result):
142 """Fires before user-entered code runs.
112 143
113 @_define_event
114 def post_run_cell():
115 """Fires after user-entered code runs."""
144 Parameters
145 ----------
146 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
147 The object which will be returned as the execution result.
148 """
116 149 pass
117 150
118 151 @_define_event
119 def finally_execute(result):
120 """Always fires after code is executed in response to user/frontend action.
152 def post_execute(result):
153 """Fires after code is executed in response to user/frontend action.
121 154
122 155 This includes comm and widget messages and silent execution, as well as user
123 156 code cells.
@@ -125,16 +158,18 b' def finally_execute(result):'
125 158 Parameters
126 159 ----------
127 160 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
161 The object which will be returned as the execution result.
128 162 """
129 163 pass
130 164
131 165 @_define_event
132 def finally_run_cell(result):
133 """Always fires after user-entered code runs.
166 def post_run_cell(result):
167 """Fires after user-entered code runs.
134 168
135 169 Parameters
136 170 ----------
137 171 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
172 The object which will be returned as the execution result.
138 173 """
139 174 pass
140 175
@@ -2625,9 +2625,9 b' class InteractiveShell(SingletonConfigurable):'
2625 2625 result = self._run_cell(
2626 2626 raw_cell, store_history, silent, shell_futures)
2627 2627 finally:
2628 self.events.trigger('finally_execute', result)
2628 self.events.trigger('post_execute', result)
2629 2629 if not silent:
2630 self.events.trigger('finally_run_cell', result)
2630 self.events.trigger('post_run_cell', result)
2631 2631 return result
2632 2632
2633 2633 def _run_cell(self, raw_cell, store_history, silent, shell_futures):
@@ -2746,7 +2746,7 b' class InteractiveShell(SingletonConfigurable):'
2746 2746 self.displayhook.exec_result = result
2747 2747
2748 2748 # Execute the user code
2749 interactivity = "none" if silent else self.ast_node_interactivity
2749 interactivity = 'none' if silent else self.ast_node_interactivity
2750 2750 has_raised = self.run_ast_nodes(code_ast.body, cell_name,
2751 2751 interactivity=interactivity, compiler=compiler, result=result)
2752 2752
@@ -2757,10 +2757,6 b' class InteractiveShell(SingletonConfigurable):'
2757 2757 # ExecutionResult
2758 2758 self.displayhook.exec_result = None
2759 2759
2760 self.events.trigger('post_execute')
2761 if not silent:
2762 self.events.trigger('post_run_cell')
2763
2764 2760 if store_history:
2765 2761 # Write output to the database. Does nothing unless
2766 2762 # history output logging is enabled.
@@ -4,12 +4,17 b' from unittest.mock import Mock'
4 4 from IPython.core import events
5 5 import IPython.testing.tools as tt
6 6
7 @events._define_event
7 8 def ping_received():
8 9 pass
9 10
11 @events._define_event
12 def event_with_argument(argument):
13 pass
14
10 15 class CallbackTests(unittest.TestCase):
11 16 def setUp(self):
12 self.em = events.EventManager(get_ipython(), {'ping_received': ping_received})
17 self.em = events.EventManager(get_ipython(), {'ping_received': ping_received, 'event_with_argument': event_with_argument})
13 18
14 19 def test_register_unregister(self):
15 20 cb = Mock()
@@ -49,3 +54,16 b' class CallbackTests(unittest.TestCase):'
49 54 self.em.trigger('ping_received')
50 55 self.assertEqual([True, True, False], invoked)
51 56 self.assertEqual([func3], self.em.callbacks['ping_received'])
57
58 def test_ignore_event_arguments_if_no_argument_required(self):
59 call_count = [0]
60 def event_with_no_argument():
61 call_count[0] += 1
62
63 self.em.register('event_with_argument', event_with_no_argument)
64 self.em.trigger('event_with_argument', 'the argument')
65 self.assertEqual(call_count[0], 1)
66
67 self.em.unregister('event_with_argument', event_with_no_argument)
68 self.em.trigger('ping_received')
69 self.assertEqual(call_count[0], 1)
@@ -263,17 +263,12 b' class InteractiveShellTestCase(unittest.TestCase):'
263 263 pre_always = mock.Mock()
264 264 post_explicit = mock.Mock()
265 265 post_always = mock.Mock()
266 finally_explicit = mock.Mock()
267 finally_always = mock.Mock()
268 all_mocks = [pre_explicit, pre_always, post_explicit, post_always,
269 finally_explicit,finally_always]
266 all_mocks = [pre_explicit, pre_always, post_explicit, post_always]
270 267
271 268 ip.events.register('pre_run_cell', pre_explicit)
272 269 ip.events.register('pre_execute', pre_always)
273 270 ip.events.register('post_run_cell', post_explicit)
274 271 ip.events.register('post_execute', post_always)
275 ip.events.register('finally_run_cell', finally_explicit)
276 ip.events.register('finally_execute', finally_always)
277 272
278 273 try:
279 274 ip.run_cell("1", silent=True)
@@ -281,31 +276,24 b' class InteractiveShellTestCase(unittest.TestCase):'
281 276 assert not pre_explicit.called
282 277 assert post_always.called
283 278 assert not post_explicit.called
284 assert finally_always.called
285 assert not finally_explicit.called
286 279 # double-check that non-silent exec did what we expected
287 280 # silent to avoid
288 281 ip.run_cell("1")
289 282 assert pre_explicit.called
290 283 assert post_explicit.called
291 assert finally_explicit.called
292 # check that finally hooks are always called
284 # check that post hooks are always called
293 285 [m.reset_mock() for m in all_mocks]
294 286 ip.run_cell("syntax error")
295 287 assert pre_always.called
296 288 assert pre_explicit.called
297 assert not post_always.called # because of `SyntaxError`
298 assert not post_explicit.called
299 assert finally_explicit.called
300 assert finally_always.called
289 assert post_always.called
290 assert post_explicit.called
301 291 finally:
302 292 # remove post-exec
303 293 ip.events.unregister('pre_run_cell', pre_explicit)
304 294 ip.events.unregister('pre_execute', pre_always)
305 295 ip.events.unregister('post_run_cell', post_explicit)
306 296 ip.events.unregister('post_execute', post_always)
307 ip.events.unregister('finally_run_cell', finally_explicit)
308 ip.events.unregister('finally_execute', finally_always)
309 297
310 298 def test_silent_noadvance(self):
311 299 """run_cell(silent=True) doesn't advance execution_count"""
@@ -21,21 +21,16 b' For example::'
21 21 def pre_execute(self):
22 22 self.last_x = self.shell.user_ns.get('x', None)
23 23
24 def post_execute(self):
24 def post_execute(self, result):
25 if result.error_before_exec:
26 print('Error before execution: %s' % result.error_before_exec)
25 27 if self.shell.user_ns.get('x', None) != self.last_x:
26 28 print("x changed!")
27 29
28 def finally_execute(self, result):
29 if result.error_before_exec:
30 print('Error before execution: %s' % result.error_before_exec)
31 else:
32 print('Execution result: %s', result.result)
33
34 30 def load_ipython_extension(ip):
35 31 vw = VarWatcher(ip)
36 32 ip.events.register('pre_execute', vw.pre_execute)
37 33 ip.events.register('post_execute', vw.post_execute)
38 ip.events.register('finally_execute', vw.finally_execute)
39 34
40 35
41 36 Events
@@ -60,6 +55,8 b' pre_run_cell'
60 55
61 56 ``pre_run_cell`` fires prior to interactive execution (e.g. a cell in a notebook).
62 57 It can be used to note the state prior to execution, and keep track of changes.
58 The object which will be returned as the execution result is provided as an
59 argument, even though the actual result is not yet available.
63 60
64 61 pre_execute
65 62 -----------
@@ -67,22 +64,25 b' pre_execute'
67 64 ``pre_execute`` is like ``pre_run_cell``, but is triggered prior to *any* execution.
68 65 Sometimes code can be executed by libraries, etc. which
69 66 skipping the history/display mechanisms, in which cases ``pre_run_cell`` will not fire.
67 The object which will be returned as the execution result is provided as an
68 argument, even though the actual result is not yet available.
70 69
71 70 post_run_cell
72 71 -------------
73 72
74 ``post_run_cell`` runs after successful interactive execution (e.g. a cell in a
75 notebook, but, for example, not when a ``SyntaxError`` was raised).
73 ``post_run_cell`` runs after interactive execution.
76 74 It can be used to cleanup or notify or perform operations on any side effects
77 75 produced during execution.
78 76 For instance, the inline matplotlib backend uses this event to display any
79 77 figures created but not explicitly displayed during the course of the cell.
78 The object which will be returned as the execution result is provided as an
79 argument.
80 80
81 81 post_execute
82 82 ------------
83 83
84 84 The same as ``pre_execute``, ``post_execute`` is like ``post_run_cell``,
85 but fires for *all* successful executions, not just interactive ones.
85 but fires for *all* executions, not just interactive ones.
86 86
87 87 finally_run_cell
88 88 -------------
@@ -10,10 +10,8 b' The execution of user code consists of the following phases:'
10 10 3. Execute the ``code`` field, see below for details.
11 11 4. If execution succeeds, expressions in ``user_expressions`` are computed.
12 12 This ensures that any error in the expressions don't affect the main code execution.
13 5. Fire the ``post_execute`` event unless the execution failed.
14 6. Fire the ``post_run_cell`` event unless the execution failed or silent is ``True``.
15 7. Fire the ``finally_execute`` event.
16 8. Fire the ``finally_run_cell`` event unless silent is ``True``.
13 5. Fire the ``post_execute`` event.
14 6. Fire the ``post_run_cell`` event unless silent is ``True``.
17 15
18 16 .. seealso::
19 17
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now