From c935d7ffe37cf9376aa30cb74c6fdaffe346255f Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Wed, 20 May 2009 16:13:55 +0000 Subject: [PATCH] Fixed #11134: signals recievers that disconnect during their processing no longer mess things up for other handlers. Thanks, Honza Kral. While I was at it I also cleaned up the formatting of the docstrings a bit. git-svn-id: http://code.djangoproject.com/svn/django/trunk@10831 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/dispatch/dispatcher.py | 175 +++++++++++++++++------------- tests/modeltests/signals/tests.py | 28 +++++ 2 files changed, 128 insertions(+), 75 deletions(-) create mode 100644 tests/modeltests/signals/tests.py diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py index 07377d6411..e9198fc934 100644 --- a/django/dispatch/dispatcher.py +++ b/django/dispatch/dispatcher.py @@ -14,15 +14,21 @@ def _make_id(target): return id(target) class Signal(object): - """Base class for all signals + """ + Base class for all signals Internal attributes: - receivers -- { receriverkey (id) : weakref(receiver) } + + receivers + { receriverkey (id) : weakref(receiver) } """ def __init__(self, providing_args=None): - """providing_args -- A list of the arguments this signal can pass along in - a send() call. + """ + Create a new signal. + + providing_args + A list of the arguments this signal can pass along in a send() call. """ self.receivers = [] if providing_args is None: @@ -30,36 +36,39 @@ class Signal(object): self.providing_args = set(providing_args) def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): - """Connect receiver to sender for signal + """ + Connect receiver to sender for signal. - receiver -- a function or an instance method which is to - receive signals. Receivers must be - hashable objects. - - if weak is True, then receiver must be weak-referencable - (more precisely saferef.safeRef() must be able to create - a reference to the receiver). + Arguments: - Receivers must be able to accept keyword arguments. + receiver + A function or an instance method which is to receive signals. + Receivers must be hashable objects. - If receivers have a dispatch_uid attribute, the receiver will - not be added if another receiver already exists with that - dispatch_uid. - - sender -- the sender to which the receiver should respond - Must either be of type Signal, or None to receive events - from any sender. - - weak -- whether to use weak references to the receiver - By default, the module will attempt to use weak - references to the receiver objects. If this parameter - is false, then strong references will be used. + if weak is True, then receiver must be weak-referencable (more + precisely saferef.safeRef() must be able to create a reference + to the receiver). - dispatch_uid -- an identifier used to uniquely identify a particular - instance of a receiver. This will usually be a string, though it - may be anything hashable. + Receivers must be able to accept keyword arguments. - returns None + If receivers have a dispatch_uid attribute, the receiver will + not be added if another receiver already exists with that + dispatch_uid. + + sender + The sender to which the receiver should respond Must either be + of type Signal, or None to receive events from any sender. + + weak + Whether to use weak references to the receiver By default, the + module will attempt to use weak references to the receiver + objects. If this parameter is false, then strong references will + be used. + + dispatch_uid + An identifier used to uniquely identify a particular instance of + a receiver. This will usually be a string, though it may be + anything hashable. """ from django.conf import settings @@ -99,22 +108,27 @@ class Signal(object): self.receivers.append((lookup_key, receiver)) def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None): - """Disconnect receiver from sender for signal - - receiver -- the registered receiver to disconnect. May be none if - dispatch_uid is specified. - sender -- the registered sender to disconnect - weak -- the weakref state to disconnect - dispatch_uid -- the unique identifier of the receiver to disconnect - - disconnect reverses the process of connect. - - If weak references are used, disconnect need not be called. - The receiver will be remove from dispatch automatically. - - returns None """ + Disconnect receiver from sender for signal. + If weak references are used, disconnect need not be called. The receiver + will be remove from dispatch automatically. + + Arguments: + + receiver + The registered receiver to disconnect. May be none if + dispatch_uid is specified. + + sender + The registered sender to disconnect + + weak + The weakref state to disconnect + + dispatch_uid + the unique identifier of the receiver to disconnect + """ if dispatch_uid: lookup_key = (dispatch_uid, _make_id(sender)) else: @@ -127,21 +141,23 @@ class Signal(object): break def send(self, sender, **named): - """Send signal from sender to all connected receivers. + """ + Send signal from sender to all connected receivers. - sender -- the sender of the signal - Either a specific object or None. + If any receiver raises an error, the error propagates back through send, + terminating the dispatch loop, so it is quite possible to not have all + receivers called if a raises an error. + + Arguments: + + sender + The sender of the signal Either a specific object or None. - named -- named arguments which will be passed to receivers. + named + Named arguments which will be passed to receivers. Returns a list of tuple pairs [(receiver, response), ... ]. - - If any receiver raises an error, the error propagates back - through send, terminating the dispatch loop, so it is quite - possible to not have all receivers called if a raises an - error. """ - responses = [] if not self.receivers: return responses @@ -152,23 +168,28 @@ class Signal(object): return responses def send_robust(self, sender, **named): - """Send signal from sender to all connected receivers catching errors - - sender -- the sender of the signal - Can be any python object (normally one registered with - a connect if you actually want something to occur). - - named -- named arguments which will be passed to receivers. - These arguments must be a subset of the argument names - defined in providing_args. - - Return a list of tuple pairs [(receiver, response), ... ], - may raise DispatcherKeyError - - if any receiver raises an error (specifically any subclass of Exception), - the error instance is returned as the result for that receiver. """ + Send signal from sender to all connected receivers catching errors. + Arguments: + + sender + The sender of the signal Can be any python object (normally one + registered with a connect if you actually want something to + occur). + + named + Named arguments which will be passed to receivers. These + arguments must be a subset of the argument names defined in + providing_args. + + Return a list of tuple pairs [(receiver, response), ... ]. May raise + DispatcherKeyError. + + if any receiver raises an error (specifically any subclass of + Exception), the error instance is returned as the result for that + receiver. + """ responses = [] if not self.receivers: return responses @@ -185,13 +206,14 @@ class Signal(object): return responses def _live_receivers(self, senderkey): - """Filter sequence of receivers to get resolved, live receivers + """ + Filter sequence of receivers to get resolved, live receivers. - This checks for weak references - and resolves them, then returning only live - receivers. + This checks for weak references and resolves them, then returning only + live receivers. """ none_senderkey = _make_id(None) + receivers = [] for (receiverkey, r_senderkey), receiver in self.receivers: if r_senderkey == none_senderkey or r_senderkey == senderkey: @@ -199,12 +221,15 @@ class Signal(object): # Dereference the weak reference. receiver = receiver() if receiver is not None: - yield receiver + receivers.append(receiver) else: - yield receiver + receivers.append(receiver) + return receivers def _remove_receiver(self, receiver): - """Remove dead receivers from connections.""" + """ + Remove dead receivers from connections. + """ to_remove = [] for key, connected_receiver in self.receivers: diff --git a/tests/modeltests/signals/tests.py b/tests/modeltests/signals/tests.py new file mode 100644 index 0000000000..329636c306 --- /dev/null +++ b/tests/modeltests/signals/tests.py @@ -0,0 +1,28 @@ +from django.db.models import signals +from django.test import TestCase +from modeltests.signals.models import Person + +class MyReceiver(object): + def __init__(self, param): + self.param = param + self._run = False + + def __call__(self, signal, sender, **kwargs): + self._run = True + signal.disconnect(receiver=self, sender=sender) + +class SignalTests(TestCase): + def test_disconnect_in_dispatch(self): + """ + Test that signals that disconnect when being called don't mess future + dispatching. + """ + a, b = MyReceiver(1), MyReceiver(2) + signals.post_save.connect(sender=Person, receiver=a) + signals.post_save.connect(sender=Person, receiver=b) + p = Person.objects.create(first_name='John', last_name='Smith') + + self.failUnless(a._run) + self.failUnless(b._run) + self.assertEqual(signals.post_save.receivers, []) +