##// END OF EJS Templates
Merge pull request #10795 from fniephaus/finally-hooks...
Thomas Kluyver -
r23998:0dbfdb9c merge
parent child Browse files
Show More
@@ -0,0 +1,7 b''
1 The *post* event callbacks are now always called, even when the execution failed
2 (for example because of a ``SyntaxError``).
3 Additionally, the execution info and result objects are now made available in
4 the corresponding *pre* or *post* ``*_run_cell`` event callbacks in a backward
5 compatible manner.
6
7 * `Related GitHub issue <https://github.com/ipython/ipython/issues/10774>`__
@@ -13,6 +13,9 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 backcall import callback_prototype
17
18
16 class EventManager(object):
19 class EventManager(object):
17 """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.
18
21
@@ -37,7 +40,7 b' class EventManager(object):'
37 self.callbacks = {n:[] for n in available_events}
40 self.callbacks = {n:[] for n in available_events}
38
41
39 def register(self, event, function):
42 def register(self, event, function):
40 """Register a new event callback
43 """Register a new event callback.
41
44
42 Parameters
45 Parameters
43 ----------
46 ----------
@@ -56,12 +59,21 b' class EventManager(object):'
56 """
59 """
57 if not callable(function):
60 if not callable(function):
58 raise TypeError('Need a callable, got %r' % function)
61 raise TypeError('Need a callable, got %r' % function)
59 self.callbacks[event].append(function)
62 callback_proto = available_events.get(event)
63 self.callbacks[event].append(callback_proto.adapt(function))
60
64
61 def unregister(self, event, function):
65 def unregister(self, event, function):
62 """Remove a callback from the given event."""
66 """Remove a callback from the given event."""
63 self.callbacks[event].remove(function)
67 if function in self.callbacks[event]:
64
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
65 def trigger(self, event, *args, **kwargs):
77 def trigger(self, event, *args, **kwargs):
66 """Call callbacks for ``event``.
78 """Call callbacks for ``event``.
67
79
@@ -78,8 +90,9 b' class EventManager(object):'
78 # event_name -> prototype mapping
90 # event_name -> prototype mapping
79 available_events = {}
91 available_events = {}
80
92
81 def _define_event(callback_proto):
93 def _define_event(callback_function):
82 available_events[callback_proto.__name__] = callback_proto
94 callback_proto = callback_prototype(callback_function)
95 available_events[callback_function.__name__] = callback_proto
83 return callback_proto
96 return callback_proto
84
97
85 # ------------------------------------------------------------------------------
98 # ------------------------------------------------------------------------------
@@ -94,12 +107,19 b' def pre_execute():'
94 """Fires before code is executed in response to user/frontend action.
107 """Fires before code is executed in response to user/frontend action.
95
108
96 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
97 code cells."""
110 code cells.
111 """
98 pass
112 pass
99
113
100 @_define_event
114 @_define_event
101 def pre_run_cell():
115 def pre_run_cell(info):
102 """Fires before user-entered code runs."""
116 """Fires before user-entered code runs.
117
118 Parameters
119 ----------
120 info : :class:`~IPython.core.interactiveshell.ExecutionInfo`
121 An object containing information used for the code execution.
122 """
103 pass
123 pass
104
124
105 @_define_event
125 @_define_event
@@ -107,12 +127,19 b' def post_execute():'
107 """Fires after code is executed in response to user/frontend action.
127 """Fires after code is executed in response to user/frontend action.
108
128
109 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
110 code cells."""
130 code cells.
131 """
111 pass
132 pass
112
133
113 @_define_event
134 @_define_event
114 def post_run_cell():
135 def post_run_cell(result):
115 """Fires after user-entered code runs."""
136 """Fires after user-entered code runs.
137
138 Parameters
139 ----------
140 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
141 The object which will be returned as the execution result.
142 """
116 pass
143 pass
117
144
118 @_define_event
145 @_define_event
@@ -173,6 +173,30 b' class DummyMod(object):'
173 pass
173 pass
174
174
175
175
176 class ExecutionInfo(object):
177 """The arguments used for 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 info = None
184 result = None
209 result = None
185
210
211 def __init__(self, info):
212 self.info = info
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 info=%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.info), repr(self.result))
201
229
202
230
203 class InteractiveShell(SingletonConfigurable):
231 class InteractiveShell(SingletonConfigurable):
@@ -1879,7 +1907,7 b' class InteractiveShell(SingletonConfigurable):'
1879 # This is overridden in TerminalInteractiveShell to show a message about
1907 # This is overridden in TerminalInteractiveShell to show a message about
1880 # the %paste magic.
1908 # the %paste magic.
1881 def showindentationerror(self):
1909 def showindentationerror(self):
1882 """Called by run_cell when there's an IndentationError in code entered
1910 """Called by _run_cell when there's an IndentationError in code entered
1883 at the prompt.
1911 at the prompt.
1884
1912
1885 This is overridden in TerminalInteractiveShell to show a message about
1913 This is overridden in TerminalInteractiveShell to show a message about
@@ -2621,13 +2649,38 b' class InteractiveShell(SingletonConfigurable):'
2621 -------
2649 -------
2622 result : :class:`ExecutionResult`
2650 result : :class:`ExecutionResult`
2623 """
2651 """
2624 result = ExecutionResult()
2652 try:
2653 result = self._run_cell(
2654 raw_cell, store_history, silent, shell_futures)
2655 finally:
2656 self.events.trigger('post_execute')
2657 if not silent:
2658 self.events.trigger('post_run_cell', result)
2659 return result
2660
2661 def _run_cell(self, raw_cell, store_history, silent, shell_futures):
2662 """Internal method to run a complete IPython cell.
2663
2664 Parameters
2665 ----------
2666 raw_cell : str
2667 store_history : bool
2668 silent : bool
2669 shell_futures : bool
2670
2671 Returns
2672 -------
2673 result : :class:`ExecutionResult`
2674 """
2675 info = ExecutionInfo(
2676 raw_cell, store_history, silent, shell_futures)
2677 result = ExecutionResult(info)
2625
2678
2626 if (not raw_cell) or raw_cell.isspace():
2679 if (not raw_cell) or raw_cell.isspace():
2627 self.last_execution_succeeded = True
2680 self.last_execution_succeeded = True
2628 self.last_execution_result = result
2681 self.last_execution_result = result
2629 return result
2682 return result
2630
2683
2631 if silent:
2684 if silent:
2632 store_history = False
2685 store_history = False
2633
2686
@@ -2642,7 +2695,7 b' class InteractiveShell(SingletonConfigurable):'
2642
2695
2643 self.events.trigger('pre_execute')
2696 self.events.trigger('pre_execute')
2644 if not silent:
2697 if not silent:
2645 self.events.trigger('pre_run_cell')
2698 self.events.trigger('pre_run_cell', info)
2646
2699
2647 # If any of our input transformation (input_transformer_manager or
2700 # If any of our input transformation (input_transformer_manager or
2648 # prefilter_manager) raises an exception, we store it in this variable
2701 # prefilter_manager) raises an exception, we store it in this variable
@@ -2723,7 +2776,7 b' class InteractiveShell(SingletonConfigurable):'
2723 self.displayhook.exec_result = result
2776 self.displayhook.exec_result = result
2724
2777
2725 # Execute the user code
2778 # Execute the user code
2726 interactivity = "none" if silent else self.ast_node_interactivity
2779 interactivity = 'none' if silent else self.ast_node_interactivity
2727 has_raised = self.run_ast_nodes(code_ast.body, cell_name,
2780 has_raised = self.run_ast_nodes(code_ast.body, cell_name,
2728 interactivity=interactivity, compiler=compiler, result=result)
2781 interactivity=interactivity, compiler=compiler, result=result)
2729
2782
@@ -2734,10 +2787,6 b' class InteractiveShell(SingletonConfigurable):'
2734 # ExecutionResult
2787 # ExecutionResult
2735 self.displayhook.exec_result = None
2788 self.displayhook.exec_result = None
2736
2789
2737 self.events.trigger('post_execute')
2738 if not silent:
2739 self.events.trigger('post_run_cell')
2740
2741 if store_history:
2790 if store_history:
2742 # Write output to the database. Does nothing unless
2791 # Write output to the database. Does nothing unless
2743 # history output logging is enabled.
2792 # history output logging is enabled.
@@ -1,15 +1,24 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
9 @events._define_event
7 def ping_received():
10 def ping_received():
8 pass
11 pass
9
12
13
14 @events._define_event
15 def event_with_argument(argument):
16 pass
17
18
10 class CallbackTests(unittest.TestCase):
19 class CallbackTests(unittest.TestCase):
11 def setUp(self):
20 def setUp(self):
12 self.em = events.EventManager(get_ipython(), {'ping_received': ping_received})
21 self.em = events.EventManager(get_ipython(), {'ping_received': ping_received, 'event_with_argument': event_with_argument})
13
22
14 def test_register_unregister(self):
23 def test_register_unregister(self):
15 cb = Mock()
24 cb = Mock()
@@ -49,3 +58,16 b' class CallbackTests(unittest.TestCase):'
49 self.em.trigger('ping_received')
58 self.em.trigger('ping_received')
50 self.assertEqual([True, True, False], invoked)
59 self.assertEqual([True, True, False], invoked)
51 self.assertEqual([func3], self.em.callbacks['ping_received'])
60 self.assertEqual([func3], self.em.callbacks['ping_received'])
61
62 def test_ignore_event_arguments_if_no_argument_required(self):
63 call_count = [0]
64 def event_with_no_argument():
65 call_count[0] += 1
66
67 self.em.register('event_with_argument', event_with_no_argument)
68 self.em.trigger('event_with_argument', 'the argument')
69 self.assertEqual(call_count[0], 1)
70
71 self.em.unregister('event_with_argument', event_with_no_argument)
72 self.em.trigger('ping_received')
73 self.assertEqual(call_count[0], 1)
@@ -263,6 +263,7 b' class InteractiveShellTestCase(unittest.TestCase):'
263 pre_always = mock.Mock()
263 pre_always = mock.Mock()
264 post_explicit = mock.Mock()
264 post_explicit = mock.Mock()
265 post_always = mock.Mock()
265 post_always = mock.Mock()
266 all_mocks = [pre_explicit, pre_always, post_explicit, post_always]
266
267
267 ip.events.register('pre_run_cell', pre_explicit)
268 ip.events.register('pre_run_cell', pre_explicit)
268 ip.events.register('pre_execute', pre_always)
269 ip.events.register('pre_execute', pre_always)
@@ -280,6 +281,19 b' class InteractiveShellTestCase(unittest.TestCase):'
280 ip.run_cell("1")
281 ip.run_cell("1")
281 assert pre_explicit.called
282 assert pre_explicit.called
282 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
288 [m.reset_mock() for m in all_mocks]
289 ip.run_cell("syntax error")
290 assert pre_always.called
291 assert pre_explicit.called
292 assert post_always.called
293 assert post_explicit.called
294 info, = pre_explicit.call_args[0]
295 result, = post_explicit.call_args[0]
296 self.assertEqual(info, result.info)
283 finally:
297 finally:
284 # remove post-exec
298 # remove post-exec
285 ip.events.unregister('pre_run_cell', pre_explicit)
299 ip.events.unregister('pre_run_cell', pre_explicit)
@@ -21,14 +21,24 b' For example::'
21 def pre_execute(self):
21 def pre_execute(self):
22 self.last_x = self.shell.user_ns.get('x', None)
22 self.last_x = self.shell.user_ns.get('x', None)
23
23
24 def pre_run_cell(self, info):
25 print('Cell code: "%s"' % info.raw_cell)
26
24 def post_execute(self):
27 def post_execute(self):
25 if self.shell.user_ns.get('x', None) != self.last_x:
28 if self.shell.user_ns.get('x', None) != self.last_x:
26 print("x changed!")
29 print("x changed!")
27
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
28 def load_ipython_extension(ip):
36 def load_ipython_extension(ip):
29 vw = VarWatcher(ip)
37 vw = VarWatcher(ip)
30 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)
31 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)
32
42
33
43
34 Events
44 Events
@@ -53,6 +63,7 b' pre_run_cell'
53
63
54 ``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).
55 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.
66 An object containing information used for the code execution is provided as an argument.
56
67
57 pre_execute
68 pre_execute
58 -----------
69 -----------
@@ -67,7 +78,8 b' post_run_cell'
67 ``post_run_cell`` runs after interactive execution (e.g. a cell in a notebook).
78 ``post_run_cell`` runs after interactive execution (e.g. a cell in a notebook).
68 It can be used to cleanup or notify or perform operations on any side effects produced during execution.
79 It can be used to cleanup or notify or perform operations on any side effects produced during execution.
69 For instance, the inline matplotlib backend uses this event to display any figures created but not explicitly displayed during the course of the cell.
80 For instance, the inline matplotlib backend uses this event to display any figures created but not explicitly displayed during the course of the cell.
70
81 The object which will be returned as the execution result is provided as an
82 argument.
71
83
72 post_execute
84 post_execute
73 ------------
85 ------------
@@ -6,11 +6,12 b' Execution semantics in the IPython kernel'
6 The execution of user code consists of the following phases:
6 The execution of user code consists of the following phases:
7
7
8 1. Fire the ``pre_execute`` event.
8 1. Fire the ``pre_execute`` event.
9 2. Fire the ``pre_run_cell`` event unless silent is True.
9 2. Fire the ``pre_run_cell`` event unless silent is ``True``.
10 3. Execute the ``code`` field, see below for details.
10 3. Execute the ``code`` field, see below for details.
11 4. If execution succeeds, expressions in ``user_expressions`` are computed.
11 4. If execution succeeds, expressions in ``user_expressions`` are computed.
12 This ensures that any error in the expressions don't affect the main code execution.
12 This ensures that any error in the expressions don't affect the main code execution.
13 5. Fire the post_execute event.
13 5. Fire the ``post_execute`` event.
14 6. Fire the ``post_run_cell`` event unless silent is ``True``.
14
15
15 .. seealso::
16 .. seealso::
16
17
@@ -192,6 +192,7 b' install_requires = ['
192 'traitlets>=4.2',
192 'traitlets>=4.2',
193 'prompt_toolkit>=1.0.15,<2.0.0',
193 'prompt_toolkit>=1.0.15,<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