Coverage for signals/dispatch/dispatcher.py: 70%
217 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-16 09:17 +0330
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-16 09:17 +0330
1import asyncio
2import logging
3import threading
4import weakref
5from collections.abc import Awaitable, Hashable, Iterator
6from types import FunctionType, MethodType
7from typing import Any, Protocol, Callable, cast, overload
9from asgiref.sync import async_to_sync, iscoroutinefunction, sync_to_async
11from lazy_settings.conf import settings
13from signals import global_settings
14from signals.utils.inspect import func_accepts_kwargs
17logger = logging.getLogger("signals.dispatch")
19settings.register(global_settings)
22@overload
23def _make_id(target: MethodType) -> tuple[int, int]: ...
26@overload
27def _make_id(target: type | FunctionType | None | Any) -> int: ...
30def _make_id(target: None | Any) -> tuple[int, int] | int:
31 if target and hasattr(target, "__func__"):
32 return id(target.__self__), id(target.__func__)
33 return id(target)
36NONE_ID = _make_id(None)
38# A marker for caching
39NO_RECEIVERS = 1
42class Receiver(Protocol):
43 def __call__(
44 self, *, signal: "Signal", sender: object, **kwargs: Any
45 ) -> Any | Awaitable[Any]: ...
47 __qualname__: str
50type RECEIVER_T = Receiver
51type RECEIVER_REF_T = weakref.ReferenceType[RECEIVER_T]
52type LOOKUP_KEY_T = tuple[Hashable | tuple[int, int] | int, tuple[int, int] | int]
55class Signal:
56 """
57 Base class for all signals
59 Internal attributes:
61 receivers:
62 [((id(receiver), id(sender)), ref(receiver), ref(sender), is_async)]
63 sender_receivers_cache:
64 WeakKeyDictionary[sender, list[receiver]]
65 """
67 def __init__(self, use_caching: bool = False) -> None:
68 """
69 Create a new signal.
70 """
71 self.receivers: list[
72 tuple[
73 tuple[Hashable | tuple[int, int] | int, tuple[int, int] | int],
74 RECEIVER_T | RECEIVER_REF_T,
75 Any,
76 bool,
77 ]
78 ] = []
79 self.lock = threading.Lock()
80 self.use_caching = use_caching
81 # For convenience we create empty caches even if they are not used.
82 # A note about caching: if use_caching is defined, then for each
83 # distinct sender we cache the receivers that sender has in
84 # 'sender_receivers_cache'. The cache is cleaned when .connect() or
85 # .disconnect() is called and populated on send().
86 self.sender_receivers_cache: (
87 weakref.WeakKeyDictionary[
88 Any,
89 list[tuple[RECEIVER_T | RECEIVER_REF_T, tuple[int, int] | int, bool]]
90 | int,
91 ]
92 | dict[
93 Any,
94 list[tuple[RECEIVER_T | RECEIVER_REF_T, tuple[int, int] | int, bool]]
95 | int,
96 ]
97 ) = weakref.WeakKeyDictionary() if use_caching else {}
98 self._dead_receivers: bool = False
100 def connect(
101 self,
102 receiver: RECEIVER_T,
103 sender: Any | None = None,
104 weak: bool = True,
105 dispatch_uid: Hashable | None = None,
106 ) -> None:
107 """
108 Connect receiver to sender for signal.
110 Arguments:
112 receiver
113 A function or an instance method which is to receive signals.
114 Receivers must be hashable objects. Receivers can be
115 asynchronous.
117 If weak is True, then receiver must be weak referenceable.
119 Receivers must be able to accept keyword arguments.
121 If a receiver is connected with a dispatch_uid argument, it
122 will not be added if another receiver was already connected
123 with that dispatch_uid.
125 sender
126 The sender to which the receiver should respond. Must either be
127 a Python object, or None to receive events from any sender.
129 weak
130 Whether to use weak references to the receiver. By default, the
131 module will attempt to use weak references to the receiver
132 objects. If this parameter is false, then strong references will
133 be used.
135 dispatch_uid
136 An identifier used to uniquely identify a particular instance of
137 a receiver. This will usually be a string, though it may be
138 anything hashable.
139 """
141 # If DEBUG is on, check that we got a good receiver
142 if settings.configured and settings.DEBUG:
143 if not callable(receiver):
144 raise TypeError("Signal receivers must be callable.")
145 # Check for **kwargs
146 if not func_accepts_kwargs(receiver):
147 raise ValueError(
148 "Signal receivers must accept keyword arguments (**kwargs)."
149 )
151 lookup_key: LOOKUP_KEY_T
152 if dispatch_uid:
153 lookup_key = (
154 dispatch_uid,
155 _make_id(sender),
156 )
157 else:
158 lookup_key = (_make_id(receiver), _make_id(sender))
160 is_async: bool = iscoroutinefunction(receiver)
162 receiver_to_store: RECEIVER_T | RECEIVER_REF_T
164 if weak:
165 ref: (
166 type[weakref.ref[RECEIVER_T]] | type[weakref.WeakMethod[RECEIVER_T]]
167 ) = weakref.ref # type: ignore[assignment]
168 receiver_object: RECEIVER_T | object = receiver
169 # Check for bound methods
170 if hasattr(receiver, "__self__") and hasattr(receiver, "__func__"):
171 ref = weakref.WeakMethod
172 receiver_object = receiver.__self__
173 receiver_to_store = ref(receiver)
174 weakref.finalize(receiver_object, self._flag_dead_receivers)
175 else:
176 receiver_to_store = receiver
178 # Keep a weakref to sender if possible to ensure associated receivers
179 # are cleared if it gets garbage collected. This ensures there is no
180 # id(sender) collisions for distinct senders with non-overlapping
181 # lifetimes.
182 sender_ref: object | None = None
183 if sender is not None:
184 try:
185 sender_ref = weakref.ref(sender, self._flag_dead_receivers)
186 except TypeError:
187 pass
189 with self.lock:
190 self._clear_dead_receivers()
191 if not any(r_key == lookup_key for r_key, _, _, _ in self.receivers):
192 self.receivers.append(
193 (lookup_key, receiver_to_store, sender_ref, is_async)
194 )
195 self.sender_receivers_cache.clear()
197 def disconnect(
198 self,
199 receiver: RECEIVER_T | None = None,
200 sender: Any | None = None,
201 dispatch_uid: Hashable | None = None,
202 ) -> bool:
203 """
204 Disconnect receiver from sender for signal.
206 If weak references are used, disconnect need not be called. The receiver
207 will be removed from dispatch automatically.
209 Arguments:
211 receiver
212 The registered receiver to disconnect. May be none if
213 dispatch_uid is specified.
215 sender
216 The registered sender to disconnect
218 dispatch_uid
219 the unique identifier of the receiver to disconnect
220 """
221 lookup_key: LOOKUP_KEY_T
222 if dispatch_uid:
223 lookup_key = (
224 dispatch_uid,
225 _make_id(sender),
226 )
227 else:
228 lookup_key = (_make_id(receiver), _make_id(sender))
230 disconnected: bool = False
231 with self.lock:
232 self._clear_dead_receivers()
233 for index in range(len(self.receivers)):
234 r_key, *_ = self.receivers[index]
235 if r_key == lookup_key:
236 disconnected = True
237 del self.receivers[index]
238 break
239 self.sender_receivers_cache.clear()
240 return disconnected
242 def has_listeners(self, sender: object | None = None) -> bool:
243 sync_receivers, async_receivers = self._live_receivers(sender)
244 return bool(sync_receivers) or bool(async_receivers)
246 def send(
247 self, sender: object, **named: object
248 ) -> list[tuple[RECEIVER_T, object | None]]:
249 """
250 Send signal from sender to all connected receivers.
252 If any receiver raises an error, the error propagates back through send,
253 terminating the dispatch loop. So it's possible that all receivers
254 won't be called if an error is raised.
256 If any receivers are asynchronous, they are called after all the
257 synchronous receivers via a single call to async_to_sync(). They are
258 also executed concurrently with asyncio.gather().
260 Arguments:
262 sender
263 The sender of the signal. Either a specific object or None.
265 named
266 Named arguments which will be passed to receivers.
268 Return a list of tuple pairs [(receiver, response), ... ].
269 """
270 if (
271 not self.receivers
272 or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
273 ):
274 return []
275 responses: list[tuple[RECEIVER_T, Any | None]] = []
276 sync_receivers, async_receivers = self._live_receivers(sender)
277 for receiver in sync_receivers:
278 response = receiver(signal=self, sender=sender, **named)
279 responses.append((receiver, response))
280 if async_receivers:
282 async def asend() -> Iterator[tuple[RECEIVER_T, object | None]]:
283 async_responses = await asyncio.gather(
284 *(
285 receiver(signal=self, sender=sender, **named)
286 for receiver in async_receivers
287 )
288 )
289 return zip(async_receivers, async_responses)
291 responses.extend(async_to_sync(asend)())
292 return responses
294 async def asend(
295 self, sender: object, **named: object
296 ) -> list[tuple[RECEIVER_T, Any | None]]:
297 """
298 Send signal from sender to all connected receivers in async mode.
300 All sync receivers will be wrapped by sync_to_async()
301 If any receiver raises an error, the error propagates back through
302 send, terminating the dispatch loop. So it's possible that all
303 receivers won't be called if an error is raised.
305 If any receivers are synchronous, they are grouped and called behind a
306 sync_to_async() adaption before executing any asynchronous receivers.
308 If any receivers are asynchronous, they are grouped and executed
309 concurrently with asyncio.gather().
311 Arguments:
313 sender
314 The sender of the signal. Either a specific object or None.
316 named
317 Named arguments which will be passed to receivers.
319 Return a list of tuple pairs [(receiver, response), ...].
320 """
321 if (
322 not self.receivers
323 or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
324 ):
325 return []
326 sync_receivers, async_receivers = self._live_receivers(sender)
327 if sync_receivers:
329 @sync_to_async
330 def sync_send() -> list[tuple[RECEIVER_T, object | None]]:
331 responses = []
332 for receiver in sync_receivers:
333 response = receiver(signal=self, sender=sender, **named)
334 responses.append((receiver, response))
335 return responses
337 else:
339 async def sync_send():
340 return []
342 responses, async_responses = await asyncio.gather(
343 sync_send(),
344 asyncio.gather(
345 *(
346 receiver(signal=self, sender=sender, **named)
347 for receiver in async_receivers
348 )
349 ),
350 )
351 responses.extend(zip(async_receivers, async_responses))
352 return responses
354 def _log_robust_failure(self, receiver: RECEIVER_T, err: Exception) -> None:
355 logger.error(
356 "Error calling %s in Signal.send_robust() (%s)",
357 receiver.__qualname__,
358 err,
359 exc_info=err,
360 )
362 def send_robust(
363 self, sender: object, **named: object
364 ) -> list[tuple[RECEIVER_T, Exception | object | None]]:
365 """
366 Send signal from sender to all connected receivers catching errors.
368 If any receivers are asynchronous, they are called after all the
369 synchronous receivers via a single call to async_to_sync(). They are
370 also executed concurrently with asyncio.gather().
372 Arguments:
374 sender
375 The sender of the signal. Can be any Python object (normally one
376 registered with a connect if you actually want something to
377 occur).
379 named
380 Named arguments which will be passed to receivers.
382 Return a list of tuple pairs [(receiver, response), ... ].
384 If any receiver raises an error (specifically any subclass of
385 Exception), return the error instance as the result for that receiver.
386 """
387 if (
388 not self.receivers
389 or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
390 ):
391 return []
393 # Call each receiver with whatever arguments it can accept.
394 # Return a list of tuple pairs [(receiver, response), ... ].
395 responses: list[tuple[RECEIVER_T, Exception | Any | None]] = []
396 sync_receivers, async_receivers = self._live_receivers(sender)
397 for receiver in sync_receivers:
398 try:
399 response: Any = receiver(signal=self, sender=sender, **named)
400 except Exception as err:
401 self._log_robust_failure(receiver, err)
402 responses.append((receiver, err))
403 else:
404 responses.append((receiver, response))
405 if async_receivers:
407 async def asend_and_wrap_exception(receiver: RECEIVER_T) -> object:
408 try:
409 response = await receiver(signal=self, sender=sender, **named)
410 except Exception as err:
411 self._log_robust_failure(receiver, err)
412 return err
413 return response
415 async def asend() -> Iterator[tuple[RECEIVER_T, object | None]]:
416 async_responses = await asyncio.gather(
417 *(
418 asend_and_wrap_exception(receiver)
419 for receiver in async_receivers
420 )
421 )
422 return zip(async_receivers, async_responses)
424 responses.extend(async_to_sync(asend)())
425 return responses
427 async def asend_robust(
428 self, sender: object, **named: object
429 ) -> list[tuple[RECEIVER_T, Exception | object | None]]:
430 """
431 Send signal from sender to all connected receivers catching errors.
433 If any receivers are synchronous, they are grouped and called behind a
434 sync_to_async() adaption before executing any asynchronous receivers.
436 If any receivers are asynchronous, they are grouped and executed
437 concurrently with asyncio.gather.
439 Arguments:
441 sender
442 The sender of the signal. Can be any Python object (normally one
443 registered with a connect if you actually want something to
444 occur).
446 named
447 Named arguments which will be passed to receivers.
449 Return a list of tuple pairs [(receiver, response), ... ].
451 If any receiver raises an error (specifically any subclass of
452 Exception), return the error instance as the result for that receiver.
453 """
454 if (
455 not self.receivers
456 or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
457 ):
458 return []
460 # Call each receiver with whatever arguments it can accept.
461 # Return a list of tuple pairs [(receiver, response), ... ].
462 sync_receivers: list[RECEIVER_T]
463 async_receivers: list[RECEIVER_T]
464 sync_receivers, async_receivers = self._live_receivers(sender)
466 if sync_receivers:
468 @sync_to_async
469 def sync_send() -> list[tuple[RECEIVER_T, object | Exception | None]]:
470 responses: list[tuple[RECEIVER_T, object | Exception]] = []
471 for receiver in sync_receivers:
472 try:
473 response = receiver(signal=self, sender=sender, **named)
474 except Exception as err:
475 self._log_robust_failure(receiver, err)
476 responses.append((receiver, err))
477 else:
478 responses.append((receiver, response))
479 return responses
481 else:
483 async def sync_send():
484 return []
486 async def asend_and_wrap_exception(receiver: RECEIVER_T) -> object | None:
487 try:
488 response = await receiver(signal=self, sender=sender, **named)
489 except Exception as err:
490 self._log_robust_failure(receiver, err)
491 return err
492 return response
494 responses, async_responses = await asyncio.gather(
495 sync_send(),
496 asyncio.gather(
497 *(asend_and_wrap_exception(receiver) for receiver in async_receivers),
498 ),
499 )
500 responses.extend(zip(async_receivers, async_responses))
501 return responses
503 def _clear_dead_receivers(self) -> None:
504 # Note: caller is assumed to hold self.lock.
505 if self._dead_receivers:
506 self._dead_receivers = False
507 self.receivers = [
508 r
509 for r in self.receivers
510 if (
511 not (isinstance(r[1], weakref.ReferenceType) and r[1]() is None)
512 and not (r[2] is not None and r[2]() is None)
513 )
514 ]
516 def _live_receivers(
517 self, sender: object
518 ) -> tuple[list[RECEIVER_T], list[RECEIVER_T]]:
519 """
520 Filter sequence of receivers to get resolved, live receivers.
522 This checks for weak references and resolves them, then returning only
523 live receivers.
524 """
525 type RECEIVER_LIST_T = list[tuple[RECEIVER_T | RECEIVER_REF_T, tuple[int, int] | int, bool]]
526 receivers: (
527 RECEIVER_LIST_T
528 | int
529 | None
530 ) = []
531 if self.use_caching and not self._dead_receivers:
532 receivers = self.sender_receivers_cache.get(sender)
533 # We could end up here with NO_RECEIVERS even if we do check this case in
534 # .send() prior to calling _live_receivers() due to concurrent .send() call.
535 if isinstance(receivers, int):
536 return [], []
538 if not receivers:
539 with self.lock:
540 self._clear_dead_receivers()
541 senderkey = _make_id(sender)
542 receivers = []
543 for (
544 (_receiverkey, r_senderkey),
545 receiver,
546 sender_ref,
547 is_async,
548 ) in self.receivers:
549 if r_senderkey == NONE_ID or r_senderkey == senderkey:
550 receivers.append((receiver, sender_ref, is_async))
551 if self.use_caching:
552 if not receivers:
553 self.sender_receivers_cache[sender] = NO_RECEIVERS
554 else:
555 # Note, we must cache the weakref versions.
556 self.sender_receivers_cache[sender] = receivers
558 non_weak_sync_receivers = []
559 non_weak_async_receivers = []
560 for rec, sender_ref, is_async in cast(RECEIVER_LIST_T, receivers):
561 # Skip if the receiver/sender is a dead weakref
562 if isinstance(rec, weakref.ReferenceType):
563 _receiver: RECEIVER_T | None = rec()
564 if _receiver is None:
565 continue
566 else:
567 _receiver = rec
568 if sender_ref is not None and sender_ref() is None:
569 continue
570 if is_async:
571 non_weak_async_receivers.append(_receiver)
572 else:
573 non_weak_sync_receivers.append(_receiver)
574 return non_weak_sync_receivers, non_weak_async_receivers
576 def _flag_dead_receivers(self, _: weakref.ReferenceType[Any] | None=None) -> None:
577 # Mark that the self.receivers list has dead weakrefs. If so, we will
578 # clean those up in connect, disconnect and _live_receivers while
579 # holding self.lock. Note that doing the cleanup here isn't a good
580 # idea, _flag_dead_receivers() will be called as side effect of garbage
581 # collection, and so the call can happen while we are already holding
582 # self.lock.
583 self._dead_receivers = True
586def receiver(signal: Signal, **kwargs: Any) -> Callable[[Callable], Callable]:
587 """
588 A decorator for connecting receivers to signals. Used by passing in the
589 signal (or list of signals) and keyword arguments to connect::
591 @receiver(post_save, sender=MyModel)
592 def signal_receiver(sender, **kwargs):
593 ...
595 @receiver([post_save, post_delete], sender=MyModel)
596 def signals_receiver(sender, **kwargs):
597 ...
598 """
600 def _decorator(func):
601 if isinstance(signal, (list, tuple)):
602 for s in signal:
603 s.connect(func, **kwargs)
604 else:
605 signal.connect(func, **kwargs)
606 return func
608 return _decorator