Fixed #20943 -- Weakly reference senders when caching their associated receivers

Backport of e55ca60903 from master.
This commit is contained in:
Simon Charette 2013-08-19 23:14:21 -04:00
parent e7a6eaf5fe
commit f0bc2865ff
3 changed files with 30 additions and 5 deletions

View File

@ -13,6 +13,6 @@ pre_delete = Signal(providing_args=["instance", "using"], use_caching=True)
post_delete = Signal(providing_args=["instance", "using"], use_caching=True) post_delete = Signal(providing_args=["instance", "using"], use_caching=True)
pre_syncdb = Signal(providing_args=["app", "create_models", "verbosity", "interactive", "db"]) pre_syncdb = Signal(providing_args=["app", "create_models", "verbosity", "interactive", "db"])
post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"], use_caching=True) post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"])
m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True) m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True)

View File

@ -4,8 +4,10 @@ import threading
from django.dispatch import saferef from django.dispatch import saferef
from django.utils.six.moves import xrange from django.utils.six.moves import xrange
WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref) WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref)
def _make_id(target): def _make_id(target):
if hasattr(target, '__func__'): if hasattr(target, '__func__'):
return (id(target.__self__), id(target.__func__)) return (id(target.__self__), id(target.__func__))
@ -15,6 +17,7 @@ NONE_ID = _make_id(None)
# A marker for caching # A marker for caching
NO_RECEIVERS = object() NO_RECEIVERS = object()
class Signal(object): class Signal(object):
""" """
Base class for all signals Base class for all signals
@ -42,7 +45,7 @@ class Signal(object):
# distinct sender we cache the receivers that sender has in # distinct sender we cache the receivers that sender has in
# 'sender_receivers_cache'. The cache is cleaned when .connect() or # 'sender_receivers_cache'. The cache is cleaned when .connect() or
# .disconnect() is called and populated on send(). # .disconnect() is called and populated on send().
self.sender_receivers_cache = {} self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {}
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
""" """
@ -116,7 +119,7 @@ class Signal(object):
break break
else: else:
self.receivers.append((lookup_key, receiver)) self.receivers.append((lookup_key, receiver))
self.sender_receivers_cache = {} self.sender_receivers_cache.clear()
def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None): def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None):
""" """
@ -151,7 +154,7 @@ class Signal(object):
if r_key == lookup_key: if r_key == lookup_key:
del self.receivers[index] del self.receivers[index]
break break
self.sender_receivers_cache = {} self.sender_receivers_cache.clear()
def has_listeners(self, sender=None): def has_listeners(self, sender=None):
return bool(self._live_receivers(sender)) return bool(self._live_receivers(sender))
@ -276,7 +279,8 @@ class Signal(object):
for idx, (r_key, _) in enumerate(reversed(self.receivers)): for idx, (r_key, _) in enumerate(reversed(self.receivers)):
if r_key == key: if r_key == key:
del self.receivers[last_idx - idx] del self.receivers[last_idx - idx]
self.sender_receivers_cache = {} self.sender_receivers_cache.clear()
def receiver(signal, **kwargs): def receiver(signal, **kwargs):
""" """

View File

@ -1,6 +1,7 @@
import gc import gc
import sys import sys
import time import time
import weakref
from django.dispatch import Signal, receiver from django.dispatch import Signal, receiver
from django.utils import unittest from django.utils import unittest
@ -35,6 +36,8 @@ class Callable(object):
a_signal = Signal(providing_args=["val"]) a_signal = Signal(providing_args=["val"])
b_signal = Signal(providing_args=["val"]) b_signal = Signal(providing_args=["val"])
c_signal = Signal(providing_args=["val"]) c_signal = Signal(providing_args=["val"])
d_signal = Signal(providing_args=["val"], use_caching=True)
class DispatcherTests(unittest.TestCase): class DispatcherTests(unittest.TestCase):
"""Test suite for dispatcher (barely started)""" """Test suite for dispatcher (barely started)"""
@ -72,6 +75,24 @@ class DispatcherTests(unittest.TestCase):
self.assertEqual(result, expected) self.assertEqual(result, expected)
self._testIsClean(a_signal) self._testIsClean(a_signal)
def testCachedGarbagedCollected(self):
"""
Make sure signal caching sender receivers don't prevent garbage
collection of senders.
"""
class sender:
pass
wref = weakref.ref(sender)
d_signal.connect(receiver_1_arg)
d_signal.send(sender, val='garbage')
del sender
garbage_collect()
try:
self.assertIsNone(wref())
finally:
# Disconnect after reference check since it flushes the tested cache.
d_signal.disconnect(receiver_1_arg)
def testMultipleRegistration(self): def testMultipleRegistration(self):
a = Callable() a = Callable()
a_signal.connect(a) a_signal.connect(a)