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