##// 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 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 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
125 request : :class:`~IPython.core.interactiveshell.ExecutionRequest`
137 The object which will be returned as the execution result.
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 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
137 request : :class:`~IPython.core.interactiveshell.ExecutionRequest`
148 The object which will be returned as the execution result.
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 result = ExecutionResult()
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 which will be returned as the execution result is provided as an
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 which will be returned as the execution result is provided as an
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 is now made available in both *pre*
3 Additionally, the execution request and result objects are now made available in
4 and *post* event callbacks in a backward compatible manner.
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