Show More
@@ -13,27 +13,8 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 |
|
16 | from backcall import callback_prototype | |
17 | from inspect import isfunction |
|
17 | ||
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 |
|
|||
37 |
|
18 | |||
38 | class EventManager(object): |
|
19 | class EventManager(object): | |
39 | """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. | |
@@ -78,24 +59,11 b' class EventManager(object):' | |||||
78 | """ |
|
59 | """ | |
79 | if not callable(function): |
|
60 | if not callable(function): | |
80 | raise TypeError('Need a callable, got %r' % function) |
|
61 | raise TypeError('Need a callable, got %r' % function) | |
81 |
|
62 | self.callbacks[event].append(_adapt_function(event, function)) | ||
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) |
|
|||
91 |
|
63 | |||
92 | def unregister(self, event, function): |
|
64 | def unregister(self, event, function): | |
93 | """Remove a callback from the given event.""" |
|
65 | """Remove a callback from the given event.""" | |
94 | wrapper = compatibility_wrapper_functions.get(function) |
|
66 | self.callbacks[event].remove(_adapt_function(event, function)) | |
95 | if wrapper: |
|
|||
96 | self.callbacks[event].remove(wrapper) |
|
|||
97 | else: |
|
|||
98 | self.callbacks[event].remove(function) |
|
|||
99 |
|
67 | |||
100 | def trigger(self, event, *args, **kwargs): |
|
68 | def trigger(self, event, *args, **kwargs): | |
101 | """Call callbacks for ``event``. |
|
69 | """Call callbacks for ``event``. | |
@@ -113,10 +81,30 b' class EventManager(object):' | |||||
113 | # event_name -> prototype mapping |
|
81 | # event_name -> prototype mapping | |
114 | available_events = {} |
|
82 | available_events = {} | |
115 |
|
83 | |||
|
84 | # (event, function) -> adapted function mapping | |||
|
85 | adapted_functions = {} | |||
|
86 | ||||
|
87 | ||||
116 | def _define_event(callback_proto): |
|
88 | def _define_event(callback_proto): | |
117 | available_events[callback_proto.__name__] = callback_proto |
|
89 | available_events[callback_proto.__name__] = callback_proto | |
118 | return callback_proto |
|
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 | # Callback prototypes |
|
109 | # Callback prototypes | |
122 | # |
|
110 | # | |
@@ -125,7 +113,8 b' def _define_event(callback_proto):' | |||||
125 | # ------------------------------------------------------------------------------ |
|
113 | # ------------------------------------------------------------------------------ | |
126 |
|
114 | |||
127 | @_define_event |
|
115 | @_define_event | |
128 | def pre_execute(result): |
|
116 | @callback_prototype | |
|
117 | def pre_execute(request): | |||
129 | """Fires before code is executed in response to user/frontend action. |
|
118 | """Fires before code is executed in response to user/frontend action. | |
130 |
|
119 | |||
131 | This includes comm and widget messages and silent execution, as well as user |
|
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 | Parameters |
|
123 | Parameters | |
135 | ---------- |
|
124 | ---------- | |
136 |
res |
|
125 | request : :class:`~IPython.core.interactiveshell.ExecutionRequest` | |
137 |
The object |
|
126 | The object representing the code execution request. | |
138 | """ |
|
127 | """ | |
139 | pass |
|
128 | pass | |
140 |
|
129 | |||
141 | @_define_event |
|
130 | @_define_event | |
142 | def pre_run_cell(result): |
|
131 | @callback_prototype | |
|
132 | def pre_run_cell(request): | |||
143 | """Fires before user-entered code runs. |
|
133 | """Fires before user-entered code runs. | |
144 |
|
134 | |||
145 | Parameters |
|
135 | Parameters | |
146 | ---------- |
|
136 | ---------- | |
147 |
res |
|
137 | request : :class:`~IPython.core.interactiveshell.ExecutionRequest` | |
148 |
The object |
|
138 | The object representing the code execution request. | |
149 | """ |
|
139 | """ | |
150 | pass |
|
140 | pass | |
151 |
|
141 | |||
152 | @_define_event |
|
142 | @_define_event | |
|
143 | @callback_prototype | |||
153 | def post_execute(result): |
|
144 | def post_execute(result): | |
154 | """Fires after code is executed in response to user/frontend action. |
|
145 | """Fires after code is executed in response to user/frontend action. | |
155 |
|
146 | |||
@@ -164,6 +155,7 b' def post_execute(result):' | |||||
164 | pass |
|
155 | pass | |
165 |
|
156 | |||
166 | @_define_event |
|
157 | @_define_event | |
|
158 | @callback_prototype | |||
167 | def post_run_cell(result): |
|
159 | def post_run_cell(result): | |
168 | """Fires after user-entered code runs. |
|
160 | """Fires after user-entered code runs. | |
169 |
|
161 |
@@ -173,6 +173,30 b' class DummyMod(object):' | |||||
173 | pass |
|
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 | 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 | request = None | |||
184 | result = None |
|
209 | result = None | |
185 |
|
210 | |||
|
211 | def __init__(self, request): | |||
|
212 | self.request = request | |||
|
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 request=%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.request), repr(self.result)) | |
201 |
|
229 | |||
202 |
|
230 | |||
203 | class InteractiveShell(SingletonConfigurable): |
|
231 | class InteractiveShell(SingletonConfigurable): | |
@@ -866,7 +894,7 b' class InteractiveShell(SingletonConfigurable):' | |||||
866 | "ip.events.register('post_run_cell', func) instead.", stacklevel=2) |
|
894 | "ip.events.register('post_run_cell', func) instead.", stacklevel=2) | |
867 | self.events.register('post_run_cell', func) |
|
895 | self.events.register('post_run_cell', func) | |
868 |
|
896 | |||
869 | def _clear_warning_registry(self): |
|
897 | def _clear_warning_registry(self, request): | |
870 | # clear the warning registry, so that different code blocks with |
|
898 | # clear the warning registry, so that different code blocks with | |
871 | # overlapping line number ranges don't cause spurious suppression of |
|
899 | # overlapping line number ranges don't cause spurious suppression of | |
872 | # warnings (see gh-6611 for details) |
|
900 | # warnings (see gh-6611 for details) | |
@@ -2644,7 +2672,9 b' class InteractiveShell(SingletonConfigurable):' | |||||
2644 | ------- |
|
2672 | ------- | |
2645 | result : :class:`ExecutionResult` |
|
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 | if (not raw_cell) or raw_cell.isspace(): |
|
2679 | if (not raw_cell) or raw_cell.isspace(): | |
2650 | self.last_execution_succeeded = True |
|
2680 | self.last_execution_succeeded = True | |
@@ -2663,9 +2693,9 b' class InteractiveShell(SingletonConfigurable):' | |||||
2663 | self.last_execution_result = result |
|
2693 | self.last_execution_result = result | |
2664 | return result |
|
2694 | return result | |
2665 |
|
2695 | |||
2666 | self.events.trigger('pre_execute') |
|
2696 | self.events.trigger('pre_execute', request) | |
2667 | if not silent: |
|
2697 | if not silent: | |
2668 | self.events.trigger('pre_run_cell') |
|
2698 | self.events.trigger('pre_run_cell', request) | |
2669 |
|
2699 | |||
2670 | # If any of our input transformation (input_transformer_manager or |
|
2700 | # If any of our input transformation (input_transformer_manager or | |
2671 | # prefilter_manager) raises an exception, we store it in this variable |
|
2701 | # prefilter_manager) raises an exception, we store it in this variable |
@@ -1,17 +1,23 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 | ||||
7 | @events._define_event |
|
9 | @events._define_event | |
|
10 | @callback_prototype | |||
8 | def ping_received(): |
|
11 | def ping_received(): | |
9 | pass |
|
12 | pass | |
10 |
|
13 | |||
|
14 | ||||
11 | @events._define_event |
|
15 | @events._define_event | |
|
16 | @callback_prototype | |||
12 | def event_with_argument(argument): |
|
17 | def event_with_argument(argument): | |
13 | pass |
|
18 | pass | |
14 |
|
19 | |||
|
20 | ||||
15 | class CallbackTests(unittest.TestCase): |
|
21 | class CallbackTests(unittest.TestCase): | |
16 | def setUp(self): |
|
22 | def setUp(self): | |
17 | self.em = events.EventManager(get_ipython(), {'ping_received': ping_received, 'event_with_argument': event_with_argument}) |
|
23 | self.em = events.EventManager(get_ipython(), {'ping_received': ping_received, 'event_with_argument': event_with_argument}) |
@@ -276,6 +276,9 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) | |||
279 | # double-check that non-silent exec did what we expected |
|
282 | # double-check that non-silent exec did what we expected | |
280 | # silent to avoid |
|
283 | # silent to avoid | |
281 | ip.run_cell("1") |
|
284 | ip.run_cell("1") | |
@@ -288,6 +291,9 b' class InteractiveShellTestCase(unittest.TestCase):' | |||||
288 | assert pre_explicit.called |
|
291 | assert pre_explicit.called | |
289 | assert post_always.called |
|
292 | assert post_always.called | |
290 | assert post_explicit.called |
|
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 | finally: |
|
297 | finally: | |
292 | # remove post-exec |
|
298 | # remove post-exec | |
293 | ip.events.unregister('pre_run_cell', pre_explicit) |
|
299 | ip.events.unregister('pre_run_cell', pre_explicit) |
@@ -18,10 +18,12 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, request): | |
|
22 | print('Cell code: "%s"' % request.raw_cell) | |||
22 | self.last_x = self.shell.user_ns.get('x', None) |
|
23 | self.last_x = self.shell.user_ns.get('x', None) | |
23 |
|
24 | |||
24 | def post_execute(self, result): |
|
25 | def post_execute(self, result): | |
|
26 | print('Cell code: "%s"' % result.request.raw_cell) | |||
25 | if result.error_before_exec: |
|
27 | if result.error_before_exec: | |
26 | print('Error before execution: %s' % result.error_before_exec) |
|
28 | print('Error before execution: %s' % result.error_before_exec) | |
27 | if self.shell.user_ns.get('x', None) != self.last_x: |
|
29 | if self.shell.user_ns.get('x', None) != self.last_x: | |
@@ -55,8 +57,7 b' pre_run_cell' | |||||
55 |
|
57 | |||
56 | ``pre_run_cell`` fires prior to interactive execution (e.g. a cell in a notebook). |
|
58 | ``pre_run_cell`` fires prior to interactive execution (e.g. a cell in a notebook). | |
57 | It can be used to note the state prior to execution, and keep track of changes. |
|
59 | It can be used to note the state prior to execution, and keep track of changes. | |
58 |
The object |
|
60 | The object representing the code execution request is provided as an argument. | |
59 | argument, even though the actual result is not yet available. |
|
|||
60 |
|
61 | |||
61 | pre_execute |
|
62 | pre_execute | |
62 | ----------- |
|
63 | ----------- | |
@@ -64,8 +65,7 b' pre_execute' | |||||
64 | ``pre_execute`` is like ``pre_run_cell``, but is triggered prior to *any* execution. |
|
65 | ``pre_execute`` is like ``pre_run_cell``, but is triggered prior to *any* execution. | |
65 | Sometimes code can be executed by libraries, etc. which |
|
66 | Sometimes code can be executed by libraries, etc. which | |
66 | skipping the history/display mechanisms, in which cases ``pre_run_cell`` will not fire. |
|
67 | skipping the history/display mechanisms, in which cases ``pre_run_cell`` will not fire. | |
67 |
The object |
|
68 | The object representing the code execution request is provided as an argument. | |
68 | argument, even though the actual result is not yet available. |
|
|||
69 |
|
69 | |||
70 | post_run_cell |
|
70 | post_run_cell | |
71 | ------------- |
|
71 | ------------- |
@@ -1,6 +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 result object |
|
3 | Additionally, the execution request and result objects are now made available in | |
4 |
|
|
4 | the corresponding *pre* or *post* event callbacks in a backward compatible | |
|
5 | manner. | |||
5 |
|
6 | |||
6 | * `Related GitHub issue <https://github.com/ipython/ipython/issues/10774>`__ |
|
7 | * `Related GitHub issue <https://github.com/ipython/ipython/issues/10774>`__ |
@@ -192,6 +192,7 b' install_requires = [' | |||||
192 | 'traitlets>=4.2', |
|
192 | 'traitlets>=4.2', | |
193 | 'prompt_toolkit>=1.0.4,<2.0.0', |
|
193 | 'prompt_toolkit>=1.0.4,<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