##// END OF EJS Templates
Make event triggering robust to (un)registration....
Craig Citro -
Show More
@@ -0,0 +1,7 b''
1 Update IPython event triggering to ensure callback registration and
2 unregistration only affects the set of callbacks the *next* time that event is
3 triggered. See :ghissue:`9447` and :ghpull:`9453`.
4
5 This is a change to the existing semantics, wherein one callback registering a
6 second callback when triggered for an event would previously be invoked for
7 that same event.
@@ -1,131 +1,131 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 from __future__ import print_function
15 from __future__ import print_function
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 def __init__(self, shell, available_events):
27 def __init__(self, shell, available_events):
28 """Initialise the :class:`CallbackManager`.
28 """Initialise the :class:`CallbackManager`.
29
29
30 Parameters
30 Parameters
31 ----------
31 ----------
32 shell
32 shell
33 The :class:`~IPython.core.interactiveshell.InteractiveShell` instance
33 The :class:`~IPython.core.interactiveshell.InteractiveShell` instance
34 available_callbacks
34 available_callbacks
35 An iterable of names for callback events.
35 An iterable of names for callback events.
36 """
36 """
37 self.shell = shell
37 self.shell = shell
38 self.callbacks = {n:[] for n in available_events}
38 self.callbacks = {n:[] for n in available_events}
39
39
40 def register(self, event, function):
40 def register(self, event, function):
41 """Register a new event callback
41 """Register a new event callback
42
42
43 Parameters
43 Parameters
44 ----------
44 ----------
45 event : str
45 event : str
46 The event for which to register this callback.
46 The event for which to register this callback.
47 function : callable
47 function : callable
48 A function to be called on the given event. It should take the same
48 A function to be called on the given event. It should take the same
49 parameters as the appropriate callback prototype.
49 parameters as the appropriate callback prototype.
50
50
51 Raises
51 Raises
52 ------
52 ------
53 TypeError
53 TypeError
54 If ``function`` is not callable.
54 If ``function`` is not callable.
55 KeyError
55 KeyError
56 If ``event`` is not one of the known events.
56 If ``event`` is not one of the known events.
57 """
57 """
58 if not callable(function):
58 if not callable(function):
59 raise TypeError('Need a callable, got %r' % function)
59 raise TypeError('Need a callable, got %r' % function)
60 self.callbacks[event].append(function)
60 self.callbacks[event].append(function)
61
61
62 def unregister(self, event, function):
62 def unregister(self, event, function):
63 """Remove a callback from the given event."""
63 """Remove a callback from the given event."""
64 self.callbacks[event].remove(function)
64 self.callbacks[event].remove(function)
65
65
66 def trigger(self, event, *args, **kwargs):
66 def trigger(self, event, *args, **kwargs):
67 """Call callbacks for ``event``.
67 """Call callbacks for ``event``.
68
68
69 Any additional arguments are passed to all callbacks registered for this
69 Any additional arguments are passed to all callbacks registered for this
70 event. Exceptions raised by callbacks are caught, and a message printed.
70 event. Exceptions raised by callbacks are caught, and a message printed.
71 """
71 """
72 for func in self.callbacks[event]:
72 for func in self.callbacks[event][:]:
73 try:
73 try:
74 func(*args, **kwargs)
74 func(*args, **kwargs)
75 except Exception:
75 except Exception:
76 print("Error in callback {} (for {}):".format(func, event))
76 print("Error in callback {} (for {}):".format(func, event))
77 self.shell.showtraceback()
77 self.shell.showtraceback()
78
78
79 # event_name -> prototype mapping
79 # event_name -> prototype mapping
80 available_events = {}
80 available_events = {}
81
81
82 def _define_event(callback_proto):
82 def _define_event(callback_proto):
83 available_events[callback_proto.__name__] = callback_proto
83 available_events[callback_proto.__name__] = callback_proto
84 return callback_proto
84 return callback_proto
85
85
86 # ------------------------------------------------------------------------------
86 # ------------------------------------------------------------------------------
87 # Callback prototypes
87 # Callback prototypes
88 #
88 #
89 # No-op functions which describe the names of available events and the
89 # No-op functions which describe the names of available events and the
90 # signatures of callbacks for those events.
90 # signatures of callbacks for those events.
91 # ------------------------------------------------------------------------------
91 # ------------------------------------------------------------------------------
92
92
93 @_define_event
93 @_define_event
94 def pre_execute():
94 def pre_execute():
95 """Fires before code is executed in response to user/frontend action.
95 """Fires before code is executed in response to user/frontend action.
96
96
97 This includes comm and widget messages and silent execution, as well as user
97 This includes comm and widget messages and silent execution, as well as user
98 code cells."""
98 code cells."""
99 pass
99 pass
100
100
101 @_define_event
101 @_define_event
102 def pre_run_cell():
102 def pre_run_cell():
103 """Fires before user-entered code runs."""
103 """Fires before user-entered code runs."""
104 pass
104 pass
105
105
106 @_define_event
106 @_define_event
107 def post_execute():
107 def post_execute():
108 """Fires after code is executed in response to user/frontend action.
108 """Fires after code is executed in response to user/frontend action.
109
109
110 This includes comm and widget messages and silent execution, as well as user
110 This includes comm and widget messages and silent execution, as well as user
111 code cells."""
111 code cells."""
112 pass
112 pass
113
113
114 @_define_event
114 @_define_event
115 def post_run_cell():
115 def post_run_cell():
116 """Fires after user-entered code runs."""
116 """Fires after user-entered code runs."""
117 pass
117 pass
118
118
119 @_define_event
119 @_define_event
120 def shell_initialized(ip):
120 def shell_initialized(ip):
121 """Fires after initialisation of :class:`~IPython.core.interactiveshell.InteractiveShell`.
121 """Fires after initialisation of :class:`~IPython.core.interactiveshell.InteractiveShell`.
122
122
123 This is before extensions and startup scripts are loaded, so it can only be
123 This is before extensions and startup scripts are loaded, so it can only be
124 set by subclassing.
124 set by subclassing.
125
125
126 Parameters
126 Parameters
127 ----------
127 ----------
128 ip : :class:`~IPython.core.interactiveshell.InteractiveShell`
128 ip : :class:`~IPython.core.interactiveshell.InteractiveShell`
129 The newly initialised shell.
129 The newly initialised shell.
130 """
130 """
131 pass
131 pass
@@ -1,32 +1,54 b''
1 import unittest
1 import unittest
2 try: # Python 3.3 +
2 try: # Python 3.3 +
3 from unittest.mock import Mock
3 from unittest.mock import Mock
4 except ImportError:
4 except ImportError:
5 from mock import Mock
5 from mock import Mock
6
6
7 from IPython.core import events
7 from IPython.core import events
8 import IPython.testing.tools as tt
8 import IPython.testing.tools as tt
9
9
10 def ping_received():
10 def ping_received():
11 pass
11 pass
12
12
13 class CallbackTests(unittest.TestCase):
13 class CallbackTests(unittest.TestCase):
14 def setUp(self):
14 def setUp(self):
15 self.em = events.EventManager(get_ipython(), {'ping_received': ping_received})
15 self.em = events.EventManager(get_ipython(), {'ping_received': ping_received})
16
16
17 def test_register_unregister(self):
17 def test_register_unregister(self):
18 cb = Mock()
18 cb = Mock()
19
19
20 self.em.register('ping_received', cb)
20 self.em.register('ping_received', cb)
21 self.em.trigger('ping_received')
21 self.em.trigger('ping_received')
22 self.assertEqual(cb.call_count, 1)
22 self.assertEqual(cb.call_count, 1)
23
23
24 self.em.unregister('ping_received', cb)
24 self.em.unregister('ping_received', cb)
25 self.em.trigger('ping_received')
25 self.em.trigger('ping_received')
26 self.assertEqual(cb.call_count, 1)
26 self.assertEqual(cb.call_count, 1)
27
27
28 def test_cb_error(self):
28 def test_cb_error(self):
29 cb = Mock(side_effect=ValueError)
29 cb = Mock(side_effect=ValueError)
30 self.em.register('ping_received', cb)
30 self.em.register('ping_received', cb)
31 with tt.AssertPrints("Error in callback"):
31 with tt.AssertPrints("Error in callback"):
32 self.em.trigger('ping_received')
32 self.em.trigger('ping_received')
33
34 def test_unregister_during_callback(self):
35 invoked = [False] * 3
36
37 def func1(*_):
38 invoked[0] = True
39 self.em.unregister('ping_received', func1)
40 self.em.register('ping_received', func3)
41
42 def func2(*_):
43 invoked[1] = True
44 self.em.unregister('ping_received', func2)
45
46 def func3(*_):
47 invoked[2] = True
48
49 self.em.register('ping_received', func1)
50 self.em.register('ping_received', func2)
51
52 self.em.trigger('ping_received')
53 self.assertEqual([True, True, False], invoked)
54 self.assertEqual([func3], self.em.callbacks['ping_received'])
General Comments 0
You need to be logged in to leave comments. Login now