From cc63e2f848524b3e5a994a85891f8212158c13b4 Mon Sep 17 00:00:00 2001
From: Jannis Leidel <jannis@leidel.info>
Date: Sun, 1 May 2011 23:55:44 +0200
Subject: [PATCH] Fixed #9015 -- Extended the signal decorator interface by allowing using the Signal.connect method as a decorator.
---
django/dispatch/dispatcher.py | 56 ++++++++++++++++++++++--------------
docs/topics/signals.txt | 12 ++++++++
tests/modeltests/signals/tests.py | 21 ++++++++++++-
3 files changed, 65 insertions(+), 24 deletions(-)
diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py
index ed9da57..305b2ba 100644
a
|
b
|
def _make_id(target):
|
13 | 13 | class Signal(object): |
14 | 14 | """ |
15 | 15 | Base class for all signals |
16 | | |
| 16 | |
17 | 17 | Internal attributes: |
18 | | |
| 18 | |
19 | 19 | receivers |
20 | 20 | { receriverkey (id) : weakref(receiver) } |
21 | 21 | """ |
22 | | |
| 22 | |
23 | 23 | def __init__(self, providing_args=None): |
24 | 24 | """ |
25 | 25 | Create a new signal. |
26 | | |
| 26 | |
27 | 27 | providing_args |
28 | 28 | A list of the arguments this signal can pass along in a send() call. |
29 | 29 | """ |
… |
… |
class Signal(object):
|
33 | 33 | self.providing_args = set(providing_args) |
34 | 34 | self.lock = threading.Lock() |
35 | 35 | |
36 | | def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): |
| 36 | def connect(self, receiver=None, *args, **kwargs): |
37 | 37 | """ |
38 | 38 | Connect receiver to sender for signal. |
39 | | |
| 39 | |
40 | 40 | Arguments: |
41 | | |
| 41 | |
42 | 42 | receiver |
43 | 43 | A function or an instance method which is to receive signals. |
44 | 44 | Receivers must be hashable objects. |
… |
… |
class Signal(object):
|
46 | 46 | If weak is True, then receiver must be weak-referencable (more |
47 | 47 | precisely saferef.safeRef() must be able to create a reference |
48 | 48 | to the receiver). |
49 | | |
| 49 | |
50 | 50 | Receivers must be able to accept keyword arguments. |
51 | 51 | |
52 | 52 | If receivers have a dispatch_uid attribute, the receiver will |
… |
… |
class Signal(object):
|
62 | 62 | module will attempt to use weak references to the receiver |
63 | 63 | objects. If this parameter is false, then strong references will |
64 | 64 | be used. |
65 | | |
| 65 | |
66 | 66 | dispatch_uid |
67 | 67 | An identifier used to uniquely identify a particular instance of |
68 | 68 | a receiver. This will usually be a string, though it may be |
69 | 69 | anything hashable. |
70 | 70 | """ |
| 71 | # ``connect`` is being called with a function. |
| 72 | if receiver: |
| 73 | self._connect(receiver, *args, **kwargs) |
| 74 | # Return the receiver, if being used for decoration. |
| 75 | return receiver |
| 76 | def wrapper(receiver): |
| 77 | self._connect(receiver, *args, **kwargs) |
| 78 | return receiver |
| 79 | # Return this wrapper so that it can be used to decorate a function. |
| 80 | return wrapper |
| 81 | |
| 82 | def _connect(self, receiver, sender=None, weak=True, dispatch_uid=None): |
71 | 83 | from django.conf import settings |
72 | | |
| 84 | |
73 | 85 | # If DEBUG is on, check that we got a good receiver |
74 | 86 | if settings.DEBUG: |
75 | 87 | import inspect |
76 | 88 | assert callable(receiver), "Signal receivers must be callable." |
77 | | |
| 89 | |
78 | 90 | # Check for **kwargs |
79 | 91 | # Not all callables are inspectable with getargspec, so we'll |
80 | 92 | # try a couple different ways but in the end fall back on assuming |
… |
… |
class Signal(object):
|
90 | 102 | if argspec: |
91 | 103 | assert argspec[2] is not None, \ |
92 | 104 | "Signal receivers must accept keyword arguments (**kwargs)." |
93 | | |
| 105 | |
94 | 106 | if dispatch_uid: |
95 | 107 | lookup_key = (dispatch_uid, _make_id(sender)) |
96 | 108 | else: |
… |
… |
class Signal(object):
|
115 | 127 | |
116 | 128 | If weak references are used, disconnect need not be called. The receiver |
117 | 129 | will be remove from dispatch automatically. |
118 | | |
| 130 | |
119 | 131 | Arguments: |
120 | | |
| 132 | |
121 | 133 | receiver |
122 | 134 | The registered receiver to disconnect. May be none if |
123 | 135 | dispatch_uid is specified. |
124 | | |
| 136 | |
125 | 137 | sender |
126 | 138 | The registered sender to disconnect |
127 | | |
| 139 | |
128 | 140 | weak |
129 | 141 | The weakref state to disconnect |
130 | | |
| 142 | |
131 | 143 | dispatch_uid |
132 | 144 | the unique identifier of the receiver to disconnect |
133 | 145 | """ |
… |
… |
class Signal(object):
|
135 | 147 | lookup_key = (dispatch_uid, _make_id(sender)) |
136 | 148 | else: |
137 | 149 | lookup_key = (_make_id(receiver), _make_id(sender)) |
138 | | |
| 150 | |
139 | 151 | self.lock.acquire() |
140 | 152 | try: |
141 | 153 | for index in xrange(len(self.receivers)): |
… |
… |
class Signal(object):
|
155 | 167 | receivers called if a raises an error. |
156 | 168 | |
157 | 169 | Arguments: |
158 | | |
| 170 | |
159 | 171 | sender |
160 | 172 | The sender of the signal Either a specific object or None. |
161 | | |
| 173 | |
162 | 174 | named |
163 | 175 | Named arguments which will be passed to receivers. |
164 | 176 | |
… |
… |
class Signal(object):
|
178 | 190 | Send signal from sender to all connected receivers catching errors. |
179 | 191 | |
180 | 192 | Arguments: |
181 | | |
| 193 | |
182 | 194 | sender |
183 | 195 | The sender of the signal. Can be any python object (normally one |
184 | 196 | registered with a connect if you actually want something to |
… |
… |
def receiver(signal, **kwargs):
|
265 | 277 | |
266 | 278 | """ |
267 | 279 | def _decorator(func): |
268 | | signal.connect(func, **kwargs) |
| 280 | signal._connect(func, **kwargs) |
269 | 281 | return func |
270 | 282 | return _decorator |
diff --git a/docs/topics/signals.txt b/docs/topics/signals.txt
index 64ce202..00f9b0a 100644
a
|
b
|
Now, our ``my_callback`` function will be called each time a request finishes.
|
133 | 133 | |
134 | 134 | The ``receiver`` decorator was added in Django 1.3. |
135 | 135 | |
| 136 | .. versionadded:: 1.4 |
| 137 | |
| 138 | Optionally you can use the signal as a decorator itself:: |
| 139 | |
| 140 | .. code-block:: python |
| 141 | |
| 142 | from django.core.signals import request_finished |
| 143 | |
| 144 | @request_finished.connect |
| 145 | def my_callback(sender, **kwargs): |
| 146 | print "Request finished!" |
| 147 | |
136 | 148 | .. admonition:: Where should this code live? |
137 | 149 | |
138 | 150 | You can put signal handling and registration code anywhere you like. |
diff --git a/tests/modeltests/signals/tests.py b/tests/modeltests/signals/tests.py
index 9b8bce0..e097e1e 100644
a
|
b
|
class SignalTests(TestCase):
|
60 | 60 | |
61 | 61 | # throw a decorator syntax receiver into the mix |
62 | 62 | @receiver(signals.pre_save) |
63 | | def pre_save_decorator_test(signal, sender, instance, **kwargs): |
| 63 | def pre_save_receiver_decorator_test(signal, sender, instance, **kwargs): |
64 | 64 | data.append(instance) |
65 | 65 | |
66 | 66 | @receiver(signals.pre_save, sender=Car) |
| 67 | def pre_save_receiver_decorator_sender_test(signal, sender, instance, **kwargs): |
| 68 | data.append(instance) |
| 69 | |
| 70 | @signals.pre_save.connect |
| 71 | def pre_save_decorator_test(signal, sender, instance, **kwargs): |
| 72 | data.append(instance) |
| 73 | |
| 74 | @signals.pre_save.connect(sender=Car) |
67 | 75 | def pre_save_decorator_sender_test(signal, sender, instance, **kwargs): |
68 | 76 | data.append(instance) |
69 | 77 | |
… |
… |
class SignalTests(TestCase):
|
73 | 81 | self.assertEqual(data, [ |
74 | 82 | (p1, False), |
75 | 83 | p1, |
| 84 | p1, |
76 | 85 | (p1, True, False), |
77 | 86 | ]) |
78 | 87 | data[:] = [] |
… |
… |
class SignalTests(TestCase):
|
82 | 91 | self.assertEqual(data, [ |
83 | 92 | (p1, False), |
84 | 93 | p1, |
| 94 | p1, |
85 | 95 | (p1, False, False), |
86 | 96 | ]) |
87 | 97 | data[:] = [] |
88 | 98 | |
89 | 99 | # Car signal (sender defined) |
90 | | c1 = Car(make="Volkswagon", model="Passat") |
| 100 | c1 = Car(make="Volkswagen", model="Passat") |
91 | 101 | c1.save() |
92 | 102 | self.assertEqual(data, [ |
93 | 103 | (c1, False), |
94 | 104 | c1, |
95 | 105 | c1, |
| 106 | c1, |
| 107 | c1, |
96 | 108 | (c1, True, False), |
97 | 109 | ]) |
98 | 110 | data[:] = [] |
… |
… |
class SignalTests(TestCase):
|
102 | 114 | self.assertEqual(data, [ |
103 | 115 | (p1, True), |
104 | 116 | p1, |
| 117 | p1, |
105 | 118 | (p1, False, True), |
106 | 119 | ]) |
107 | 120 | data[:] = [] |
… |
… |
class SignalTests(TestCase):
|
119 | 132 | self.assertEqual(data, [ |
120 | 133 | (p2, False), |
121 | 134 | p2, |
| 135 | p2, |
122 | 136 | (p2, True, False), |
123 | 137 | ]) |
124 | 138 | data[:] = [] |
… |
… |
class SignalTests(TestCase):
|
128 | 142 | self.assertEqual(data, [ |
129 | 143 | (p2, False), |
130 | 144 | p2, |
| 145 | p2, |
131 | 146 | (p2, True, False), |
132 | 147 | ]) |
133 | 148 | data[:] = [] |
… |
… |
class SignalTests(TestCase):
|
151 | 166 | signals.pre_save.disconnect(pre_save_test) |
152 | 167 | signals.pre_save.disconnect(pre_save_decorator_test) |
153 | 168 | signals.pre_save.disconnect(pre_save_decorator_sender_test, sender=Car) |
| 169 | signals.pre_save.disconnect(pre_save_receiver_decorator_test) |
| 170 | signals.pre_save.disconnect(pre_save_receiver_decorator_sender_test, sender=Car) |
154 | 171 | |
155 | 172 | # Check that all our signals got disconnected properly. |
156 | 173 | post_signals = ( |