Show More
@@ -40,7 +40,7 b' class EventManager(object):' | |||||
40 | self.callbacks = {n:[] for n in available_events} |
|
40 | self.callbacks = {n:[] for n in available_events} | |
41 |
|
41 | |||
42 | def register(self, event, function): |
|
42 | def register(self, event, function): | |
43 | """Register a new event callback |
|
43 | """Register a new event callback. | |
44 |
|
44 | |||
45 | Parameters |
|
45 | Parameters | |
46 | ---------- |
|
46 | ---------- | |
@@ -59,12 +59,21 b' class EventManager(object):' | |||||
59 | """ |
|
59 | """ | |
60 | if not callable(function): |
|
60 | if not callable(function): | |
61 | raise TypeError('Need a callable, got %r' % function) |
|
61 | raise TypeError('Need a callable, got %r' % function) | |
62 | self.callbacks[event].append(_adapt_function(event, function)) |
|
62 | callback_proto = available_events.get(event) | |
|
63 | self.callbacks[event].append(callback_proto.adapt(function)) | |||
63 |
|
64 | |||
64 | def unregister(self, event, function): |
|
65 | def unregister(self, event, function): | |
65 | """Remove a callback from the given event.""" |
|
66 | """Remove a callback from the given event.""" | |
66 | self.callbacks[event].remove(_adapt_function(event, function)) |
|
67 | if function in self.callbacks[event]: | |
67 |
|
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 | ||||
68 | def trigger(self, event, *args, **kwargs): |
|
77 | def trigger(self, event, *args, **kwargs): | |
69 | """Call callbacks for ``event``. |
|
78 | """Call callbacks for ``event``. | |
70 |
|
79 | |||
@@ -81,30 +90,11 b' class EventManager(object):' | |||||
81 | # event_name -> prototype mapping |
|
90 | # event_name -> prototype mapping | |
82 | available_events = {} |
|
91 | available_events = {} | |
83 |
|
92 | |||
84 | # (event, function) -> adapted function mapping |
|
93 | def _define_event(callback_function): | |
85 | adapted_functions = {} |
|
94 | callback_proto = callback_prototype(callback_function) | |
86 |
|
95 | available_events[callback_function.__name__] = callback_proto | ||
87 |
|
||||
88 | def _define_event(callback_proto): |
|
|||
89 | available_events[callback_proto.__name__] = callback_proto |
|
|||
90 | return callback_proto |
|
96 | return callback_proto | |
91 |
|
97 | |||
92 |
|
||||
93 | def _adapt_function(event, function): |
|
|||
94 | """Adapts and caches a function using `backcall` to provide compatibility. |
|
|||
95 |
|
||||
96 | Function adaptations depend not only on the function but also on the event, |
|
|||
97 | as events may expect different arguments (e.g. `request` vs. `result`). |
|
|||
98 | Hence, `(event, function)` is used as the cache key. |
|
|||
99 | """ |
|
|||
100 | if (event, function) in adapted_functions: |
|
|||
101 | return adapted_functions[(event, function)] |
|
|||
102 | callback_proto = available_events.get(event) |
|
|||
103 | adapted_function = callback_proto.adapt(function) |
|
|||
104 | adapted_functions[(event, function)] = adapted_function |
|
|||
105 | return adapted_function |
|
|||
106 |
|
||||
107 |
|
||||
108 | # ------------------------------------------------------------------------------ |
|
98 | # ------------------------------------------------------------------------------ | |
109 | # Callback prototypes |
|
99 | # Callback prototypes | |
110 | # |
|
100 | # | |
@@ -113,49 +103,35 b' def _adapt_function(event, function):' | |||||
113 | # ------------------------------------------------------------------------------ |
|
103 | # ------------------------------------------------------------------------------ | |
114 |
|
104 | |||
115 | @_define_event |
|
105 | @_define_event | |
116 | @callback_prototype |
|
106 | def pre_execute(): | |
117 | def pre_execute(request): |
|
|||
118 | """Fires before code is executed in response to user/frontend action. |
|
107 | """Fires before code is executed in response to user/frontend action. | |
119 |
|
108 | |||
120 | 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 | |
121 | code cells. |
|
110 | code cells. | |
122 |
|
||||
123 | Parameters |
|
|||
124 | ---------- |
|
|||
125 | request : :class:`~IPython.core.interactiveshell.ExecutionRequest` |
|
|||
126 | The object representing the code execution request. |
|
|||
127 | """ |
|
111 | """ | |
128 | pass |
|
112 | pass | |
129 |
|
113 | |||
130 | @_define_event |
|
114 | @_define_event | |
131 | @callback_prototype |
|
115 | def pre_run_cell(info): | |
132 | def pre_run_cell(request): |
|
|||
133 | """Fires before user-entered code runs. |
|
116 | """Fires before user-entered code runs. | |
134 |
|
117 | |||
135 | Parameters |
|
118 | Parameters | |
136 | ---------- |
|
119 | ---------- | |
137 |
|
|
120 | info : :class:`~IPython.core.interactiveshell.ExecutionInfo` | |
138 |
|
|
121 | An object containing information used for the code execution. | |
139 | """ |
|
122 | """ | |
140 | pass |
|
123 | pass | |
141 |
|
124 | |||
142 | @_define_event |
|
125 | @_define_event | |
143 | @callback_prototype |
|
126 | def post_execute(): | |
144 | def post_execute(result): |
|
|||
145 | """Fires after code is executed in response to user/frontend action. |
|
127 | """Fires after code is executed in response to user/frontend action. | |
146 |
|
128 | |||
147 | 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 | |
148 | code cells. |
|
130 | code cells. | |
149 |
|
||||
150 | Parameters |
|
|||
151 | ---------- |
|
|||
152 | result : :class:`~IPython.core.interactiveshell.ExecutionResult` |
|
|||
153 | The object which will be returned as the execution result. |
|
|||
154 | """ |
|
131 | """ | |
155 | pass |
|
132 | pass | |
156 |
|
133 | |||
157 | @_define_event |
|
134 | @_define_event | |
158 | @callback_prototype |
|
|||
159 | def post_run_cell(result): |
|
135 | def post_run_cell(result): | |
160 | """Fires after user-entered code runs. |
|
136 | """Fires after user-entered code runs. | |
161 |
|
137 |
@@ -173,8 +173,8 b' class DummyMod(object):' | |||||
173 | pass |
|
173 | pass | |
174 |
|
174 | |||
175 |
|
175 | |||
176 |
class Execution |
|
176 | class ExecutionInfo(object): | |
177 |
"""The |
|
177 | """The arguments used for a call to :meth:`InteractiveShell.run_cell` | |
178 |
|
178 | |||
179 | Stores information about what is going to happen. |
|
179 | Stores information about what is going to happen. | |
180 | """ |
|
180 | """ | |
@@ -205,11 +205,11 b' class ExecutionResult(object):' | |||||
205 | execution_count = None |
|
205 | execution_count = None | |
206 | error_before_exec = None |
|
206 | error_before_exec = None | |
207 | error_in_exec = None |
|
207 | error_in_exec = None | |
208 |
|
|
208 | info = None | |
209 | result = None |
|
209 | result = None | |
210 |
|
210 | |||
211 |
def __init__(self, |
|
211 | def __init__(self, info): | |
212 | self.request = request |
|
212 | self.info = info | |
213 |
|
213 | |||
214 | @property |
|
214 | @property | |
215 | def success(self): |
|
215 | def success(self): | |
@@ -224,8 +224,8 b' class ExecutionResult(object):' | |||||
224 |
|
224 | |||
225 | def __repr__(self): |
|
225 | def __repr__(self): | |
226 | name = self.__class__.__qualname__ |
|
226 | name = self.__class__.__qualname__ | |
227 |
return '<%s object at %x, execution_count=%s error_before_exec=%s error_in_exec=%s |
|
227 | return '<%s object at %x, execution_count=%s error_before_exec=%s error_in_exec=%s info=%s result=%s>' %\ | |
228 |
(name, id(self), self.execution_count, self.error_before_exec, self.error_in_exec, repr(self. |
|
228 | (name, id(self), self.execution_count, self.error_before_exec, self.error_in_exec, repr(self.info), repr(self.result)) | |
229 |
|
229 | |||
230 |
|
230 | |||
231 | class InteractiveShell(SingletonConfigurable): |
|
231 | class InteractiveShell(SingletonConfigurable): | |
@@ -894,7 +894,7 b' class InteractiveShell(SingletonConfigurable):' | |||||
894 | "ip.events.register('post_run_cell', func) instead.", stacklevel=2) |
|
894 | "ip.events.register('post_run_cell', func) instead.", stacklevel=2) | |
895 | self.events.register('post_run_cell', func) |
|
895 | self.events.register('post_run_cell', func) | |
896 |
|
896 | |||
897 |
def _clear_warning_registry(self |
|
897 | def _clear_warning_registry(self): | |
898 | # clear the warning registry, so that different code blocks with |
|
898 | # clear the warning registry, so that different code blocks with | |
899 | # overlapping line number ranges don't cause spurious suppression of |
|
899 | # overlapping line number ranges don't cause spurious suppression of | |
900 | # warnings (see gh-6611 for details) |
|
900 | # warnings (see gh-6611 for details) | |
@@ -2653,7 +2653,7 b' class InteractiveShell(SingletonConfigurable):' | |||||
2653 | result = self._run_cell( |
|
2653 | result = self._run_cell( | |
2654 | raw_cell, store_history, silent, shell_futures) |
|
2654 | raw_cell, store_history, silent, shell_futures) | |
2655 | finally: |
|
2655 | finally: | |
2656 |
self.events.trigger('post_execute' |
|
2656 | self.events.trigger('post_execute') | |
2657 | if not silent: |
|
2657 | if not silent: | |
2658 | self.events.trigger('post_run_cell', result) |
|
2658 | self.events.trigger('post_run_cell', result) | |
2659 | return result |
|
2659 | return result | |
@@ -2672,9 +2672,9 b' class InteractiveShell(SingletonConfigurable):' | |||||
2672 | ------- |
|
2672 | ------- | |
2673 | result : :class:`ExecutionResult` |
|
2673 | result : :class:`ExecutionResult` | |
2674 | """ |
|
2674 | """ | |
2675 |
|
|
2675 | info = ExecutionInfo( | |
2676 | raw_cell, store_history, silent, shell_futures) |
|
2676 | raw_cell, store_history, silent, shell_futures) | |
2677 |
result = ExecutionResult( |
|
2677 | result = ExecutionResult(info) | |
2678 |
|
2678 | |||
2679 | if (not raw_cell) or raw_cell.isspace(): |
|
2679 | if (not raw_cell) or raw_cell.isspace(): | |
2680 | self.last_execution_succeeded = True |
|
2680 | self.last_execution_succeeded = True | |
@@ -2693,9 +2693,9 b' class InteractiveShell(SingletonConfigurable):' | |||||
2693 | self.last_execution_result = result |
|
2693 | self.last_execution_result = result | |
2694 | return result |
|
2694 | return result | |
2695 |
|
2695 | |||
2696 |
self.events.trigger('pre_execute' |
|
2696 | self.events.trigger('pre_execute') | |
2697 | if not silent: |
|
2697 | if not silent: | |
2698 |
self.events.trigger('pre_run_cell', |
|
2698 | self.events.trigger('pre_run_cell', info) | |
2699 |
|
2699 | |||
2700 | # If any of our input transformation (input_transformer_manager or |
|
2700 | # If any of our input transformation (input_transformer_manager or | |
2701 | # prefilter_manager) raises an exception, we store it in this variable |
|
2701 | # prefilter_manager) raises an exception, we store it in this variable |
@@ -7,13 +7,11 b' import IPython.testing.tools as tt' | |||||
7 |
|
7 | |||
8 |
|
8 | |||
9 | @events._define_event |
|
9 | @events._define_event | |
10 | @callback_prototype |
|
|||
11 | def ping_received(): |
|
10 | def ping_received(): | |
12 | pass |
|
11 | pass | |
13 |
|
12 | |||
14 |
|
13 | |||
15 | @events._define_event |
|
14 | @events._define_event | |
16 | @callback_prototype |
|
|||
17 | def event_with_argument(argument): |
|
15 | def event_with_argument(argument): | |
18 | pass |
|
16 | pass | |
19 |
|
17 |
@@ -276,14 +276,14 b' class InteractiveShellTestCase(unittest.TestCase):' | |||||
276 | assert not pre_explicit.called |
|
276 | assert not pre_explicit.called | |
277 | assert post_always.called |
|
277 | assert post_always.called | |
278 | assert not post_explicit.called |
|
278 | assert not post_explicit.called | |
279 | request, = pre_always.call_args[0] |
|
|||
280 | result, = post_always.call_args[0] |
|
|||
281 | self.assertEqual(request, result.request) |
|
|||
282 | # double-check that non-silent exec did what we expected |
|
279 | # double-check that non-silent exec did what we expected | |
283 | # silent to avoid |
|
280 | # silent to avoid | |
284 | ip.run_cell("1") |
|
281 | ip.run_cell("1") | |
285 | assert pre_explicit.called |
|
282 | assert pre_explicit.called | |
286 | 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 |
|
287 | # check that post hooks are always called | |
288 | [m.reset_mock() for m in all_mocks] |
|
288 | [m.reset_mock() for m in all_mocks] | |
289 | ip.run_cell("syntax error") |
|
289 | ip.run_cell("syntax error") | |
@@ -291,9 +291,9 b' class InteractiveShellTestCase(unittest.TestCase):' | |||||
291 | assert pre_explicit.called |
|
291 | assert pre_explicit.called | |
292 | assert post_always.called |
|
292 | assert post_always.called | |
293 | assert post_explicit.called |
|
293 | assert post_explicit.called | |
294 |
|
|
294 | info, = pre_explicit.call_args[0] | |
295 |
result, = post_ |
|
295 | result, = post_explicit.call_args[0] | |
296 |
self.assertEqual( |
|
296 | self.assertEqual(info, result.info) | |
297 | finally: |
|
297 | finally: | |
298 | # remove post-exec |
|
298 | # remove post-exec | |
299 | ip.events.unregister('pre_run_cell', pre_explicit) |
|
299 | ip.events.unregister('pre_run_cell', pre_explicit) |
@@ -18,21 +18,27 b' For example::' | |||||
18 | self.shell = ip |
|
18 | self.shell = ip | |
19 | self.last_x = None |
|
19 | self.last_x = None | |
20 |
|
20 | |||
21 |
def pre_execute(self |
|
21 | def pre_execute(self): | |
22 | print('Cell code: "%s"' % request.raw_cell) |
|
|||
23 | self.last_x = self.shell.user_ns.get('x', None) |
|
22 | self.last_x = self.shell.user_ns.get('x', None) | |
24 |
|
23 | |||
25 |
def p |
|
24 | def pre_run_cell(self, info): | |
26 |
print('Cell code: "%s"' % |
|
25 | print('Cell code: "%s"' % info.raw_cell) | |
27 | if result.error_before_exec: |
|
26 | ||
28 | print('Error before execution: %s' % result.error_before_exec) |
|
27 | def post_execute(self): | |
29 | if self.shell.user_ns.get('x', None) != self.last_x: |
|
28 | if self.shell.user_ns.get('x', None) != self.last_x: | |
30 | print("x changed!") |
|
29 | print("x changed!") | |
31 |
|
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 | ||||
32 | def load_ipython_extension(ip): |
|
36 | def load_ipython_extension(ip): | |
33 | vw = VarWatcher(ip) |
|
37 | vw = VarWatcher(ip) | |
34 | 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) | |||
35 | 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) | |||
36 |
|
42 | |||
37 |
|
43 | |||
38 | Events |
|
44 | Events | |
@@ -57,7 +63,7 b' pre_run_cell' | |||||
57 |
|
63 | |||
58 | ``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). | |
59 | 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. | |
60 |
|
|
66 | An object containing information used for the code execution is provided as an argument. | |
61 |
|
67 | |||
62 | pre_execute |
|
68 | pre_execute | |
63 | ----------- |
|
69 | ----------- | |
@@ -65,7 +71,6 b' pre_execute' | |||||
65 | ``pre_execute`` is like ``pre_run_cell``, but is triggered prior to *any* execution. |
|
71 | ``pre_execute`` is like ``pre_run_cell``, but is triggered prior to *any* execution. | |
66 | Sometimes code can be executed by libraries, etc. which |
|
72 | Sometimes code can be executed by libraries, etc. which | |
67 | skipping the history/display mechanisms, in which cases ``pre_run_cell`` will not fire. |
|
73 | skipping the history/display mechanisms, in which cases ``pre_run_cell`` will not fire. | |
68 | The object representing the code execution request is provided as an argument. |
|
|||
69 |
|
74 | |||
70 | post_run_cell |
|
75 | post_run_cell | |
71 | ------------- |
|
76 | ------------- |
@@ -1,7 +1,7 b'' | |||||
1 | The *post* event callbacks are now always called, even when the execution failed |
|
1 | The *post* event callbacks are now always called, even when the execution failed | |
2 | (for example because of a ``SyntaxError``). |
|
2 | (for example because of a ``SyntaxError``). | |
3 |
Additionally, the execution |
|
3 | Additionally, the execution info and result objects are now made available in | |
4 |
the corresponding *pre* or *post* event callbacks in a backward |
|
4 | the corresponding *pre* or *post* ``*_run_cell`` event callbacks in a backward | |
5 | manner. |
|
5 | compatible manner. | |
6 |
|
6 | |||
7 | * `Related GitHub issue <https://github.com/ipython/ipython/issues/10774>`__ |
|
7 | * `Related GitHub issue <https://github.com/ipython/ipython/issues/10774>`__ |
General Comments 0
You need to be logged in to leave comments.
Login now