Show More
@@ -13,27 +13,8 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 | from inspect import isfunction | |
|
18 | try: | |
|
19 | from inspect import getfullargspec | |
|
20 | except: | |
|
21 | from inspect import getargspec as getfullargspec # for Python2 compatibility. | |
|
22 | ||
|
23 | # original function -> wrapper function mapping | |
|
24 | compatibility_wrapper_functions = {} | |
|
25 | ||
|
26 | def _compatibility_wrapper_for(function): | |
|
27 | """Returns a wrapper for a function without args that accepts any args.""" | |
|
28 | if len(getfullargspec(function).args) > 0: | |
|
29 | raise TypeError('%s cannot have arguments' % function) | |
|
30 | if function in compatibility_wrapper_functions: | |
|
31 | return compatibility_wrapper_functions[function] | |
|
32 | @wraps(function) | |
|
33 | def wrapper(*args, **kwargs): | |
|
34 | function() | |
|
35 | compatibility_wrapper_functions[function] = wrapper | |
|
36 | return wrapper | |
|
16 | from backcall import callback_prototype | |
|
17 | ||
|
37 | 18 | |
|
38 | 19 | class EventManager(object): |
|
39 | 20 | """Manage a collection of events and a sequence of callbacks for each. |
@@ -78,24 +59,11 b' class EventManager(object):' | |||
|
78 | 59 | """ |
|
79 | 60 | if not callable(function): |
|
80 | 61 | raise TypeError('Need a callable, got %r' % function) |
|
81 | ||
|
82 | callback_proto = available_events.get(event) | |
|
83 | if (isfunction(callback_proto) and isfunction(function) and | |
|
84 | len(getfullargspec(callback_proto).args) > 0 and | |
|
85 | len(getfullargspec(function).args) == 0): | |
|
86 | # `callback_proto` has args but `function` does not, so a | |
|
87 | # compatibility wrapper is needed. | |
|
88 | self.callbacks[event].append(_compatibility_wrapper_for(function)) | |
|
89 | else: | |
|
90 | self.callbacks[event].append(function) | |
|
62 | self.callbacks[event].append(_adapt_function(event, function)) | |
|
91 | 63 | |
|
92 | 64 | def unregister(self, event, function): |
|
93 | 65 | """Remove a callback from the given event.""" |
|
94 | wrapper = compatibility_wrapper_functions.get(function) | |
|
95 | if wrapper: | |
|
96 | self.callbacks[event].remove(wrapper) | |
|
97 | else: | |
|
98 | self.callbacks[event].remove(function) | |
|
66 | self.callbacks[event].remove(_adapt_function(event, function)) | |
|
99 | 67 | |
|
100 | 68 | def trigger(self, event, *args, **kwargs): |
|
101 | 69 | """Call callbacks for ``event``. |
@@ -113,10 +81,30 b' class EventManager(object):' | |||
|
113 | 81 | # event_name -> prototype mapping |
|
114 | 82 | available_events = {} |
|
115 | 83 | |
|
84 | # (event, function) -> adapted function mapping | |
|
85 | adapted_functions = {} | |
|
86 | ||
|
87 | ||
|
116 | 88 | def _define_event(callback_proto): |
|
117 | 89 | available_events[callback_proto.__name__] = callback_proto |
|
118 | 90 | return callback_proto |
|
119 | 91 | |
|
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 | ||
|
120 | 108 | # ------------------------------------------------------------------------------ |
|
121 | 109 | # Callback prototypes |
|
122 | 110 | # |
@@ -125,7 +113,8 b' def _define_event(callback_proto):' | |||
|
125 | 113 | # ------------------------------------------------------------------------------ |
|
126 | 114 | |
|
127 | 115 | @_define_event |
|
128 | def pre_execute(result): | |
|
116 | @callback_prototype | |
|
117 | def pre_execute(request): | |
|
129 | 118 | """Fires before code is executed in response to user/frontend action. |
|
130 | 119 | |
|
131 | 120 | This includes comm and widget messages and silent execution, as well as user |
@@ -133,23 +122,25 b' def pre_execute(result):' | |||
|
133 | 122 | |
|
134 | 123 | Parameters |
|
135 | 124 | ---------- |
|
136 |
res |
|
|
137 |
The object |
|
|
125 | request : :class:`~IPython.core.interactiveshell.ExecutionRequest` | |
|
126 | The object representing the code execution request. | |
|
138 | 127 | """ |
|
139 | 128 | pass |
|
140 | 129 | |
|
141 | 130 | @_define_event |
|
142 | def pre_run_cell(result): | |
|
131 | @callback_prototype | |
|
132 | def pre_run_cell(request): | |
|
143 | 133 | """Fires before user-entered code runs. |
|
144 | 134 | |
|
145 | 135 | Parameters |
|
146 | 136 | ---------- |
|
147 |
res |
|
|
148 |
The object |
|
|
137 | request : :class:`~IPython.core.interactiveshell.ExecutionRequest` | |
|
138 | The object representing the code execution request. | |
|
149 | 139 | """ |
|
150 | 140 | pass |
|
151 | 141 | |
|
152 | 142 | @_define_event |
|
143 | @callback_prototype | |
|
153 | 144 | def post_execute(result): |
|
154 | 145 | """Fires after code is executed in response to user/frontend action. |
|
155 | 146 | |
@@ -164,6 +155,7 b' def post_execute(result):' | |||
|
164 | 155 | pass |
|
165 | 156 | |
|
166 | 157 | @_define_event |
|
158 | @callback_prototype | |
|
167 | 159 | def post_run_cell(result): |
|
168 | 160 | """Fires after user-entered code runs. |
|
169 | 161 |
@@ -173,6 +173,30 b' class DummyMod(object):' | |||
|
173 | 173 | pass |
|
174 | 174 | |
|
175 | 175 | |
|
176 | class ExecutionRequest(object): | |
|
177 | """The request of 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 | 200 | class ExecutionResult(object): |
|
177 | 201 | """The result of a call to :meth:`InteractiveShell.run_cell` |
|
178 | 202 | |
@@ -181,8 +205,12 b' class ExecutionResult(object):' | |||
|
181 | 205 | execution_count = None |
|
182 | 206 | error_before_exec = None |
|
183 | 207 | error_in_exec = None |
|
208 | request = None | |
|
184 | 209 | result = None |
|
185 | 210 | |
|
211 | def __init__(self, request): | |
|
212 | self.request = request | |
|
213 | ||
|
186 | 214 | @property |
|
187 | 215 | def success(self): |
|
188 | 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 | 225 | def __repr__(self): |
|
198 | 226 | name = self.__class__.__qualname__ |
|
199 | return '<%s object at %x, execution_count=%s error_before_exec=%s error_in_exec=%s result=%s>' %\ | |
|
200 | (name, id(self), self.execution_count, self.error_before_exec, self.error_in_exec, repr(self.result)) | |
|
227 | return '<%s object at %x, execution_count=%s error_before_exec=%s error_in_exec=%s request=%s result=%s>' %\ | |
|
228 | (name, id(self), self.execution_count, self.error_before_exec, self.error_in_exec, repr(self.request), repr(self.result)) | |
|
201 | 229 | |
|
202 | 230 | |
|
203 | 231 | class InteractiveShell(SingletonConfigurable): |
@@ -866,7 +894,7 b' class InteractiveShell(SingletonConfigurable):' | |||
|
866 | 894 | "ip.events.register('post_run_cell', func) instead.", stacklevel=2) |
|
867 | 895 | self.events.register('post_run_cell', func) |
|
868 | 896 | |
|
869 | def _clear_warning_registry(self): | |
|
897 | def _clear_warning_registry(self, request): | |
|
870 | 898 | # clear the warning registry, so that different code blocks with |
|
871 | 899 | # overlapping line number ranges don't cause spurious suppression of |
|
872 | 900 | # warnings (see gh-6611 for details) |
@@ -2644,13 +2672,15 b' class InteractiveShell(SingletonConfigurable):' | |||
|
2644 | 2672 | ------- |
|
2645 | 2673 | result : :class:`ExecutionResult` |
|
2646 | 2674 | """ |
|
2647 |
res |
|
|
2675 | request = ExecutionRequest( | |
|
2676 | raw_cell, store_history, silent, shell_futures) | |
|
2677 | result = ExecutionResult(request) | |
|
2648 | 2678 | |
|
2649 | 2679 | if (not raw_cell) or raw_cell.isspace(): |
|
2650 | 2680 | self.last_execution_succeeded = True |
|
2651 | 2681 | self.last_execution_result = result |
|
2652 | 2682 | return result |
|
2653 | ||
|
2683 | ||
|
2654 | 2684 | if silent: |
|
2655 | 2685 | store_history = False |
|
2656 | 2686 | |
@@ -2663,9 +2693,9 b' class InteractiveShell(SingletonConfigurable):' | |||
|
2663 | 2693 | self.last_execution_result = result |
|
2664 | 2694 | return result |
|
2665 | 2695 | |
|
2666 | self.events.trigger('pre_execute') | |
|
2696 | self.events.trigger('pre_execute', request) | |
|
2667 | 2697 | if not silent: |
|
2668 | self.events.trigger('pre_run_cell') | |
|
2698 | self.events.trigger('pre_run_cell', request) | |
|
2669 | 2699 | |
|
2670 | 2700 | # If any of our input transformation (input_transformer_manager or |
|
2671 | 2701 | # prefilter_manager) raises an exception, we store it in this variable |
@@ -1,17 +1,23 b'' | |||
|
1 | from backcall import callback_prototype | |
|
1 | 2 | import unittest |
|
2 | 3 | from unittest.mock import Mock |
|
3 | 4 | |
|
4 | 5 | from IPython.core import events |
|
5 | 6 | import IPython.testing.tools as tt |
|
6 | 7 | |
|
8 | ||
|
7 | 9 | @events._define_event |
|
10 | @callback_prototype | |
|
8 | 11 | def ping_received(): |
|
9 | 12 | pass |
|
10 | 13 | |
|
14 | ||
|
11 | 15 | @events._define_event |
|
16 | @callback_prototype | |
|
12 | 17 | def event_with_argument(argument): |
|
13 | 18 | pass |
|
14 | 19 | |
|
20 | ||
|
15 | 21 | class CallbackTests(unittest.TestCase): |
|
16 | 22 | def setUp(self): |
|
17 | 23 | self.em = events.EventManager(get_ipython(), {'ping_received': ping_received, 'event_with_argument': event_with_argument}) |
@@ -60,7 +66,7 b' class CallbackTests(unittest.TestCase):' | |||
|
60 | 66 | def event_with_no_argument(): |
|
61 | 67 | call_count[0] += 1 |
|
62 | 68 | |
|
63 |
self.em.register('event_with_argument', event_with_no_argument) |
|
|
69 | self.em.register('event_with_argument', event_with_no_argument) | |
|
64 | 70 | self.em.trigger('event_with_argument', 'the argument') |
|
65 | 71 | self.assertEqual(call_count[0], 1) |
|
66 | 72 |
@@ -276,6 +276,9 b' class InteractiveShellTestCase(unittest.TestCase):' | |||
|
276 | 276 | assert not pre_explicit.called |
|
277 | 277 | assert post_always.called |
|
278 | 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) | |
|
279 | 282 | # double-check that non-silent exec did what we expected |
|
280 | 283 | # silent to avoid |
|
281 | 284 | ip.run_cell("1") |
@@ -288,6 +291,9 b' class InteractiveShellTestCase(unittest.TestCase):' | |||
|
288 | 291 | assert pre_explicit.called |
|
289 | 292 | assert post_always.called |
|
290 | 293 | assert post_explicit.called |
|
294 | request, = pre_always.call_args[0] | |
|
295 | result, = post_always.call_args[0] | |
|
296 | self.assertEqual(request, result.request) | |
|
291 | 297 | finally: |
|
292 | 298 | # remove post-exec |
|
293 | 299 | ip.events.unregister('pre_run_cell', pre_explicit) |
@@ -18,10 +18,12 b' For example::' | |||
|
18 | 18 | self.shell = ip |
|
19 | 19 | self.last_x = None |
|
20 | 20 | |
|
21 | def pre_execute(self): | |
|
21 | def pre_execute(self, request): | |
|
22 | print('Cell code: "%s"' % request.raw_cell) | |
|
22 | 23 | self.last_x = self.shell.user_ns.get('x', None) |
|
23 | 24 | |
|
24 | 25 | def post_execute(self, result): |
|
26 | print('Cell code: "%s"' % result.request.raw_cell) | |
|
25 | 27 | if result.error_before_exec: |
|
26 | 28 | print('Error before execution: %s' % result.error_before_exec) |
|
27 | 29 | if self.shell.user_ns.get('x', None) != self.last_x: |
@@ -55,8 +57,7 b' pre_run_cell' | |||
|
55 | 57 | |
|
56 | 58 | ``pre_run_cell`` fires prior to interactive execution (e.g. a cell in a notebook). |
|
57 | 59 | It can be used to note the state prior to execution, and keep track of changes. |
|
58 |
The object |
|
|
59 | argument, even though the actual result is not yet available. | |
|
60 | The object representing the code execution request is provided as an argument. | |
|
60 | 61 | |
|
61 | 62 | pre_execute |
|
62 | 63 | ----------- |
@@ -64,8 +65,7 b' pre_execute' | |||
|
64 | 65 | ``pre_execute`` is like ``pre_run_cell``, but is triggered prior to *any* execution. |
|
65 | 66 | Sometimes code can be executed by libraries, etc. which |
|
66 | 67 | skipping the history/display mechanisms, in which cases ``pre_run_cell`` will not fire. |
|
67 |
The object |
|
|
68 | argument, even though the actual result is not yet available. | |
|
68 | The object representing the code execution request is provided as an argument. | |
|
69 | 69 | |
|
70 | 70 | post_run_cell |
|
71 | 71 | ------------- |
@@ -1,6 +1,7 b'' | |||
|
1 | 1 | The *post* event callbacks are now always called, even when the execution failed |
|
2 | 2 | (for example because of a ``SyntaxError``). |
|
3 |
Additionally, the execution result object |
|
|
4 |
|
|
|
3 | Additionally, the execution request and result objects are now made available in | |
|
4 | the corresponding *pre* or *post* event callbacks in a backward compatible | |
|
5 | manner. | |
|
5 | 6 | |
|
6 | 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