Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/signals/dispatch/dispatcher.py: 47%
123 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:03 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:03 -0500
1import logging
2import threading
3import weakref
5from plain.utils.inspect import func_accepts_kwargs
7logger = logging.getLogger("plain.signals.dispatch")
10def _make_id(target):
11 if hasattr(target, "__func__"):
12 return (id(target.__self__), id(target.__func__))
13 return id(target)
16NONE_ID = _make_id(None)
18# A marker for caching
19NO_RECEIVERS = object()
22class Signal:
23 """
24 Base class for all signals
26 Internal attributes:
28 receivers
29 { receiverkey (id) : weakref(receiver) }
30 """
32 def __init__(self, use_caching=False):
33 """
34 Create a new signal.
35 """
36 self.receivers = []
37 self.lock = threading.Lock()
38 self.use_caching = use_caching
39 # For convenience we create empty caches even if they are not used.
40 # A note about caching: if use_caching is defined, then for each
41 # distinct sender we cache the receivers that sender has in
42 # 'sender_receivers_cache'. The cache is cleaned when .connect() or
43 # .disconnect() is called and populated on send().
44 self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {}
45 self._dead_receivers = False
47 def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
48 """
49 Connect receiver to sender for signal.
51 Arguments:
53 receiver
54 A function or an instance method which is to receive signals.
55 Receivers must be hashable objects. Receivers can be
56 asynchronous.
58 If weak is True, then receiver must be weak referenceable.
60 Receivers must be able to accept keyword arguments.
62 If a receiver is connected with a dispatch_uid argument, it
63 will not be added if another receiver was already connected
64 with that dispatch_uid.
66 sender
67 The sender to which the receiver should respond. Must either be
68 a Python object, or None to receive events from any sender.
70 weak
71 Whether to use weak references to the receiver. By default, the
72 module will attempt to use weak references to the receiver
73 objects. If this parameter is false, then strong references will
74 be used.
76 dispatch_uid
77 An identifier used to uniquely identify a particular instance of
78 a receiver. This will usually be a string, though it may be
79 anything hashable.
80 """
81 from plain.runtime import settings
83 # If DEBUG is on, check that we got a good receiver
84 if settings.configured and settings.DEBUG:
85 if not callable(receiver):
86 raise TypeError("Signal receivers must be callable.")
87 # Check for **kwargs
88 if not func_accepts_kwargs(receiver):
89 raise ValueError(
90 "Signal receivers must accept keyword arguments (**kwargs)."
91 )
93 if dispatch_uid:
94 lookup_key = (dispatch_uid, _make_id(sender))
95 else:
96 lookup_key = (_make_id(receiver), _make_id(sender))
98 if weak:
99 ref = weakref.ref
100 receiver_object = receiver
101 # Check for bound methods
102 if hasattr(receiver, "__self__") and hasattr(receiver, "__func__"):
103 ref = weakref.WeakMethod
104 receiver_object = receiver.__self__
105 receiver = ref(receiver)
106 weakref.finalize(receiver_object, self._remove_receiver)
108 with self.lock:
109 self._clear_dead_receivers()
110 if not any(r_key == lookup_key for r_key, _ in self.receivers):
111 self.receivers.append((lookup_key, receiver))
112 self.sender_receivers_cache.clear()
114 def disconnect(self, receiver=None, sender=None, dispatch_uid=None):
115 """
116 Disconnect receiver from sender for signal.
118 If weak references are used, disconnect need not be called. The receiver
119 will be removed from dispatch automatically.
121 Arguments:
123 receiver
124 The registered receiver to disconnect. May be none if
125 dispatch_uid is specified.
127 sender
128 The registered sender to disconnect
130 dispatch_uid
131 the unique identifier of the receiver to disconnect
132 """
133 if dispatch_uid:
134 lookup_key = (dispatch_uid, _make_id(sender))
135 else:
136 lookup_key = (_make_id(receiver), _make_id(sender))
138 disconnected = False
139 with self.lock:
140 self._clear_dead_receivers()
141 for index in range(len(self.receivers)):
142 r_key, *_ = self.receivers[index]
143 if r_key == lookup_key:
144 disconnected = True
145 del self.receivers[index]
146 break
147 self.sender_receivers_cache.clear()
148 return disconnected
150 def has_listeners(self, sender=None):
151 sync_receivers = self._live_receivers(sender)
152 return bool(sync_receivers)
154 def send(self, sender, **named):
155 """
156 Send signal from sender to all connected receivers.
158 If any receiver raises an error, the error propagates back through send,
159 terminating the dispatch loop. So it's possible that all receivers
160 won't be called if an error is raised.
162 If any receivers are asynchronous, they are called after all the
163 synchronous receivers via a single call to async_to_sync(). They are
164 also executed concurrently with asyncio.gather().
166 Arguments:
168 sender
169 The sender of the signal. Either a specific object or None.
171 named
172 Named arguments which will be passed to receivers.
174 Return a list of tuple pairs [(receiver, response), ... ].
175 """
176 if (
177 not self.receivers
178 or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
179 ):
180 return []
181 responses = []
182 sync_receivers = self._live_receivers(sender)
183 for receiver in sync_receivers:
184 response = receiver(signal=self, sender=sender, **named)
185 responses.append((receiver, response))
186 return responses
188 def _log_robust_failure(self, receiver, err):
189 logger.error(
190 "Error calling %s in Signal.send_robust() (%s)",
191 receiver.__qualname__,
192 err,
193 exc_info=err,
194 )
196 def send_robust(self, sender, **named):
197 """
198 Send signal from sender to all connected receivers catching errors.
200 If any receivers are asynchronous, they are called after all the
201 synchronous receivers via a single call to async_to_sync(). They are
202 also executed concurrently with asyncio.gather().
204 Arguments:
206 sender
207 The sender of the signal. Can be any Python object (normally one
208 registered with a connect if you actually want something to
209 occur).
211 named
212 Named arguments which will be passed to receivers.
214 Return a list of tuple pairs [(receiver, response), ... ].
216 If any receiver raises an error (specifically any subclass of
217 Exception), return the error instance as the result for that receiver.
218 """
219 if (
220 not self.receivers
221 or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
222 ):
223 return []
225 # Call each receiver with whatever arguments it can accept.
226 # Return a list of tuple pairs [(receiver, response), ... ].
227 responses = []
228 sync_receivers = self._live_receivers(sender)
229 for receiver in sync_receivers:
230 try:
231 response = receiver(signal=self, sender=sender, **named)
232 except Exception as err:
233 self._log_robust_failure(receiver, err)
234 responses.append((receiver, err))
235 else:
236 responses.append((receiver, response))
237 return responses
239 def _clear_dead_receivers(self):
240 # Note: caller is assumed to hold self.lock.
241 if self._dead_receivers:
242 self._dead_receivers = False
243 self.receivers = [
244 r
245 for r in self.receivers
246 if not (isinstance(r[1], weakref.ReferenceType) and r[1]() is None)
247 ]
249 def _live_receivers(self, sender):
250 """
251 Filter sequence of receivers to get resolved, live receivers.
253 This checks for weak references and resolves them, then returning only
254 live receivers.
255 """
256 receivers = None
257 if self.use_caching and not self._dead_receivers:
258 receivers = self.sender_receivers_cache.get(sender)
259 # We could end up here with NO_RECEIVERS even if we do check this case in
260 # .send() prior to calling _live_receivers() due to concurrent .send() call.
261 if receivers is NO_RECEIVERS:
262 return []
263 if receivers is None:
264 with self.lock:
265 self._clear_dead_receivers()
266 senderkey = _make_id(sender)
267 receivers = []
268 for (_receiverkey, r_senderkey), receiver in self.receivers:
269 if r_senderkey == NONE_ID or r_senderkey == senderkey:
270 receivers.append(receiver)
271 if self.use_caching:
272 if not receivers:
273 self.sender_receivers_cache[sender] = NO_RECEIVERS
274 else:
275 # Note, we must cache the weakref versions.
276 self.sender_receivers_cache[sender] = receivers
277 non_weak_sync_receivers = []
278 for receiver in receivers:
279 if isinstance(receiver, weakref.ReferenceType):
280 # Dereference the weak reference.
281 receiver = receiver()
282 if receiver is not None:
283 non_weak_sync_receivers.append(receiver)
284 else:
285 non_weak_sync_receivers.append(receiver)
286 return non_weak_sync_receivers
288 def _remove_receiver(self, receiver=None):
289 # Mark that the self.receivers list has dead weakrefs. If so, we will
290 # clean those up in connect, disconnect and _live_receivers while
291 # holding self.lock. Note that doing the cleanup here isn't a good
292 # idea, _remove_receiver() will be called as side effect of garbage
293 # collection, and so the call can happen while we are already holding
294 # self.lock.
295 self._dead_receivers = True
298def receiver(signal, **kwargs):
299 """
300 A decorator for connecting receivers to signals. Used by passing in the
301 signal (or list of signals) and keyword arguments to connect::
303 @receiver(post_save, sender=MyModel)
304 def signal_receiver(sender, **kwargs):
305 ...
307 @receiver([post_save, post_delete], sender=MyModel)
308 def signals_receiver(sender, **kwargs):
309 ...
310 """
312 def _decorator(func):
313 if isinstance(signal, list | tuple):
314 for s in signal:
315 s.connect(func, **kwargs)
316 else:
317 signal.connect(func, **kwargs)
318 return func
320 return _decorator