From 68860ee51dbd7504abae05c4031eed5146a677e5 2016-05-12 06:27:23 From: Craig Citro Date: 2016-05-12 06:27:23 Subject: [PATCH] Make event triggering robust to (un)registration. Event callbacks can register or unregister new callbacks for the same event while executing, and the previous triggering implementation allowed for event callbacks to be inadvertently skipped. The fix is to make a copy of the list of callbacks before executing any of them. With this change, the resulting semantics are simple: any callbacks registered before triggering are executed, and any new callbacks registered are only visible at the next triggering of the event. Note that this could potentially break existing callers who expected newly-appended callbacks were immediately executed. Fixes #9447. Originally based on a patch by @marksandler2. --- diff --git a/IPython/core/events.py b/IPython/core/events.py index 798ef01..bfd09fe 100644 --- a/IPython/core/events.py +++ b/IPython/core/events.py @@ -69,7 +69,7 @@ class EventManager(object): Any additional arguments are passed to all callbacks registered for this event. Exceptions raised by callbacks are caught, and a message printed. """ - for func in self.callbacks[event]: + for func in self.callbacks[event][:]: try: func(*args, **kwargs) except Exception: diff --git a/IPython/core/tests/test_events.py b/IPython/core/tests/test_events.py index 8e8402c..3053a70 100644 --- a/IPython/core/tests/test_events.py +++ b/IPython/core/tests/test_events.py @@ -30,3 +30,25 @@ class CallbackTests(unittest.TestCase): self.em.register('ping_received', cb) with tt.AssertPrints("Error in callback"): self.em.trigger('ping_received') + + def test_unregister_during_callback(self): + invoked = [False] * 3 + + def func1(*_): + invoked[0] = True + self.em.unregister('ping_received', func1) + self.em.register('ping_received', func3) + + def func2(*_): + invoked[1] = True + self.em.unregister('ping_received', func2) + + def func3(*_): + invoked[2] = True + + self.em.register('ping_received', func1) + self.em.register('ping_received', func2) + + self.em.trigger('ping_received') + self.assertEqual([True, True, False], invoked) + self.assertEqual([func3], self.em.callbacks['ping_received']) diff --git a/docs/source/whatsnew/pr/incompat-event-triggering.rst b/docs/source/whatsnew/pr/incompat-event-triggering.rst new file mode 100644 index 0000000..432af68 --- /dev/null +++ b/docs/source/whatsnew/pr/incompat-event-triggering.rst @@ -0,0 +1,7 @@ +Update IPython event triggering to ensure callback registration and +unregistration only affects the set of callbacks the *next* time that event is +triggered. See :ghissue:`9447` and :ghpull:`9453`. + +This is a change to the existing semantics, wherein one callback registering a +second callback when triggered for an event would previously be invoked for +that same event.