Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/blinker/base.py: 35%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import collections.abc as c
4import typing as t
5import warnings
6import weakref
7from collections import defaultdict
8from contextlib import AbstractContextManager
9from contextlib import contextmanager
10from functools import cached_property
11from inspect import iscoroutinefunction
12from weakref import WeakValueDictionary
14from ._utilities import make_id
15from ._utilities import make_ref
16from ._utilities import Symbol
18if t.TYPE_CHECKING:
19 F = t.TypeVar("F", bound=c.Callable[..., t.Any])
21ANY = Symbol("ANY")
22"""Symbol for "any sender"."""
24ANY_ID = 0
27class Signal:
28 """A notification emitter.
30 :param doc: The docstring for the signal.
31 """
33 ANY = ANY
34 """An alias for the :data:`~blinker.ANY` sender symbol."""
36 set_class: type[set[t.Any]] = set
37 """The set class to use for tracking connected receivers and senders.
38 Python's ``set`` is unordered. If receivers must be dispatched in the order
39 they were connected, an ordered set implementation can be used.
41 .. versionadded:: 1.7
42 """
44 @cached_property
45 def receiver_connected(self) -> Signal:
46 """Emitted at the end of each :meth:`connect` call.
48 The signal sender is the signal instance, and the :meth:`connect`
49 arguments are passed through: ``receiver``, ``sender``, and ``weak``.
51 .. versionadded:: 1.2
52 """
53 return Signal(doc="Emitted after a receiver connects.")
55 @cached_property
56 def receiver_disconnected(self) -> Signal:
57 """Emitted at the end of each :meth:`disconnect` call.
59 The sender is the signal instance, and the :meth:`disconnect` arguments
60 are passed through: ``receiver`` and ``sender``.
62 This signal is emitted **only** when :meth:`disconnect` is called
63 explicitly. This signal cannot be emitted by an automatic disconnect
64 when a weakly referenced receiver or sender goes out of scope, as the
65 instance is no longer be available to be used as the sender for this
66 signal.
68 An alternative approach is available by subscribing to
69 :attr:`receiver_connected` and setting up a custom weakref cleanup
70 callback on weak receivers and senders.
72 .. versionadded:: 1.2
73 """
74 return Signal(doc="Emitted after a receiver disconnects.")
76 def __init__(self, doc: str | None = None) -> None:
77 if doc:
78 self.__doc__ = doc
80 self.receivers: dict[
81 t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any]
82 ] = {}
83 """The map of connected receivers. Useful to quickly check if any
84 receivers are connected to the signal: ``if s.receivers:``. The
85 structure and data is not part of the public API, but checking its
86 boolean value is.
87 """
89 self.is_muted: bool = False
90 self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
91 self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
92 self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {}
94 def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F:
95 """Connect ``receiver`` to be called when the signal is sent by
96 ``sender``.
98 :param receiver: The callable to call when :meth:`send` is called with
99 the given ``sender``, passing ``sender`` as a positional argument
100 along with any extra keyword arguments.
101 :param sender: Any object or :data:`ANY`. ``receiver`` will only be
102 called when :meth:`send` is called with this sender. If ``ANY``, the
103 receiver will be called for any sender. A receiver may be connected
104 to multiple senders by calling :meth:`connect` multiple times.
105 :param weak: Track the receiver with a :mod:`weakref`. The receiver will
106 be automatically disconnected when it is garbage collected. When
107 connecting a receiver defined within a function, set to ``False``,
108 otherwise it will be disconnected when the function scope ends.
109 """
110 receiver_id = make_id(receiver)
111 sender_id = ANY_ID if sender is ANY else make_id(sender)
113 if weak:
114 self.receivers[receiver_id] = make_ref(
115 receiver, self._make_cleanup_receiver(receiver_id)
116 )
117 else:
118 self.receivers[receiver_id] = receiver
120 self._by_sender[sender_id].add(receiver_id)
121 self._by_receiver[receiver_id].add(sender_id)
123 if sender is not ANY and sender_id not in self._weak_senders:
124 # store a cleanup for weakref-able senders
125 try:
126 self._weak_senders[sender_id] = make_ref(
127 sender, self._make_cleanup_sender(sender_id)
128 )
129 except TypeError:
130 pass
132 if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers:
133 try:
134 self.receiver_connected.send(
135 self, receiver=receiver, sender=sender, weak=weak
136 )
137 except TypeError:
138 # TODO no explanation or test for this
139 self.disconnect(receiver, sender)
140 raise
142 if _receiver_connected.receivers and self is not _receiver_connected:
143 try:
144 _receiver_connected.send(
145 self, receiver_arg=receiver, sender_arg=sender, weak_arg=weak
146 )
147 except TypeError:
148 self.disconnect(receiver, sender)
149 raise
151 return receiver
153 def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]:
154 """Connect the decorated function to be called when the signal is sent
155 by ``sender``.
157 The decorated function will be called when :meth:`send` is called with
158 the given ``sender``, passing ``sender`` as a positional argument along
159 with any extra keyword arguments.
161 :param sender: Any object or :data:`ANY`. ``receiver`` will only be
162 called when :meth:`send` is called with this sender. If ``ANY``, the
163 receiver will be called for any sender. A receiver may be connected
164 to multiple senders by calling :meth:`connect` multiple times.
165 :param weak: Track the receiver with a :mod:`weakref`. The receiver will
166 be automatically disconnected when it is garbage collected. When
167 connecting a receiver defined within a function, set to ``False``,
168 otherwise it will be disconnected when the function scope ends.=
170 .. versionadded:: 1.1
171 """
173 def decorator(fn: F) -> F:
174 self.connect(fn, sender, weak)
175 return fn
177 return decorator
179 @contextmanager
180 def connected_to(
181 self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY
182 ) -> c.Generator[None, None, None]:
183 """A context manager that temporarily connects ``receiver`` to the
184 signal while a ``with`` block executes. When the block exits, the
185 receiver is disconnected. Useful for tests.
187 :param receiver: The callable to call when :meth:`send` is called with
188 the given ``sender``, passing ``sender`` as a positional argument
189 along with any extra keyword arguments.
190 :param sender: Any object or :data:`ANY`. ``receiver`` will only be
191 called when :meth:`send` is called with this sender. If ``ANY``, the
192 receiver will be called for any sender.
194 .. versionadded:: 1.1
195 """
196 self.connect(receiver, sender=sender, weak=False)
198 try:
199 yield None
200 finally:
201 self.disconnect(receiver)
203 @contextmanager
204 def muted(self) -> c.Generator[None, None, None]:
205 """A context manager that temporarily disables the signal. No receivers
206 will be called if the signal is sent, until the ``with`` block exits.
207 Useful for tests.
208 """
209 self.is_muted = True
211 try:
212 yield None
213 finally:
214 self.is_muted = False
216 def temporarily_connected_to(
217 self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY
218 ) -> AbstractContextManager[None]:
219 """Deprecated alias for :meth:`connected_to`.
221 .. deprecated:: 1.1
222 Renamed to ``connected_to``. Will be removed in Blinker 1.9.
224 .. versionadded:: 0.9
225 """
226 warnings.warn(
227 "'temporarily_connected_to' is renamed to 'connected_to'. The old name is"
228 " deprecated and will be removed in Blinker 1.9.",
229 DeprecationWarning,
230 stacklevel=2,
231 )
232 return self.connected_to(receiver, sender)
234 def send(
235 self,
236 sender: t.Any | None = None,
237 /,
238 *,
239 _async_wrapper: c.Callable[
240 [c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any]
241 ]
242 | None = None,
243 **kwargs: t.Any,
244 ) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
245 """Call all receivers that are connected to the given ``sender``
246 or :data:`ANY`. Each receiver is called with ``sender`` as a positional
247 argument along with any extra keyword arguments. Return a list of
248 ``(receiver, return value)`` tuples.
250 The order receivers are called is undefined, but can be influenced by
251 setting :attr:`set_class`.
253 If a receiver raises an exception, that exception will propagate up.
254 This makes debugging straightforward, with an assumption that correctly
255 implemented receivers will not raise.
257 :param sender: Call receivers connected to this sender, in addition to
258 those connected to :data:`ANY`.
259 :param _async_wrapper: Will be called on any receivers that are async
260 coroutines to turn them into sync callables. For example, could run
261 the receiver with an event loop.
262 :param kwargs: Extra keyword arguments to pass to each receiver.
264 .. versionchanged:: 1.7
265 Added the ``_async_wrapper`` argument.
266 """
267 if self.is_muted:
268 return []
270 results = []
272 for receiver in self.receivers_for(sender):
273 if iscoroutinefunction(receiver):
274 if _async_wrapper is None:
275 raise RuntimeError("Cannot send to a coroutine function.")
277 result = _async_wrapper(receiver)(sender, **kwargs)
278 else:
279 result = receiver(sender, **kwargs)
281 results.append((receiver, result))
283 return results
285 async def send_async(
286 self,
287 sender: t.Any | None = None,
288 /,
289 *,
290 _sync_wrapper: c.Callable[
291 [c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]
292 ]
293 | None = None,
294 **kwargs: t.Any,
295 ) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
296 """Await all receivers that are connected to the given ``sender``
297 or :data:`ANY`. Each receiver is called with ``sender`` as a positional
298 argument along with any extra keyword arguments. Return a list of
299 ``(receiver, return value)`` tuples.
301 The order receivers are called is undefined, but can be influenced by
302 setting :attr:`set_class`.
304 If a receiver raises an exception, that exception will propagate up.
305 This makes debugging straightforward, with an assumption that correctly
306 implemented receivers will not raise.
308 :param sender: Call receivers connected to this sender, in addition to
309 those connected to :data:`ANY`.
310 :param _sync_wrapper: Will be called on any receivers that are sync
311 callables to turn them into async coroutines. For example,
312 could call the receiver in a thread.
313 :param kwargs: Extra keyword arguments to pass to each receiver.
315 .. versionadded:: 1.7
316 """
317 if self.is_muted:
318 return []
320 results = []
322 for receiver in self.receivers_for(sender):
323 if not iscoroutinefunction(receiver):
324 if _sync_wrapper is None:
325 raise RuntimeError("Cannot send to a non-coroutine function.")
327 result = await _sync_wrapper(receiver)(sender, **kwargs)
328 else:
329 result = await receiver(sender, **kwargs)
331 results.append((receiver, result))
333 return results
335 def has_receivers_for(self, sender: t.Any) -> bool:
336 """Check if there is at least one receiver that will be called with the
337 given ``sender``. A receiver connected to :data:`ANY` will always be
338 called, regardless of sender. Does not check if weakly referenced
339 receivers are still live. See :meth:`receivers_for` for a stronger
340 search.
342 :param sender: Check for receivers connected to this sender, in addition
343 to those connected to :data:`ANY`.
344 """
345 if not self.receivers:
346 return False
348 if self._by_sender[ANY_ID]:
349 return True
351 if sender is ANY:
352 return False
354 return make_id(sender) in self._by_sender
356 def receivers_for(
357 self, sender: t.Any
358 ) -> c.Generator[c.Callable[..., t.Any], None, None]:
359 """Yield each receiver to be called for ``sender``, in addition to those
360 to be called for :data:`ANY`. Weakly referenced receivers that are not
361 live will be disconnected and skipped.
363 :param sender: Yield receivers connected to this sender, in addition
364 to those connected to :data:`ANY`.
365 """
366 # TODO: test receivers_for(ANY)
367 if not self.receivers:
368 return
370 sender_id = make_id(sender)
372 if sender_id in self._by_sender:
373 ids = self._by_sender[ANY_ID] | self._by_sender[sender_id]
374 else:
375 ids = self._by_sender[ANY_ID].copy()
377 for receiver_id in ids:
378 receiver = self.receivers.get(receiver_id)
380 if receiver is None:
381 continue
383 if isinstance(receiver, weakref.ref):
384 strong = receiver()
386 if strong is None:
387 self._disconnect(receiver_id, ANY_ID)
388 continue
390 yield strong
391 else:
392 yield receiver
394 def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None:
395 """Disconnect ``receiver`` from being called when the signal is sent by
396 ``sender``.
398 :param receiver: A connected receiver callable.
399 :param sender: Disconnect from only this sender. By default, disconnect
400 from all senders.
401 """
402 sender_id: c.Hashable
404 if sender is ANY:
405 sender_id = ANY_ID
406 else:
407 sender_id = make_id(sender)
409 receiver_id = make_id(receiver)
410 self._disconnect(receiver_id, sender_id)
412 if (
413 "receiver_disconnected" in self.__dict__
414 and self.receiver_disconnected.receivers
415 ):
416 self.receiver_disconnected.send(self, receiver=receiver, sender=sender)
418 def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None:
419 if sender_id == ANY_ID:
420 if self._by_receiver.pop(receiver_id, None) is not None:
421 for bucket in self._by_sender.values():
422 bucket.discard(receiver_id)
424 self.receivers.pop(receiver_id, None)
425 else:
426 self._by_sender[sender_id].discard(receiver_id)
427 self._by_receiver[receiver_id].discard(sender_id)
429 def _make_cleanup_receiver(
430 self, receiver_id: c.Hashable
431 ) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]:
432 """Create a callback function to disconnect a weakly referenced
433 receiver when it is garbage collected.
434 """
436 def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None:
437 self._disconnect(receiver_id, ANY_ID)
439 return cleanup
441 def _make_cleanup_sender(
442 self, sender_id: c.Hashable
443 ) -> c.Callable[[weakref.ref[t.Any]], None]:
444 """Create a callback function to disconnect all receivers for a weakly
445 referenced sender when it is garbage collected.
446 """
447 assert sender_id != ANY_ID
449 def cleanup(ref: weakref.ref[t.Any]) -> None:
450 self._weak_senders.pop(sender_id, None)
452 for receiver_id in self._by_sender.pop(sender_id, ()):
453 self._by_receiver[receiver_id].discard(sender_id)
455 return cleanup
457 def _cleanup_bookkeeping(self) -> None:
458 """Prune unused sender/receiver bookkeeping. Not threadsafe.
460 Connecting & disconnecting leaves behind a small amount of bookkeeping
461 data. Typical workloads using Blinker, for example in most web apps,
462 Flask, CLI scripts, etc., are not adversely affected by this
463 bookkeeping.
465 With a long-running process performing dynamic signal routing with high
466 volume, e.g. connecting to function closures, senders are all unique
467 object instances. Doing all of this over and over may cause memory usage
468 to grow due to extraneous bookkeeping. (An empty ``set`` for each stale
469 sender/receiver pair.)
471 This method will prune that bookkeeping away, with the caveat that such
472 pruning is not threadsafe. The risk is that cleanup of a fully
473 disconnected receiver/sender pair occurs while another thread is
474 connecting that same pair. If you are in the highly dynamic, unique
475 receiver/sender situation that has lead you to this method, that failure
476 mode is perhaps not a big deal for you.
477 """
478 for mapping in (self._by_sender, self._by_receiver):
479 for ident, bucket in list(mapping.items()):
480 if not bucket:
481 mapping.pop(ident, None)
483 def _clear_state(self) -> None:
484 """Disconnect all receivers and senders. Useful for tests."""
485 self._weak_senders.clear()
486 self.receivers.clear()
487 self._by_sender.clear()
488 self._by_receiver.clear()
491_receiver_connected = Signal(
492 """\
493Sent by a :class:`Signal` after a receiver connects.
495:argument: the Signal that was connected to
496:keyword receiver_arg: the connected receiver
497:keyword sender_arg: the sender to connect to
498:keyword weak_arg: true if the connection to receiver_arg is a weak reference
500.. deprecated:: 1.2
501 Individual signals have their own :attr:`~Signal.receiver_connected` and
502 :attr:`~Signal.receiver_disconnected` signals with a slightly simplified
503 call signature. This global signal will be removed in Blinker 1.9.
504"""
505)
508class NamedSignal(Signal):
509 """A named generic notification emitter. The name is not used by the signal
510 itself, but matches the key in the :class:`Namespace` that it belongs to.
512 :param name: The name of the signal within the namespace.
513 :param doc: The docstring for the signal.
514 """
516 def __init__(self, name: str, doc: str | None = None) -> None:
517 super().__init__(doc)
519 #: The name of this signal.
520 self.name: str = name
522 def __repr__(self) -> str:
523 base = super().__repr__()
524 return f"{base[:-1]}; {self.name!r}>" # noqa: E702
527if t.TYPE_CHECKING:
529 class PNamespaceSignal(t.Protocol):
530 def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ...
532 # Python < 3.9
533 _NamespaceBase = dict[str, NamedSignal] # type: ignore[misc]
534else:
535 _NamespaceBase = dict
538class Namespace(_NamespaceBase):
539 """A dict mapping names to signals."""
541 def signal(self, name: str, doc: str | None = None) -> NamedSignal:
542 """Return the :class:`NamedSignal` for the given ``name``, creating it
543 if required. Repeated calls with the same name return the same signal.
545 :param name: The name of the signal.
546 :param doc: The docstring of the signal.
547 """
548 if name not in self:
549 self[name] = NamedSignal(name, doc)
551 return self[name]
554class _WeakNamespace(WeakValueDictionary): # type: ignore[type-arg]
555 """A weak mapping of names to signals.
557 Automatically cleans up unused signals when the last reference goes out
558 of scope. This namespace implementation provides similar behavior to Blinker
559 <= 1.2.
561 .. deprecated:: 1.3
562 Will be removed in Blinker 1.9.
564 .. versionadded:: 1.3
565 """
567 def __init__(self) -> None:
568 warnings.warn(
569 "'WeakNamespace' is deprecated and will be removed in Blinker 1.9."
570 " Use 'Namespace' instead.",
571 DeprecationWarning,
572 stacklevel=2,
573 )
574 super().__init__()
576 def signal(self, name: str, doc: str | None = None) -> NamedSignal:
577 """Return the :class:`NamedSignal` for the given ``name``, creating it
578 if required. Repeated calls with the same name return the same signal.
580 :param name: The name of the signal.
581 :param doc: The docstring of the signal.
582 """
583 if name not in self:
584 self[name] = NamedSignal(name, doc)
586 return self[name] # type: ignore[no-any-return]
589default_namespace: Namespace = Namespace()
590"""A default :class:`Namespace` for creating named signals. :func:`signal`
591creates a :class:`NamedSignal` in this namespace.
592"""
594signal: PNamespaceSignal = default_namespace.signal
595"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given
596``name``, creating it if required. Repeated calls with the same name return the
597same signal.
598"""
601def __getattr__(name: str) -> t.Any:
602 if name == "receiver_connected":
603 warnings.warn(
604 "The global 'receiver_connected' signal is deprecated and will be"
605 " removed in Blinker 1.9. Use 'Signal.receiver_connected' and"
606 " 'Signal.receiver_disconnected' instead.",
607 DeprecationWarning,
608 stacklevel=2,
609 )
610 return _receiver_connected
612 if name == "WeakNamespace":
613 warnings.warn(
614 "'WeakNamespace' is deprecated and will be removed in Blinker 1.9."
615 " Use 'Namespace' instead.",
616 DeprecationWarning,
617 stacklevel=2,
618 )
619 return _WeakNamespace
621 raise AttributeError(name)