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