Show More
@@ -0,0 +1,7 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 info and result objects are now made available in | |||
|
4 | the corresponding *pre* or *post* ``*_run_cell`` event callbacks in a backward | |||
|
5 | compatible manner. | |||
|
6 | ||||
|
7 | * `Related GitHub issue <https://github.com/ipython/ipython/issues/10774>`__ |
@@ -13,6 +13,9 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 backcall import callback_prototype | |||
|
17 | ||||
|
18 | ||||
16 | class EventManager(object): |
|
19 | class EventManager(object): | |
17 | """Manage a collection of events and a sequence of callbacks for each. |
|
20 | """Manage a collection of events and a sequence of callbacks for each. | |
18 |
|
21 | |||
@@ -37,7 +40,7 b' class EventManager(object):' | |||||
37 | self.callbacks = {n:[] for n in available_events} |
|
40 | self.callbacks = {n:[] for n in available_events} | |
38 |
|
41 | |||
39 | def register(self, event, function): |
|
42 | def register(self, event, function): | |
40 | """Register a new event callback |
|
43 | """Register a new event callback. | |
41 |
|
44 | |||
42 | Parameters |
|
45 | Parameters | |
43 | ---------- |
|
46 | ---------- | |
@@ -56,12 +59,21 b' class EventManager(object):' | |||||
56 | """ |
|
59 | """ | |
57 | if not callable(function): |
|
60 | if not callable(function): | |
58 | raise TypeError('Need a callable, got %r' % function) |
|
61 | raise TypeError('Need a callable, got %r' % function) | |
59 | self.callbacks[event].append(function) |
|
62 | callback_proto = available_events.get(event) | |
|
63 | self.callbacks[event].append(callback_proto.adapt(function)) | |||
60 |
|
64 | |||
61 | def unregister(self, event, function): |
|
65 | def unregister(self, event, function): | |
62 | """Remove a callback from the given event.""" |
|
66 | """Remove a callback from the given event.""" | |
63 |
self.callbacks[event] |
|
67 | if function in self.callbacks[event]: | |
64 |
|
68 | return self.callbacks[event].remove(function) | ||
|
69 | ||||
|
70 | # Remove callback in case ``function`` was adapted by `backcall`. | |||
|
71 | for callback in self.callbacks[event]: | |||
|
72 | if callback.__wrapped__ is function: | |||
|
73 | return self.callbacks[event].remove(callback) | |||
|
74 | ||||
|
75 | raise ValueError('Function {!r} is not registered as a {} callback'.format(function, event)) | |||
|
76 | ||||
65 | def trigger(self, event, *args, **kwargs): |
|
77 | def trigger(self, event, *args, **kwargs): | |
66 | """Call callbacks for ``event``. |
|
78 | """Call callbacks for ``event``. | |
67 |
|
79 | |||
@@ -78,8 +90,9 b' class EventManager(object):' | |||||
78 | # event_name -> prototype mapping |
|
90 | # event_name -> prototype mapping | |
79 | available_events = {} |
|
91 | available_events = {} | |
80 |
|
92 | |||
81 |
def _define_event(callback_ |
|
93 | def _define_event(callback_function): | |
82 | available_events[callback_proto.__name__] = callback_proto |
|
94 | callback_proto = callback_prototype(callback_function) | |
|
95 | available_events[callback_function.__name__] = callback_proto | |||
83 | return callback_proto |
|
96 | return callback_proto | |
84 |
|
97 | |||
85 | # ------------------------------------------------------------------------------ |
|
98 | # ------------------------------------------------------------------------------ | |
@@ -94,12 +107,19 b' def pre_execute():' | |||||
94 | """Fires before code is executed in response to user/frontend action. |
|
107 | """Fires before code is executed in response to user/frontend action. | |
95 |
|
108 | |||
96 | This includes comm and widget messages and silent execution, as well as user |
|
109 | This includes comm and widget messages and silent execution, as well as user | |
97 |
code cells. |
|
110 | code cells. | |
|
111 | """ | |||
98 | pass |
|
112 | pass | |
99 |
|
113 | |||
100 | @_define_event |
|
114 | @_define_event | |
101 | def pre_run_cell(): |
|
115 | def pre_run_cell(info): | |
102 |
"""Fires before user-entered code runs. |
|
116 | """Fires before user-entered code runs. | |
|
117 | ||||
|
118 | Parameters | |||
|
119 | ---------- | |||
|
120 | info : :class:`~IPython.core.interactiveshell.ExecutionInfo` | |||
|
121 | An object containing information used for the code execution. | |||
|
122 | """ | |||
103 | pass |
|
123 | pass | |
104 |
|
124 | |||
105 | @_define_event |
|
125 | @_define_event | |
@@ -107,12 +127,19 b' def post_execute():' | |||||
107 | """Fires after code is executed in response to user/frontend action. |
|
127 | """Fires after code is executed in response to user/frontend action. | |
108 |
|
128 | |||
109 | This includes comm and widget messages and silent execution, as well as user |
|
129 | This includes comm and widget messages and silent execution, as well as user | |
110 |
code cells. |
|
130 | code cells. | |
|
131 | """ | |||
111 | pass |
|
132 | pass | |
112 |
|
133 | |||
113 | @_define_event |
|
134 | @_define_event | |
114 | def post_run_cell(): |
|
135 | def post_run_cell(result): | |
115 |
"""Fires after user-entered code runs. |
|
136 | """Fires after user-entered code runs. | |
|
137 | ||||
|
138 | Parameters | |||
|
139 | ---------- | |||
|
140 | result : :class:`~IPython.core.interactiveshell.ExecutionResult` | |||
|
141 | The object which will be returned as the execution result. | |||
|
142 | """ | |||
116 | pass |
|
143 | pass | |
117 |
|
144 | |||
118 | @_define_event |
|
145 | @_define_event |
@@ -173,6 +173,30 b' class DummyMod(object):' | |||||
173 | pass |
|
173 | pass | |
174 |
|
174 | |||
175 |
|
175 | |||
|
176 | class ExecutionInfo(object): | |||
|
177 | """The arguments used for a call to :meth:`InteractiveShell.run_cell` | |||
|
178 | ||||
|
179 | Stores information about what is going to happen. | |||
|
180 | """ | |||
|
181 | raw_cell = None | |||
|
182 | store_history = False | |||
|
183 | silent = False | |||
|
184 | shell_futures = True | |||
|
185 | ||||
|
186 | def __init__(self, raw_cell, store_history, silent, shell_futures): | |||
|
187 | self.raw_cell = raw_cell | |||
|
188 | self.store_history = store_history | |||
|
189 | self.silent = silent | |||
|
190 | self.shell_futures = shell_futures | |||
|
191 | ||||
|
192 | def __repr__(self): | |||
|
193 | name = self.__class__.__qualname__ | |||
|
194 | raw_cell = ((self.raw_cell[:50] + '..') | |||
|
195 | if len(self.raw_cell) > 50 else self.raw_cell) | |||
|
196 | return '<%s object at %x, raw_cell="%s" store_history=%s silent=%s shell_futures=%s result=%s>' %\ | |||
|
197 | (name, id(self), raw_cell, store_history, silent, shell_futures, repr(self.result)) | |||
|
198 | ||||
|
199 | ||||
176 | class ExecutionResult(object): |
|
200 | class ExecutionResult(object): | |
177 | """The result of a call to :meth:`InteractiveShell.run_cell` |
|
201 | """The result of a call to :meth:`InteractiveShell.run_cell` | |
178 |
|
202 | |||
@@ -181,8 +205,12 b' class ExecutionResult(object):' | |||||
181 | execution_count = None |
|
205 | execution_count = None | |
182 | error_before_exec = None |
|
206 | error_before_exec = None | |
183 | error_in_exec = None |
|
207 | error_in_exec = None | |
|
208 | info = None | |||
184 | result = None |
|
209 | result = None | |
185 |
|
210 | |||
|
211 | def __init__(self, info): | |||
|
212 | self.info = info | |||
|
213 | ||||
186 | @property |
|
214 | @property | |
187 | def success(self): |
|
215 | def success(self): | |
188 | return (self.error_before_exec is None) and (self.error_in_exec is None) |
|
216 | return (self.error_before_exec is None) and (self.error_in_exec is None) | |
@@ -196,8 +224,8 b' class ExecutionResult(object):' | |||||
196 |
|
224 | |||
197 | def __repr__(self): |
|
225 | def __repr__(self): | |
198 | name = self.__class__.__qualname__ |
|
226 | name = self.__class__.__qualname__ | |
199 | return '<%s object at %x, execution_count=%s error_before_exec=%s error_in_exec=%s result=%s>' %\ |
|
227 | return '<%s object at %x, execution_count=%s error_before_exec=%s error_in_exec=%s info=%s result=%s>' %\ | |
200 | (name, id(self), self.execution_count, self.error_before_exec, self.error_in_exec, repr(self.result)) |
|
228 | (name, id(self), self.execution_count, self.error_before_exec, self.error_in_exec, repr(self.info), repr(self.result)) | |
201 |
|
229 | |||
202 |
|
230 | |||
203 | class InteractiveShell(SingletonConfigurable): |
|
231 | class InteractiveShell(SingletonConfigurable): | |
@@ -1879,7 +1907,7 b' class InteractiveShell(SingletonConfigurable):' | |||||
1879 | # This is overridden in TerminalInteractiveShell to show a message about |
|
1907 | # This is overridden in TerminalInteractiveShell to show a message about | |
1880 | # the %paste magic. |
|
1908 | # the %paste magic. | |
1881 | def showindentationerror(self): |
|
1909 | def showindentationerror(self): | |
1882 | """Called by run_cell when there's an IndentationError in code entered |
|
1910 | """Called by _run_cell when there's an IndentationError in code entered | |
1883 | at the prompt. |
|
1911 | at the prompt. | |
1884 |
|
1912 | |||
1885 | This is overridden in TerminalInteractiveShell to show a message about |
|
1913 | This is overridden in TerminalInteractiveShell to show a message about | |
@@ -2621,13 +2649,38 b' class InteractiveShell(SingletonConfigurable):' | |||||
2621 | ------- |
|
2649 | ------- | |
2622 | result : :class:`ExecutionResult` |
|
2650 | result : :class:`ExecutionResult` | |
2623 | """ |
|
2651 | """ | |
2624 | result = ExecutionResult() |
|
2652 | try: | |
|
2653 | result = self._run_cell( | |||
|
2654 | raw_cell, store_history, silent, shell_futures) | |||
|
2655 | finally: | |||
|
2656 | self.events.trigger('post_execute') | |||
|
2657 | if not silent: | |||
|
2658 | self.events.trigger('post_run_cell', result) | |||
|
2659 | return result | |||
|
2660 | ||||
|
2661 | def _run_cell(self, raw_cell, store_history, silent, shell_futures): | |||
|
2662 | """Internal method to run a complete IPython cell. | |||
|
2663 | ||||
|
2664 | Parameters | |||
|
2665 | ---------- | |||
|
2666 | raw_cell : str | |||
|
2667 | store_history : bool | |||
|
2668 | silent : bool | |||
|
2669 | shell_futures : bool | |||
|
2670 | ||||
|
2671 | Returns | |||
|
2672 | ------- | |||
|
2673 | result : :class:`ExecutionResult` | |||
|
2674 | """ | |||
|
2675 | info = ExecutionInfo( | |||
|
2676 | raw_cell, store_history, silent, shell_futures) | |||
|
2677 | result = ExecutionResult(info) | |||
2625 |
|
2678 | |||
2626 | if (not raw_cell) or raw_cell.isspace(): |
|
2679 | if (not raw_cell) or raw_cell.isspace(): | |
2627 | self.last_execution_succeeded = True |
|
2680 | self.last_execution_succeeded = True | |
2628 | self.last_execution_result = result |
|
2681 | self.last_execution_result = result | |
2629 | return result |
|
2682 | return result | |
2630 |
|
2683 | |||
2631 | if silent: |
|
2684 | if silent: | |
2632 | store_history = False |
|
2685 | store_history = False | |
2633 |
|
2686 | |||
@@ -2642,7 +2695,7 b' class InteractiveShell(SingletonConfigurable):' | |||||
2642 |
|
2695 | |||
2643 | self.events.trigger('pre_execute') |
|
2696 | self.events.trigger('pre_execute') | |
2644 | if not silent: |
|
2697 | if not silent: | |
2645 | self.events.trigger('pre_run_cell') |
|
2698 | self.events.trigger('pre_run_cell', info) | |
2646 |
|
2699 | |||
2647 | # If any of our input transformation (input_transformer_manager or |
|
2700 | # If any of our input transformation (input_transformer_manager or | |
2648 | # prefilter_manager) raises an exception, we store it in this variable |
|
2701 | # prefilter_manager) raises an exception, we store it in this variable | |
@@ -2723,7 +2776,7 b' class InteractiveShell(SingletonConfigurable):' | |||||
2723 | self.displayhook.exec_result = result |
|
2776 | self.displayhook.exec_result = result | |
2724 |
|
2777 | |||
2725 | # Execute the user code |
|
2778 | # Execute the user code | |
2726 |
interactivity = |
|
2779 | interactivity = 'none' if silent else self.ast_node_interactivity | |
2727 | has_raised = self.run_ast_nodes(code_ast.body, cell_name, |
|
2780 | has_raised = self.run_ast_nodes(code_ast.body, cell_name, | |
2728 | interactivity=interactivity, compiler=compiler, result=result) |
|
2781 | interactivity=interactivity, compiler=compiler, result=result) | |
2729 |
|
2782 | |||
@@ -2734,10 +2787,6 b' class InteractiveShell(SingletonConfigurable):' | |||||
2734 | # ExecutionResult |
|
2787 | # ExecutionResult | |
2735 | self.displayhook.exec_result = None |
|
2788 | self.displayhook.exec_result = None | |
2736 |
|
2789 | |||
2737 | self.events.trigger('post_execute') |
|
|||
2738 | if not silent: |
|
|||
2739 | self.events.trigger('post_run_cell') |
|
|||
2740 |
|
||||
2741 | if store_history: |
|
2790 | if store_history: | |
2742 | # Write output to the database. Does nothing unless |
|
2791 | # Write output to the database. Does nothing unless | |
2743 | # history output logging is enabled. |
|
2792 | # history output logging is enabled. |
@@ -1,15 +1,24 b'' | |||||
|
1 | from backcall import callback_prototype | |||
1 | import unittest |
|
2 | import unittest | |
2 | from unittest.mock import Mock |
|
3 | from unittest.mock import Mock | |
3 |
|
4 | |||
4 | from IPython.core import events |
|
5 | from IPython.core import events | |
5 | import IPython.testing.tools as tt |
|
6 | import IPython.testing.tools as tt | |
6 |
|
7 | |||
|
8 | ||||
|
9 | @events._define_event | |||
7 | def ping_received(): |
|
10 | def ping_received(): | |
8 | pass |
|
11 | pass | |
9 |
|
12 | |||
|
13 | ||||
|
14 | @events._define_event | |||
|
15 | def event_with_argument(argument): | |||
|
16 | pass | |||
|
17 | ||||
|
18 | ||||
10 | class CallbackTests(unittest.TestCase): |
|
19 | class CallbackTests(unittest.TestCase): | |
11 | def setUp(self): |
|
20 | def setUp(self): | |
12 | self.em = events.EventManager(get_ipython(), {'ping_received': ping_received}) |
|
21 | self.em = events.EventManager(get_ipython(), {'ping_received': ping_received, 'event_with_argument': event_with_argument}) | |
13 |
|
22 | |||
14 | def test_register_unregister(self): |
|
23 | def test_register_unregister(self): | |
15 | cb = Mock() |
|
24 | cb = Mock() | |
@@ -49,3 +58,16 b' class CallbackTests(unittest.TestCase):' | |||||
49 | self.em.trigger('ping_received') |
|
58 | self.em.trigger('ping_received') | |
50 | self.assertEqual([True, True, False], invoked) |
|
59 | self.assertEqual([True, True, False], invoked) | |
51 | self.assertEqual([func3], self.em.callbacks['ping_received']) |
|
60 | self.assertEqual([func3], self.em.callbacks['ping_received']) | |
|
61 | ||||
|
62 | def test_ignore_event_arguments_if_no_argument_required(self): | |||
|
63 | call_count = [0] | |||
|
64 | def event_with_no_argument(): | |||
|
65 | call_count[0] += 1 | |||
|
66 | ||||
|
67 | self.em.register('event_with_argument', event_with_no_argument) | |||
|
68 | self.em.trigger('event_with_argument', 'the argument') | |||
|
69 | self.assertEqual(call_count[0], 1) | |||
|
70 | ||||
|
71 | self.em.unregister('event_with_argument', event_with_no_argument) | |||
|
72 | self.em.trigger('ping_received') | |||
|
73 | self.assertEqual(call_count[0], 1) |
@@ -263,6 +263,7 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 | all_mocks = [pre_explicit, pre_always, post_explicit, post_always] | |||
266 |
|
267 | |||
267 | ip.events.register('pre_run_cell', pre_explicit) |
|
268 | ip.events.register('pre_run_cell', pre_explicit) | |
268 | ip.events.register('pre_execute', pre_always) |
|
269 | ip.events.register('pre_execute', pre_always) | |
@@ -280,6 +281,19 b' class InteractiveShellTestCase(unittest.TestCase):' | |||||
280 | ip.run_cell("1") |
|
281 | ip.run_cell("1") | |
281 | assert pre_explicit.called |
|
282 | assert pre_explicit.called | |
282 | assert post_explicit.called |
|
283 | assert post_explicit.called | |
|
284 | info, = pre_explicit.call_args[0] | |||
|
285 | result, = post_explicit.call_args[0] | |||
|
286 | self.assertEqual(info, result.info) | |||
|
287 | # check that post hooks are always called | |||
|
288 | [m.reset_mock() for m in all_mocks] | |||
|
289 | ip.run_cell("syntax error") | |||
|
290 | assert pre_always.called | |||
|
291 | assert pre_explicit.called | |||
|
292 | assert post_always.called | |||
|
293 | assert post_explicit.called | |||
|
294 | info, = pre_explicit.call_args[0] | |||
|
295 | result, = post_explicit.call_args[0] | |||
|
296 | self.assertEqual(info, result.info) | |||
283 | finally: |
|
297 | finally: | |
284 | # remove post-exec |
|
298 | # remove post-exec | |
285 | ip.events.unregister('pre_run_cell', pre_explicit) |
|
299 | ip.events.unregister('pre_run_cell', pre_explicit) |
@@ -21,14 +21,24 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 pre_run_cell(self, info): | |||
|
25 | print('Cell code: "%s"' % info.raw_cell) | |||
|
26 | ||||
24 | def post_execute(self): |
|
27 | def post_execute(self): | |
25 | if self.shell.user_ns.get('x', None) != self.last_x: |
|
28 | if self.shell.user_ns.get('x', None) != self.last_x: | |
26 | print("x changed!") |
|
29 | print("x changed!") | |
27 |
|
30 | |||
|
31 | def post_run_cell(self, result): | |||
|
32 | print('Cell code: "%s"' % result.info.raw_cell) | |||
|
33 | if result.error_before_exec: | |||
|
34 | print('Error before execution: %s' % result.error_before_exec) | |||
|
35 | ||||
28 | def load_ipython_extension(ip): |
|
36 | def load_ipython_extension(ip): | |
29 | vw = VarWatcher(ip) |
|
37 | vw = VarWatcher(ip) | |
30 | ip.events.register('pre_execute', vw.pre_execute) |
|
38 | ip.events.register('pre_execute', vw.pre_execute) | |
|
39 | ip.events.register('pre_run_cell', vw.pre_run_cell) | |||
31 | ip.events.register('post_execute', vw.post_execute) |
|
40 | ip.events.register('post_execute', vw.post_execute) | |
|
41 | ip.events.register('post_run_cell', vw.post_run_cell) | |||
32 |
|
42 | |||
33 |
|
43 | |||
34 | Events |
|
44 | Events | |
@@ -53,6 +63,7 b' pre_run_cell' | |||||
53 |
|
63 | |||
54 | ``pre_run_cell`` fires prior to interactive execution (e.g. a cell in a notebook). |
|
64 | ``pre_run_cell`` fires prior to interactive execution (e.g. a cell in a notebook). | |
55 | It can be used to note the state prior to execution, and keep track of changes. |
|
65 | It can be used to note the state prior to execution, and keep track of changes. | |
|
66 | An object containing information used for the code execution is provided as an argument. | |||
56 |
|
67 | |||
57 | pre_execute |
|
68 | pre_execute | |
58 | ----------- |
|
69 | ----------- | |
@@ -67,7 +78,8 b' post_run_cell' | |||||
67 | ``post_run_cell`` runs after interactive execution (e.g. a cell in a notebook). |
|
78 | ``post_run_cell`` runs after interactive execution (e.g. a cell in a notebook). | |
68 | It can be used to cleanup or notify or perform operations on any side effects produced during execution. |
|
79 | It can be used to cleanup or notify or perform operations on any side effects produced during execution. | |
69 | For instance, the inline matplotlib backend uses this event to display any figures created but not explicitly displayed during the course of the cell. |
|
80 | For instance, the inline matplotlib backend uses this event to display any figures created but not explicitly displayed during the course of the cell. | |
70 |
|
81 | The object which will be returned as the execution result is provided as an | ||
|
82 | argument. | |||
71 |
|
83 | |||
72 | post_execute |
|
84 | post_execute | |
73 | ------------ |
|
85 | ------------ |
@@ -6,11 +6,12 b' Execution semantics in the IPython kernel' | |||||
6 | The execution of user code consists of the following phases: |
|
6 | The execution of user code consists of the following phases: | |
7 |
|
7 | |||
8 | 1. Fire the ``pre_execute`` event. |
|
8 | 1. Fire the ``pre_execute`` event. | |
9 | 2. Fire the ``pre_run_cell`` event unless silent is True. |
|
9 | 2. Fire the ``pre_run_cell`` event unless silent is ``True``. | |
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 silent is ``True``. | |||
14 |
|
15 | |||
15 | .. seealso:: |
|
16 | .. seealso:: | |
16 |
|
17 |
@@ -192,6 +192,7 b' install_requires = [' | |||||
192 | 'traitlets>=4.2', |
|
192 | 'traitlets>=4.2', | |
193 | 'prompt_toolkit>=1.0.15,<2.0.0', |
|
193 | 'prompt_toolkit>=1.0.15,<2.0.0', | |
194 | 'pygments', |
|
194 | 'pygments', | |
|
195 | 'backcall', | |||
195 | ] |
|
196 | ] | |
196 |
|
197 | |||
197 | # Platform-specific dependencies: |
|
198 | # Platform-specific dependencies: |
General Comments 0
You need to be logged in to leave comments.
Login now