##// END OF EJS Templates
address #11630 by catching KeyboardInterrupt from event handlers
jsnydes -
Show More
@@ -1,160 +1,160
1 """Infrastructure for registering and firing callbacks on application events.
1 """Infrastructure for registering and firing callbacks on application events.
2
2
3 Unlike :mod:`IPython.core.hooks`, which lets end users set single functions to
3 Unlike :mod:`IPython.core.hooks`, which lets end users set single functions to
4 be called at specific times, or a collection of alternative methods to try,
4 be called at specific times, or a collection of alternative methods to try,
5 callbacks are designed to be used by extension authors. A number of callbacks
5 callbacks are designed to be used by extension authors. A number of callbacks
6 can be registered for the same event without needing to be aware of one another.
6 can be registered for the same event without needing to be aware of one another.
7
7
8 The functions defined in this module are no-ops indicating the names of available
8 The functions defined in this module are no-ops indicating the names of available
9 events and the arguments which will be passed to them.
9 events and the arguments which will be passed to them.
10
10
11 .. note::
11 .. note::
12
12
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
16 from backcall import callback_prototype
17
17
18
18
19 class EventManager(object):
19 class EventManager(object):
20 """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.
21
21
22 This is attached to :class:`~IPython.core.interactiveshell.InteractiveShell`
22 This is attached to :class:`~IPython.core.interactiveshell.InteractiveShell`
23 instances as an ``events`` attribute.
23 instances as an ``events`` attribute.
24
24
25 .. note::
25 .. note::
26
26
27 This API is experimental in IPython 2.0, and may be revised in future versions.
27 This API is experimental in IPython 2.0, and may be revised in future versions.
28 """
28 """
29 def __init__(self, shell, available_events):
29 def __init__(self, shell, available_events):
30 """Initialise the :class:`CallbackManager`.
30 """Initialise the :class:`CallbackManager`.
31
31
32 Parameters
32 Parameters
33 ----------
33 ----------
34 shell
34 shell
35 The :class:`~IPython.core.interactiveshell.InteractiveShell` instance
35 The :class:`~IPython.core.interactiveshell.InteractiveShell` instance
36 available_callbacks
36 available_callbacks
37 An iterable of names for callback events.
37 An iterable of names for callback events.
38 """
38 """
39 self.shell = shell
39 self.shell = shell
40 self.callbacks = {n:[] for n in available_events}
40 self.callbacks = {n:[] for n in available_events}
41
41
42 def register(self, event, function):
42 def register(self, event, function):
43 """Register a new event callback.
43 """Register a new event callback.
44
44
45 Parameters
45 Parameters
46 ----------
46 ----------
47 event : str
47 event : str
48 The event for which to register this callback.
48 The event for which to register this callback.
49 function : callable
49 function : callable
50 A function to be called on the given event. It should take the same
50 A function to be called on the given event. It should take the same
51 parameters as the appropriate callback prototype.
51 parameters as the appropriate callback prototype.
52
52
53 Raises
53 Raises
54 ------
54 ------
55 TypeError
55 TypeError
56 If ``function`` is not callable.
56 If ``function`` is not callable.
57 KeyError
57 KeyError
58 If ``event`` is not one of the known events.
58 If ``event`` is not one of the known events.
59 """
59 """
60 if not callable(function):
60 if not callable(function):
61 raise TypeError('Need a callable, got %r' % function)
61 raise TypeError('Need a callable, got %r' % function)
62 callback_proto = available_events.get(event)
62 callback_proto = available_events.get(event)
63 self.callbacks[event].append(callback_proto.adapt(function))
63 self.callbacks[event].append(callback_proto.adapt(function))
64
64
65 def unregister(self, event, function):
65 def unregister(self, event, function):
66 """Remove a callback from the given event."""
66 """Remove a callback from the given event."""
67 if function in self.callbacks[event]:
67 if function in self.callbacks[event]:
68 return self.callbacks[event].remove(function)
68 return self.callbacks[event].remove(function)
69
69
70 # Remove callback in case ``function`` was adapted by `backcall`.
70 # Remove callback in case ``function`` was adapted by `backcall`.
71 for callback in self.callbacks[event]:
71 for callback in self.callbacks[event]:
72 try:
72 try:
73 if callback.__wrapped__ is function:
73 if callback.__wrapped__ is function:
74 return self.callbacks[event].remove(callback)
74 return self.callbacks[event].remove(callback)
75 except AttributeError:
75 except AttributeError:
76 pass
76 pass
77
77
78 raise ValueError('Function {!r} is not registered as a {} callback'.format(function, event))
78 raise ValueError('Function {!r} is not registered as a {} callback'.format(function, event))
79
79
80 def trigger(self, event, *args, **kwargs):
80 def trigger(self, event, *args, **kwargs):
81 """Call callbacks for ``event``.
81 """Call callbacks for ``event``.
82
82
83 Any additional arguments are passed to all callbacks registered for this
83 Any additional arguments are passed to all callbacks registered for this
84 event. Exceptions raised by callbacks are caught, and a message printed.
84 event. Exceptions raised by callbacks are caught, and a message printed.
85 """
85 """
86 for func in self.callbacks[event][:]:
86 for func in self.callbacks[event][:]:
87 try:
87 try:
88 func(*args, **kwargs)
88 func(*args, **kwargs)
89 except Exception:
89 except (Exception, KeyboardInterrupt):
90 print("Error in callback {} (for {}):".format(func, event))
90 print("Error in callback {} (for {}):".format(func, event))
91 self.shell.showtraceback()
91 self.shell.showtraceback()
92
92
93 # event_name -> prototype mapping
93 # event_name -> prototype mapping
94 available_events = {}
94 available_events = {}
95
95
96 def _define_event(callback_function):
96 def _define_event(callback_function):
97 callback_proto = callback_prototype(callback_function)
97 callback_proto = callback_prototype(callback_function)
98 available_events[callback_function.__name__] = callback_proto
98 available_events[callback_function.__name__] = callback_proto
99 return callback_proto
99 return callback_proto
100
100
101 # ------------------------------------------------------------------------------
101 # ------------------------------------------------------------------------------
102 # Callback prototypes
102 # Callback prototypes
103 #
103 #
104 # No-op functions which describe the names of available events and the
104 # No-op functions which describe the names of available events and the
105 # signatures of callbacks for those events.
105 # signatures of callbacks for those events.
106 # ------------------------------------------------------------------------------
106 # ------------------------------------------------------------------------------
107
107
108 @_define_event
108 @_define_event
109 def pre_execute():
109 def pre_execute():
110 """Fires before code is executed in response to user/frontend action.
110 """Fires before code is executed in response to user/frontend action.
111
111
112 This includes comm and widget messages and silent execution, as well as user
112 This includes comm and widget messages and silent execution, as well as user
113 code cells.
113 code cells.
114 """
114 """
115 pass
115 pass
116
116
117 @_define_event
117 @_define_event
118 def pre_run_cell(info):
118 def pre_run_cell(info):
119 """Fires before user-entered code runs.
119 """Fires before user-entered code runs.
120
120
121 Parameters
121 Parameters
122 ----------
122 ----------
123 info : :class:`~IPython.core.interactiveshell.ExecutionInfo`
123 info : :class:`~IPython.core.interactiveshell.ExecutionInfo`
124 An object containing information used for the code execution.
124 An object containing information used for the code execution.
125 """
125 """
126 pass
126 pass
127
127
128 @_define_event
128 @_define_event
129 def post_execute():
129 def post_execute():
130 """Fires after code is executed in response to user/frontend action.
130 """Fires after code is executed in response to user/frontend action.
131
131
132 This includes comm and widget messages and silent execution, as well as user
132 This includes comm and widget messages and silent execution, as well as user
133 code cells.
133 code cells.
134 """
134 """
135 pass
135 pass
136
136
137 @_define_event
137 @_define_event
138 def post_run_cell(result):
138 def post_run_cell(result):
139 """Fires after user-entered code runs.
139 """Fires after user-entered code runs.
140
140
141 Parameters
141 Parameters
142 ----------
142 ----------
143 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
143 result : :class:`~IPython.core.interactiveshell.ExecutionResult`
144 The object which will be returned as the execution result.
144 The object which will be returned as the execution result.
145 """
145 """
146 pass
146 pass
147
147
148 @_define_event
148 @_define_event
149 def shell_initialized(ip):
149 def shell_initialized(ip):
150 """Fires after initialisation of :class:`~IPython.core.interactiveshell.InteractiveShell`.
150 """Fires after initialisation of :class:`~IPython.core.interactiveshell.InteractiveShell`.
151
151
152 This is before extensions and startup scripts are loaded, so it can only be
152 This is before extensions and startup scripts are loaded, so it can only be
153 set by subclassing.
153 set by subclassing.
154
154
155 Parameters
155 Parameters
156 ----------
156 ----------
157 ip : :class:`~IPython.core.interactiveshell.InteractiveShell`
157 ip : :class:`~IPython.core.interactiveshell.InteractiveShell`
158 The newly initialised shell.
158 The newly initialised shell.
159 """
159 """
160 pass
160 pass
@@ -1,87 +1,93
1 from backcall import callback_prototype
1 from backcall import callback_prototype
2 import unittest
2 import unittest
3 from unittest.mock import Mock
3 from unittest.mock import Mock
4 import nose.tools as nt
4 import nose.tools as nt
5
5
6 from IPython.core import events
6 from IPython.core import events
7 import IPython.testing.tools as tt
7 import IPython.testing.tools as tt
8
8
9
9
10 @events._define_event
10 @events._define_event
11 def ping_received():
11 def ping_received():
12 pass
12 pass
13
13
14
14
15 @events._define_event
15 @events._define_event
16 def event_with_argument(argument):
16 def event_with_argument(argument):
17 pass
17 pass
18
18
19
19
20 class CallbackTests(unittest.TestCase):
20 class CallbackTests(unittest.TestCase):
21 def setUp(self):
21 def setUp(self):
22 self.em = events.EventManager(get_ipython(),
22 self.em = events.EventManager(get_ipython(),
23 {'ping_received': ping_received,
23 {'ping_received': ping_received,
24 'event_with_argument': event_with_argument})
24 'event_with_argument': event_with_argument})
25
25
26 def test_register_unregister(self):
26 def test_register_unregister(self):
27 cb = Mock()
27 cb = Mock()
28
28
29 self.em.register('ping_received', cb)
29 self.em.register('ping_received', cb)
30 self.em.trigger('ping_received')
30 self.em.trigger('ping_received')
31 self.assertEqual(cb.call_count, 1)
31 self.assertEqual(cb.call_count, 1)
32
32
33 self.em.unregister('ping_received', cb)
33 self.em.unregister('ping_received', cb)
34 self.em.trigger('ping_received')
34 self.em.trigger('ping_received')
35 self.assertEqual(cb.call_count, 1)
35 self.assertEqual(cb.call_count, 1)
36
36
37 def test_bare_function_missed_unregister(self):
37 def test_bare_function_missed_unregister(self):
38 def cb1():
38 def cb1():
39 ...
39 ...
40
40
41 def cb2():
41 def cb2():
42 ...
42 ...
43
43
44 self.em.register('ping_received', cb1)
44 self.em.register('ping_received', cb1)
45 nt.assert_raises(ValueError, self.em.unregister, 'ping_received', cb2)
45 nt.assert_raises(ValueError, self.em.unregister, 'ping_received', cb2)
46 self.em.unregister('ping_received', cb1)
46 self.em.unregister('ping_received', cb1)
47
47
48 def test_cb_error(self):
48 def test_cb_error(self):
49 cb = Mock(side_effect=ValueError)
49 cb = Mock(side_effect=ValueError)
50 self.em.register('ping_received', cb)
50 self.em.register('ping_received', cb)
51 with tt.AssertPrints("Error in callback"):
51 with tt.AssertPrints("Error in callback"):
52 self.em.trigger('ping_received')
52 self.em.trigger('ping_received')
53
53
54 def test_cb_keyboard_interrupt(self):
55 cb = Mock(side_effect=KeyboardInterrupt)
56 self.em.register('ping_received', cb)
57 with tt.assertPrints("Error in callback"):
58 self.em.trigger('ping_received')
59
54 def test_unregister_during_callback(self):
60 def test_unregister_during_callback(self):
55 invoked = [False] * 3
61 invoked = [False] * 3
56
62
57 def func1(*_):
63 def func1(*_):
58 invoked[0] = True
64 invoked[0] = True
59 self.em.unregister('ping_received', func1)
65 self.em.unregister('ping_received', func1)
60 self.em.register('ping_received', func3)
66 self.em.register('ping_received', func3)
61
67
62 def func2(*_):
68 def func2(*_):
63 invoked[1] = True
69 invoked[1] = True
64 self.em.unregister('ping_received', func2)
70 self.em.unregister('ping_received', func2)
65
71
66 def func3(*_):
72 def func3(*_):
67 invoked[2] = True
73 invoked[2] = True
68
74
69 self.em.register('ping_received', func1)
75 self.em.register('ping_received', func1)
70 self.em.register('ping_received', func2)
76 self.em.register('ping_received', func2)
71
77
72 self.em.trigger('ping_received')
78 self.em.trigger('ping_received')
73 self.assertEqual([True, True, False], invoked)
79 self.assertEqual([True, True, False], invoked)
74 self.assertEqual([func3], self.em.callbacks['ping_received'])
80 self.assertEqual([func3], self.em.callbacks['ping_received'])
75
81
76 def test_ignore_event_arguments_if_no_argument_required(self):
82 def test_ignore_event_arguments_if_no_argument_required(self):
77 call_count = [0]
83 call_count = [0]
78 def event_with_no_argument():
84 def event_with_no_argument():
79 call_count[0] += 1
85 call_count[0] += 1
80
86
81 self.em.register('event_with_argument', event_with_no_argument)
87 self.em.register('event_with_argument', event_with_no_argument)
82 self.em.trigger('event_with_argument', 'the argument')
88 self.em.trigger('event_with_argument', 'the argument')
83 self.assertEqual(call_count[0], 1)
89 self.assertEqual(call_count[0], 1)
84
90
85 self.em.unregister('event_with_argument', event_with_no_argument)
91 self.em.unregister('event_with_argument', event_with_no_argument)
86 self.em.trigger('ping_received')
92 self.em.trigger('ping_received')
87 self.assertEqual(call_count[0], 1)
93 self.assertEqual(call_count[0], 1)
General Comments 0
You need to be logged in to leave comments. Login now