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 |
|
152 | def post_execute(result): | |
120 |
""" |
|
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 |
|
166 | def post_run_cell(result): | |
133 |
""" |
|
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(' |
|
2628 | self.events.trigger('post_execute', result) | |
2629 | if not silent: |
|
2629 | if not silent: | |
2630 |
self.events.trigger(' |
|
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 = |
|
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 |
|
289 | assert post_always.called | |
298 |
assert |
|
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 |
|
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* |
|
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 |
|
13 | 5. Fire the ``post_execute`` event. | |
14 |
6. Fire the ``post_run_cell`` event unless |
|
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