##// END OF EJS Templates
Use `backcall` and introduce `ExecutionRequest`
Fabio Niephaus -
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 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
137 The object which will be returned as the execution result.
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 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
148 The object which will be returned as the execution result.
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,7 +2672,9 b' class InteractiveShell(SingletonConfigurable):'
2644 2672 -------
2645 2673 result : :class:`ExecutionResult`
2646 2674 """
2647 result = ExecutionResult()
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
@@ -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})
@@ -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 which will be returned as the execution result is provided as an
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 which will be returned as the execution result is provided as an
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 is now made available in both *pre*
4 and *post* event callbacks in a backward compatible manner.
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>`__
@@ -192,6 +192,7 b' install_requires = ['
192 192 'traitlets>=4.2',
193 193 'prompt_toolkit>=1.0.4,<2.0.0',
194 194 'pygments',
195 'backcall',
195 196 ]
196 197
197 198 # Platform-specific dependencies:
General Comments 0
You need to be logged in to leave comments. Login now