codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import collections.abc as c4import typing as t5import warnings6import weakref7from collections import defaultdict8from contextlib import AbstractContextManager9from contextlib import contextmanager10from functools import cached_property11from inspect import iscoroutinefunction12from weakref import WeakValueDictionary13 14from ._utilities import make_id15from ._utilities import make_ref16from ._utilities import Symbol17 18if t.TYPE_CHECKING:19 F = t.TypeVar("F", bound=c.Callable[..., t.Any])20 21ANY = Symbol("ANY")22"""Symbol for "any sender"."""23 24ANY_ID = 025 26 27class Signal:28 """A notification emitter.29 30 :param doc: The docstring for the signal.31 """32 33 ANY = ANY34 """An alias for the :data:`~blinker.ANY` sender symbol."""35 36 set_class: type[set[t.Any]] = set37 """The set class to use for tracking connected receivers and senders.38 Python's ``set`` is unordered. If receivers must be dispatched in the order39 they were connected, an ordered set implementation can be used.40 41 .. versionadded:: 1.742 """43 44 @cached_property45 def receiver_connected(self) -> Signal:46 """Emitted at the end of each :meth:`connect` call.47 48 The signal sender is the signal instance, and the :meth:`connect`49 arguments are passed through: ``receiver``, ``sender``, and ``weak``.50 51 .. versionadded:: 1.252 """53 return Signal(doc="Emitted after a receiver connects.")54 55 @cached_property56 def receiver_disconnected(self) -> Signal:57 """Emitted at the end of each :meth:`disconnect` call.58 59 The sender is the signal instance, and the :meth:`disconnect` arguments60 are passed through: ``receiver`` and ``sender``.61 62 This signal is emitted **only** when :meth:`disconnect` is called63 explicitly. This signal cannot be emitted by an automatic disconnect64 when a weakly referenced receiver or sender goes out of scope, as the65 instance is no longer be available to be used as the sender for this66 signal.67 68 An alternative approach is available by subscribing to69 :attr:`receiver_connected` and setting up a custom weakref cleanup70 callback on weak receivers and senders.71 72 .. versionadded:: 1.273 """74 return Signal(doc="Emitted after a receiver disconnects.")75 76 def __init__(self, doc: str | None = None) -> None:77 if doc:78 self.__doc__ = doc79 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 any84 receivers are connected to the signal: ``if s.receivers:``. The85 structure and data is not part of the public API, but checking its86 boolean value is.87 """88 89 self.is_muted: bool = False90 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]] = {}93 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 by96 ``sender``.97 98 :param receiver: The callable to call when :meth:`send` is called with99 the given ``sender``, passing ``sender`` as a positional argument100 along with any extra keyword arguments.101 :param sender: Any object or :data:`ANY`. ``receiver`` will only be102 called when :meth:`send` is called with this sender. If ``ANY``, the103 receiver will be called for any sender. A receiver may be connected104 to multiple senders by calling :meth:`connect` multiple times.105 :param weak: Track the receiver with a :mod:`weakref`. The receiver will106 be automatically disconnected when it is garbage collected. When107 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)112 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] = receiver119 120 self._by_sender[sender_id].add(receiver_id)121 self._by_receiver[receiver_id].add(sender_id)122 123 if sender is not ANY and sender_id not in self._weak_senders:124 # store a cleanup for weakref-able senders125 try:126 self._weak_senders[sender_id] = make_ref(127 sender, self._make_cleanup_sender(sender_id)128 )129 except TypeError:130 pass131 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=weak136 )137 except TypeError:138 # TODO no explanation or test for this139 self.disconnect(receiver, sender)140 raise141 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=weak146 )147 except TypeError:148 self.disconnect(receiver, sender)149 raise150 151 return receiver152 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 sent155 by ``sender``.156 157 The decorated function will be called when :meth:`send` is called with158 the given ``sender``, passing ``sender`` as a positional argument along159 with any extra keyword arguments.160 161 :param sender: Any object or :data:`ANY`. ``receiver`` will only be162 called when :meth:`send` is called with this sender. If ``ANY``, the163 receiver will be called for any sender. A receiver may be connected164 to multiple senders by calling :meth:`connect` multiple times.165 :param weak: Track the receiver with a :mod:`weakref`. The receiver will166 be automatically disconnected when it is garbage collected. When167 connecting a receiver defined within a function, set to ``False``,168 otherwise it will be disconnected when the function scope ends.=169 170 .. versionadded:: 1.1171 """172 173 def decorator(fn: F) -> F:174 self.connect(fn, sender, weak)175 return fn176 177 return decorator178 179 @contextmanager180 def connected_to(181 self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY182 ) -> c.Generator[None, None, None]:183 """A context manager that temporarily connects ``receiver`` to the184 signal while a ``with`` block executes. When the block exits, the185 receiver is disconnected. Useful for tests.186 187 :param receiver: The callable to call when :meth:`send` is called with188 the given ``sender``, passing ``sender`` as a positional argument189 along with any extra keyword arguments.190 :param sender: Any object or :data:`ANY`. ``receiver`` will only be191 called when :meth:`send` is called with this sender. If ``ANY``, the192 receiver will be called for any sender.193 194 .. versionadded:: 1.1195 """196 self.connect(receiver, sender=sender, weak=False)197 198 try:199 yield None200 finally:201 self.disconnect(receiver)202 203 @contextmanager204 def muted(self) -> c.Generator[None, None, None]:205 """A context manager that temporarily disables the signal. No receivers206 will be called if the signal is sent, until the ``with`` block exits.207 Useful for tests.208 """209 self.is_muted = True210 211 try:212 yield None213 finally:214 self.is_muted = False215 216 def temporarily_connected_to(217 self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY218 ) -> AbstractContextManager[None]:219 """Deprecated alias for :meth:`connected_to`.220 221 .. deprecated:: 1.1222 Renamed to ``connected_to``. Will be removed in Blinker 1.9.223 224 .. versionadded:: 0.9225 """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)233 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 positional247 argument along with any extra keyword arguments. Return a list of248 ``(receiver, return value)`` tuples.249 250 The order receivers are called is undefined, but can be influenced by251 setting :attr:`set_class`.252 253 If a receiver raises an exception, that exception will propagate up.254 This makes debugging straightforward, with an assumption that correctly255 implemented receivers will not raise.256 257 :param sender: Call receivers connected to this sender, in addition to258 those connected to :data:`ANY`.259 :param _async_wrapper: Will be called on any receivers that are async260 coroutines to turn them into sync callables. For example, could run261 the receiver with an event loop.262 :param kwargs: Extra keyword arguments to pass to each receiver.263 264 .. versionchanged:: 1.7265 Added the ``_async_wrapper`` argument.266 """267 if self.is_muted:268 return []269 270 results = []271 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.")276 277 result = _async_wrapper(receiver)(sender, **kwargs)278 else:279 result = receiver(sender, **kwargs)280 281 results.append((receiver, result))282 283 return results284 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 positional298 argument along with any extra keyword arguments. Return a list of299 ``(receiver, return value)`` tuples.300 301 The order receivers are called is undefined, but can be influenced by302 setting :attr:`set_class`.303 304 If a receiver raises an exception, that exception will propagate up.305 This makes debugging straightforward, with an assumption that correctly306 implemented receivers will not raise.307 308 :param sender: Call receivers connected to this sender, in addition to309 those connected to :data:`ANY`.310 :param _sync_wrapper: Will be called on any receivers that are sync311 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.314 315 .. versionadded:: 1.7316 """317 if self.is_muted:318 return []319 320 results = []321 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.")326 327 result = await _sync_wrapper(receiver)(sender, **kwargs)328 else:329 result = await receiver(sender, **kwargs)330 331 results.append((receiver, result))332 333 return results334 335 def has_receivers_for(self, sender: t.Any) -> bool:336 """Check if there is at least one receiver that will be called with the337 given ``sender``. A receiver connected to :data:`ANY` will always be338 called, regardless of sender. Does not check if weakly referenced339 receivers are still live. See :meth:`receivers_for` for a stronger340 search.341 342 :param sender: Check for receivers connected to this sender, in addition343 to those connected to :data:`ANY`.344 """345 if not self.receivers:346 return False347 348 if self._by_sender[ANY_ID]:349 return True350 351 if sender is ANY:352 return False353 354 return make_id(sender) in self._by_sender355 356 def receivers_for(357 self, sender: t.Any358 ) -> c.Generator[c.Callable[..., t.Any], None, None]:359 """Yield each receiver to be called for ``sender``, in addition to those360 to be called for :data:`ANY`. Weakly referenced receivers that are not361 live will be disconnected and skipped.362 363 :param sender: Yield receivers connected to this sender, in addition364 to those connected to :data:`ANY`.365 """366 # TODO: test receivers_for(ANY)367 if not self.receivers:368 return369 370 sender_id = make_id(sender)371 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()376 377 for receiver_id in ids:378 receiver = self.receivers.get(receiver_id)379 380 if receiver is None:381 continue382 383 if isinstance(receiver, weakref.ref):384 strong = receiver()385 386 if strong is None:387 self._disconnect(receiver_id, ANY_ID)388 continue389 390 yield strong391 else:392 yield receiver393 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 by396 ``sender``.397 398 :param receiver: A connected receiver callable.399 :param sender: Disconnect from only this sender. By default, disconnect400 from all senders.401 """402 sender_id: c.Hashable403 404 if sender is ANY:405 sender_id = ANY_ID406 else:407 sender_id = make_id(sender)408 409 receiver_id = make_id(receiver)410 self._disconnect(receiver_id, sender_id)411 412 if (413 "receiver_disconnected" in self.__dict__414 and self.receiver_disconnected.receivers415 ):416 self.receiver_disconnected.send(self, receiver=receiver, sender=sender)417 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)423 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)428 429 def _make_cleanup_receiver(430 self, receiver_id: c.Hashable431 ) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]:432 """Create a callback function to disconnect a weakly referenced433 receiver when it is garbage collected.434 """435 436 def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None:437 self._disconnect(receiver_id, ANY_ID)438 439 return cleanup440 441 def _make_cleanup_sender(442 self, sender_id: c.Hashable443 ) -> c.Callable[[weakref.ref[t.Any]], None]:444 """Create a callback function to disconnect all receivers for a weakly445 referenced sender when it is garbage collected.446 """447 assert sender_id != ANY_ID448 449 def cleanup(ref: weakref.ref[t.Any]) -> None:450 self._weak_senders.pop(sender_id, None)451 452 for receiver_id in self._by_sender.pop(sender_id, ()):453 self._by_receiver[receiver_id].discard(sender_id)454 455 return cleanup456 457 def _cleanup_bookkeeping(self) -> None:458 """Prune unused sender/receiver bookkeeping. Not threadsafe.459 460 Connecting & disconnecting leaves behind a small amount of bookkeeping461 data. Typical workloads using Blinker, for example in most web apps,462 Flask, CLI scripts, etc., are not adversely affected by this463 bookkeeping.464 465 With a long-running process performing dynamic signal routing with high466 volume, e.g. connecting to function closures, senders are all unique467 object instances. Doing all of this over and over may cause memory usage468 to grow due to extraneous bookkeeping. (An empty ``set`` for each stale469 sender/receiver pair.)470 471 This method will prune that bookkeeping away, with the caveat that such472 pruning is not threadsafe. The risk is that cleanup of a fully473 disconnected receiver/sender pair occurs while another thread is474 connecting that same pair. If you are in the highly dynamic, unique475 receiver/sender situation that has lead you to this method, that failure476 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)482 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()489 490 491_receiver_connected = Signal(492 """\493Sent by a :class:`Signal` after a receiver connects.494 495:argument: the Signal that was connected to496:keyword receiver_arg: the connected receiver497:keyword sender_arg: the sender to connect to498:keyword weak_arg: true if the connection to receiver_arg is a weak reference499 500.. deprecated:: 1.2501 Individual signals have their own :attr:`~Signal.receiver_connected` and502 :attr:`~Signal.receiver_disconnected` signals with a slightly simplified503 call signature. This global signal will be removed in Blinker 1.9.504"""505)506 507 508class NamedSignal(Signal):509 """A named generic notification emitter. The name is not used by the signal510 itself, but matches the key in the :class:`Namespace` that it belongs to.511 512 :param name: The name of the signal within the namespace.513 :param doc: The docstring for the signal.514 """515 516 def __init__(self, name: str, doc: str | None = None) -> None:517 super().__init__(doc)518 519 #: The name of this signal.520 self.name: str = name521 522 def __repr__(self) -> str:523 base = super().__repr__()524 return f"{base[:-1]}; {self.name!r}>" # noqa: E702525 526 527if t.TYPE_CHECKING:528 529 class PNamespaceSignal(t.Protocol):530 def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ...531 532 # Python < 3.9533 _NamespaceBase = dict[str, NamedSignal] # type: ignore[misc]534else:535 _NamespaceBase = dict536 537 538class Namespace(_NamespaceBase):539 """A dict mapping names to signals."""540 541 def signal(self, name: str, doc: str | None = None) -> NamedSignal:542 """Return the :class:`NamedSignal` for the given ``name``, creating it543 if required. Repeated calls with the same name return the same signal.544 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)550 551 return self[name]552 553 554class _WeakNamespace(WeakValueDictionary): # type: ignore[type-arg]555 """A weak mapping of names to signals.556 557 Automatically cleans up unused signals when the last reference goes out558 of scope. This namespace implementation provides similar behavior to Blinker559 <= 1.2.560 561 .. deprecated:: 1.3562 Will be removed in Blinker 1.9.563 564 .. versionadded:: 1.3565 """566 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__()575 576 def signal(self, name: str, doc: str | None = None) -> NamedSignal:577 """Return the :class:`NamedSignal` for the given ``name``, creating it578 if required. Repeated calls with the same name return the same signal.579 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)585 586 return self[name] # type: ignore[no-any-return]587 588 589default_namespace: Namespace = Namespace()590"""A default :class:`Namespace` for creating named signals. :func:`signal`591creates a :class:`NamedSignal` in this namespace.592"""593 594signal: PNamespaceSignal = default_namespace.signal595"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given596``name``, creating it if required. Repeated calls with the same name return the597same signal.598"""599 600 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_connected611 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 _WeakNamespace620 621 raise AttributeError(name)622 