##// 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 1 """Infrastructure for registering and firing callbacks on application events.
2 2
3 3 Unlike :mod:`IPython.core.hooks`, which lets end users set single functions to
4 4 be called at specific times, or a collection of alternative methods to try,
5 5 callbacks are designed to be used by extension authors. A number of callbacks
6 6 can be registered for the same event without needing to be aware of one another.
7 7
8 8 The functions defined in this module are no-ops indicating the names of available
9 9 events and the arguments which will be passed to them.
10 10
11 11 .. note::
12 12
13 13 This API is experimental in IPython 2.0, and may be revised in future versions.
14 14 """
15 15 from __future__ import print_function
16 16
17 17 class EventManager(object):
18 18 """Manage a collection of events and a sequence of callbacks for each.
19 19
20 20 This is attached to :class:`~IPython.core.interactiveshell.InteractiveShell`
21 21 instances as an ``events`` attribute.
22 22
23 23 .. note::
24 24
25 25 This API is experimental in IPython 2.0, and may be revised in future versions.
26 26 """
27 27 def __init__(self, shell, available_events):
28 28 """Initialise the :class:`CallbackManager`.
29 29
30 30 Parameters
31 31 ----------
32 32 shell
33 33 The :class:`~IPython.core.interactiveshell.InteractiveShell` instance
34 34 available_callbacks
35 35 An iterable of names for callback events.
36 36 """
37 37 self.shell = shell
38 38 self.callbacks = {n:[] for n in available_events}
39 39
40 40 def register(self, event, function):
41 41 """Register a new event callback
42 42
43 43 Parameters
44 44 ----------
45 45 event : str
46 46 The event for which to register this callback.
47 47 function : callable
48 48 A function to be called on the given event. It should take the same
49 49 parameters as the appropriate callback prototype.
50 50
51 51 Raises
52 52 ------
53 53 TypeError
54 54 If ``function`` is not callable.
55 55 KeyError
56 56 If ``event`` is not one of the known events.
57 57 """
58 58 if not callable(function):
59 59 raise TypeError('Need a callable, got %r' % function)
60 60 self.callbacks[event].append(function)
61 61
62 62 def unregister(self, event, function):
63 63 """Remove a callback from the given event."""
64 64 self.callbacks[event].remove(function)
65 65
66 66 def trigger(self, event, *args, **kwargs):
67 67 """Call callbacks for ``event``.
68 68
69 69 Any additional arguments are passed to all callbacks registered for this
70 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 73 try:
74 74 func(*args, **kwargs)
75 75 except Exception:
76 76 print("Error in callback {} (for {}):".format(func, event))
77 77 self.shell.showtraceback()
78 78
79 79 # event_name -> prototype mapping
80 80 available_events = {}
81 81
82 82 def _define_event(callback_proto):
83 83 available_events[callback_proto.__name__] = callback_proto
84 84 return callback_proto
85 85
86 86 # ------------------------------------------------------------------------------
87 87 # Callback prototypes
88 88 #
89 89 # No-op functions which describe the names of available events and the
90 90 # signatures of callbacks for those events.
91 91 # ------------------------------------------------------------------------------
92 92
93 93 @_define_event
94 94 def pre_execute():
95 95 """Fires before code is executed in response to user/frontend action.
96 96
97 97 This includes comm and widget messages and silent execution, as well as user
98 98 code cells."""
99 99 pass
100 100
101 101 @_define_event
102 102 def pre_run_cell():
103 103 """Fires before user-entered code runs."""
104 104 pass
105 105
106 106 @_define_event
107 107 def post_execute():
108 108 """Fires after code is executed in response to user/frontend action.
109 109
110 110 This includes comm and widget messages and silent execution, as well as user
111 111 code cells."""
112 112 pass
113 113
114 114 @_define_event
115 115 def post_run_cell():
116 116 """Fires after user-entered code runs."""
117 117 pass
118 118
119 119 @_define_event
120 120 def shell_initialized(ip):
121 121 """Fires after initialisation of :class:`~IPython.core.interactiveshell.InteractiveShell`.
122 122
123 123 This is before extensions and startup scripts are loaded, so it can only be
124 124 set by subclassing.
125 125
126 126 Parameters
127 127 ----------
128 128 ip : :class:`~IPython.core.interactiveshell.InteractiveShell`
129 129 The newly initialised shell.
130 130 """
131 131 pass
@@ -1,32 +1,54 b''
1 1 import unittest
2 2 try: # Python 3.3 +
3 3 from unittest.mock import Mock
4 4 except ImportError:
5 5 from mock import Mock
6 6
7 7 from IPython.core import events
8 8 import IPython.testing.tools as tt
9 9
10 10 def ping_received():
11 11 pass
12 12
13 13 class CallbackTests(unittest.TestCase):
14 14 def setUp(self):
15 15 self.em = events.EventManager(get_ipython(), {'ping_received': ping_received})
16 16
17 17 def test_register_unregister(self):
18 18 cb = Mock()
19 19
20 20 self.em.register('ping_received', cb)
21 21 self.em.trigger('ping_received')
22 22 self.assertEqual(cb.call_count, 1)
23 23
24 24 self.em.unregister('ping_received', cb)
25 25 self.em.trigger('ping_received')
26 26 self.assertEqual(cb.call_count, 1)
27 27
28 28 def test_cb_error(self):
29 29 cb = Mock(side_effect=ValueError)
30 30 self.em.register('ping_received', cb)
31 31 with tt.AssertPrints("Error in callback"):
32 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