From c4707d0304f9064e48fbf7d066cc3b3a8b4dba3f Mon Sep 17 00:00:00 2001
From: Simon Charette <charette.s@gmail.com>
Date: Mon, 19 Aug 2013 23:14:21 -0400
Subject: [PATCH] Fixed #20943 -- Weakly reference senders when caching their
associated receivers
---
django/db/models/signals.py | 2 +-
django/dispatch/dispatcher.py | 12 ++++++++----
tests/dispatch/tests/test_dispatcher.py | 21 +++++++++++++++++++++
3 files changed, 30 insertions(+), 5 deletions(-)
diff --git a/django/db/models/signals.py b/django/db/models/signals.py
index 3e32189..0782442 100644
a
|
b
|
pre_delete = Signal(providing_args=["instance", "using"], use_caching=True)
|
13 | 13 | post_delete = Signal(providing_args=["instance", "using"], use_caching=True) |
14 | 14 | |
15 | 15 | pre_syncdb = Signal(providing_args=["app", "create_models", "verbosity", "interactive", "db"]) |
16 | | post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"], use_caching=True) |
| 16 | post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"]) |
17 | 17 | |
18 | 18 | m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True) |
diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py
index 65c5c40..a8cdc93 100644
a
|
b
|
import threading
|
4 | 4 | from django.dispatch import saferef |
5 | 5 | from django.utils.six.moves import xrange |
6 | 6 | |
| 7 | |
7 | 8 | WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref) |
8 | 9 | |
| 10 | |
9 | 11 | def _make_id(target): |
10 | 12 | if hasattr(target, '__func__'): |
11 | 13 | return (id(target.__self__), id(target.__func__)) |
… |
… |
NONE_ID = _make_id(None)
|
15 | 17 | # A marker for caching |
16 | 18 | NO_RECEIVERS = object() |
17 | 19 | |
| 20 | |
18 | 21 | class Signal(object): |
19 | 22 | """ |
20 | 23 | Base class for all signals |
… |
… |
class Signal(object):
|
42 | 45 | # distinct sender we cache the receivers that sender has in |
43 | 46 | # 'sender_receivers_cache'. The cache is cleaned when .connect() or |
44 | 47 | # .disconnect() is called and populated on send(). |
45 | | self.sender_receivers_cache = {} |
| 48 | self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {} |
46 | 49 | |
47 | 50 | def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): |
48 | 51 | """ |
… |
… |
class Signal(object):
|
116 | 119 | break |
117 | 120 | else: |
118 | 121 | self.receivers.append((lookup_key, receiver)) |
119 | | self.sender_receivers_cache = {} |
| 122 | self.sender_receivers_cache.clear() |
120 | 123 | |
121 | 124 | def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None): |
122 | 125 | """ |
… |
… |
class Signal(object):
|
151 | 154 | if r_key == lookup_key: |
152 | 155 | del self.receivers[index] |
153 | 156 | break |
154 | | self.sender_receivers_cache = {} |
| 157 | self.sender_receivers_cache.clear() |
155 | 158 | |
156 | 159 | def has_listeners(self, sender=None): |
157 | 160 | return bool(self._live_receivers(sender)) |
… |
… |
class Signal(object):
|
276 | 279 | for idx, (r_key, _) in enumerate(reversed(self.receivers)): |
277 | 280 | if r_key == key: |
278 | 281 | del self.receivers[last_idx - idx] |
279 | | self.sender_receivers_cache = {} |
| 282 | self.sender_receivers_cache.clear() |
| 283 | |
280 | 284 | |
281 | 285 | def receiver(signal, **kwargs): |
282 | 286 | """ |
diff --git a/tests/dispatch/tests/test_dispatcher.py b/tests/dispatch/tests/test_dispatcher.py
index 5f7dca8..e25f60b 100644
a
|
b
|
import gc
|
2 | 2 | import sys |
3 | 3 | import time |
4 | 4 | import unittest |
| 5 | import weakref |
5 | 6 | |
6 | 7 | from django.dispatch import Signal, receiver |
7 | 8 | |
… |
… |
class Callable(object):
|
35 | 36 | a_signal = Signal(providing_args=["val"]) |
36 | 37 | b_signal = Signal(providing_args=["val"]) |
37 | 38 | c_signal = Signal(providing_args=["val"]) |
| 39 | d_signal = Signal(providing_args=["val"], use_caching=True) |
| 40 | |
38 | 41 | |
39 | 42 | class DispatcherTests(unittest.TestCase): |
40 | 43 | """Test suite for dispatcher (barely started)""" |
… |
… |
class DispatcherTests(unittest.TestCase):
|
72 | 75 | self.assertEqual(result, expected) |
73 | 76 | self._testIsClean(a_signal) |
74 | 77 | |
| 78 | def testCachedGarbagedCollected(self): |
| 79 | """ |
| 80 | Make sure signal caching sender receivers don't prevent garbage |
| 81 | collection of senders. |
| 82 | """ |
| 83 | class sender: |
| 84 | pass |
| 85 | wref = weakref.ref(sender) |
| 86 | d_signal.connect(receiver_1_arg) |
| 87 | d_signal.send(sender, val='garbage') |
| 88 | del sender |
| 89 | garbage_collect() |
| 90 | try: |
| 91 | self.assertIsNone(wref()) |
| 92 | finally: |
| 93 | # Disconnect after reference check since it flushes the tested cache. |
| 94 | d_signal.disconnect(receiver_1_arg) |
| 95 | |
75 | 96 | def testMultipleRegistration(self): |
76 | 97 | a = Callable() |
77 | 98 | a_signal.connect(a) |