Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
base.py513 linesDownload Raw Back to blinker
1from __future__ import annotations2 3import collections.abc as c4import sys5import typing as t6import weakref7from collections import defaultdict8from contextlib import contextmanager9from functools import cached_property10from inspect import iscoroutinefunction11 12from ._utilities import make_id13from ._utilities import make_ref14from ._utilities import Symbol15 16F = t.TypeVar("F", bound=c.Callable[..., t.Any])17 18ANY = Symbol("ANY")19"""Symbol for "any sender"."""20 21ANY_ID = 022 23 24class Signal:25    """A notification emitter.26 27    :param doc: The docstring for the signal.28    """29 30    ANY = ANY31    """An alias for the :data:`~blinker.ANY` sender symbol."""32 33    set_class: type[set[t.Any]] = set34    """The set class to use for tracking connected receivers and senders.35    Python's ``set`` is unordered. If receivers must be dispatched in the order36    they were connected, an ordered set implementation can be used.37 38    .. versionadded:: 1.739    """40 41    @cached_property42    def receiver_connected(self) -> Signal:43        """Emitted at the end of each :meth:`connect` call.44 45        The signal sender is the signal instance, and the :meth:`connect`46        arguments are passed through: ``receiver``, ``sender``, and ``weak``.47 48        .. versionadded:: 1.249        """50        return Signal(doc="Emitted after a receiver connects.")51 52    @cached_property53    def receiver_disconnected(self) -> Signal:54        """Emitted at the end of each :meth:`disconnect` call.55 56        The sender is the signal instance, and the :meth:`disconnect` arguments57        are passed through: ``receiver`` and ``sender``.58 59        This signal is emitted **only** when :meth:`disconnect` is called60        explicitly. This signal cannot be emitted by an automatic disconnect61        when a weakly referenced receiver or sender goes out of scope, as the62        instance is no longer be available to be used as the sender for this63        signal.64 65        An alternative approach is available by subscribing to66        :attr:`receiver_connected` and setting up a custom weakref cleanup67        callback on weak receivers and senders.68 69        .. versionadded:: 1.270        """71        return Signal(doc="Emitted after a receiver disconnects.")72 73    def __init__(self, doc: str | None = None) -> None:74        if doc:75            self.__doc__ = doc76 77        self.receivers: dict[78            t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any]79        ] = {}80        """The map of connected receivers. Useful to quickly check if any81        receivers are connected to the signal: ``if s.receivers:``. The82        structure and data is not part of the public API, but checking its83        boolean value is.84        """85 86        self.is_muted: bool = False87        self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)88        self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)89        self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {}90 91    def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F:92        """Connect ``receiver`` to be called when the signal is sent by93        ``sender``.94 95        :param receiver: The callable to call when :meth:`send` is called with96            the given ``sender``, passing ``sender`` as a positional argument97            along with any extra keyword arguments.98        :param sender: Any object or :data:`ANY`. ``receiver`` will only be99            called when :meth:`send` is called with this sender. If ``ANY``, the100            receiver will be called for any sender. A receiver may be connected101            to multiple senders by calling :meth:`connect` multiple times.102        :param weak: Track the receiver with a :mod:`weakref`. The receiver will103            be automatically disconnected when it is garbage collected. When104            connecting a receiver defined within a function, set to ``False``,105            otherwise it will be disconnected when the function scope ends.106        """107        receiver_id = make_id(receiver)108        sender_id = ANY_ID if sender is ANY else make_id(sender)109 110        if weak:111            self.receivers[receiver_id] = make_ref(112                receiver, self._make_cleanup_receiver(receiver_id)113            )114        else:115            self.receivers[receiver_id] = receiver116 117        self._by_sender[sender_id].add(receiver_id)118        self._by_receiver[receiver_id].add(sender_id)119 120        if sender is not ANY and sender_id not in self._weak_senders:121            # store a cleanup for weakref-able senders122            try:123                self._weak_senders[sender_id] = make_ref(124                    sender, self._make_cleanup_sender(sender_id)125                )126            except TypeError:127                pass128 129        if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers:130            try:131                self.receiver_connected.send(132                    self, receiver=receiver, sender=sender, weak=weak133                )134            except TypeError:135                # TODO no explanation or test for this136                self.disconnect(receiver, sender)137                raise138 139        return receiver140 141    def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]:142        """Connect the decorated function to be called when the signal is sent143        by ``sender``.144 145        The decorated function will be called when :meth:`send` is called with146        the given ``sender``, passing ``sender`` as a positional argument along147        with any extra keyword arguments.148 149        :param sender: Any object or :data:`ANY`. ``receiver`` will only be150            called when :meth:`send` is called with this sender. If ``ANY``, the151            receiver will be called for any sender. A receiver may be connected152            to multiple senders by calling :meth:`connect` multiple times.153        :param weak: Track the receiver with a :mod:`weakref`. The receiver will154            be automatically disconnected when it is garbage collected. When155            connecting a receiver defined within a function, set to ``False``,156            otherwise it will be disconnected when the function scope ends.=157 158        .. versionadded:: 1.1159        """160 161        def decorator(fn: F) -> F:162            self.connect(fn, sender, weak)163            return fn164 165        return decorator166 167    @contextmanager168    def connected_to(169        self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY170    ) -> c.Generator[None, None, None]:171        """A context manager that temporarily connects ``receiver`` to the172        signal while a ``with`` block executes. When the block exits, the173        receiver is disconnected. Useful for tests.174 175        :param receiver: The callable to call when :meth:`send` is called with176            the given ``sender``, passing ``sender`` as a positional argument177            along with any extra keyword arguments.178        :param sender: Any object or :data:`ANY`. ``receiver`` will only be179            called when :meth:`send` is called with this sender. If ``ANY``, the180            receiver will be called for any sender.181 182        .. versionadded:: 1.1183        """184        self.connect(receiver, sender=sender, weak=False)185 186        try:187            yield None188        finally:189            self.disconnect(receiver)190 191    @contextmanager192    def muted(self) -> c.Generator[None, None, None]:193        """A context manager that temporarily disables the signal. No receivers194        will be called if the signal is sent, until the ``with`` block exits.195        Useful for tests.196        """197        self.is_muted = True198 199        try:200            yield None201        finally:202            self.is_muted = False203 204    def send(205        self,206        sender: t.Any | None = None,207        /,208        *,209        _async_wrapper: c.Callable[210            [c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any]211        ]212        | None = None,213        **kwargs: t.Any,214    ) -> list[tuple[c.Callable[..., t.Any], t.Any]]:215        """Call all receivers that are connected to the given ``sender``216        or :data:`ANY`. Each receiver is called with ``sender`` as a positional217        argument along with any extra keyword arguments. Return a list of218        ``(receiver, return value)`` tuples.219 220        The order receivers are called is undefined, but can be influenced by221        setting :attr:`set_class`.222 223        If a receiver raises an exception, that exception will propagate up.224        This makes debugging straightforward, with an assumption that correctly225        implemented receivers will not raise.226 227        :param sender: Call receivers connected to this sender, in addition to228            those connected to :data:`ANY`.229        :param _async_wrapper: Will be called on any receivers that are async230            coroutines to turn them into sync callables. For example, could run231            the receiver with an event loop.232        :param kwargs: Extra keyword arguments to pass to each receiver.233 234        .. versionchanged:: 1.7235            Added the ``_async_wrapper`` argument.236        """237        if self.is_muted:238            return []239 240        results = []241 242        for receiver in self.receivers_for(sender):243            if iscoroutinefunction(receiver):244                if _async_wrapper is None:245                    raise RuntimeError("Cannot send to a coroutine function.")246 247                result = _async_wrapper(receiver)(sender, **kwargs)248            else:249                result = receiver(sender, **kwargs)250 251            results.append((receiver, result))252 253        return results254 255    async def send_async(256        self,257        sender: t.Any | None = None,258        /,259        *,260        _sync_wrapper: c.Callable[261            [c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]262        ]263        | None = None,264        **kwargs: t.Any,265    ) -> list[tuple[c.Callable[..., t.Any], t.Any]]:266        """Await all receivers that are connected to the given ``sender``267        or :data:`ANY`. Each receiver is called with ``sender`` as a positional268        argument along with any extra keyword arguments. Return a list of269        ``(receiver, return value)`` tuples.270 271        The order receivers are called is undefined, but can be influenced by272        setting :attr:`set_class`.273 274        If a receiver raises an exception, that exception will propagate up.275        This makes debugging straightforward, with an assumption that correctly276        implemented receivers will not raise.277 278        :param sender: Call receivers connected to this sender, in addition to279            those connected to :data:`ANY`.280        :param _sync_wrapper: Will be called on any receivers that are sync281            callables to turn them into async coroutines. For example,282            could call the receiver in a thread.283        :param kwargs: Extra keyword arguments to pass to each receiver.284 285        .. versionadded:: 1.7286        """287        if self.is_muted:288            return []289 290        results = []291 292        for receiver in self.receivers_for(sender):293            if not iscoroutinefunction(receiver):294                if _sync_wrapper is None:295                    raise RuntimeError("Cannot send to a non-coroutine function.")296 297                result = await _sync_wrapper(receiver)(sender, **kwargs)298            else:299                result = await receiver(sender, **kwargs)300 301            results.append((receiver, result))302 303        return results304 305    def has_receivers_for(self, sender: t.Any) -> bool:306        """Check if there is at least one receiver that will be called with the307        given ``sender``. A receiver connected to :data:`ANY` will always be308        called, regardless of sender. Does not check if weakly referenced309        receivers are still live. See :meth:`receivers_for` for a stronger310        search.311 312        :param sender: Check for receivers connected to this sender, in addition313            to those connected to :data:`ANY`.314        """315        if not self.receivers:316            return False317 318        if self._by_sender[ANY_ID]:319            return True320 321        if sender is ANY:322            return False323 324        return make_id(sender) in self._by_sender325 326    def receivers_for(327        self, sender: t.Any328    ) -> c.Generator[c.Callable[..., t.Any], None, None]:329        """Yield each receiver to be called for ``sender``, in addition to those330        to be called for :data:`ANY`. Weakly referenced receivers that are not331        live will be disconnected and skipped.332 333        :param sender: Yield receivers connected to this sender, in addition334            to those connected to :data:`ANY`.335        """336        # TODO: test receivers_for(ANY)337        if not self.receivers:338            return339 340        sender_id = make_id(sender)341 342        if sender_id in self._by_sender:343            ids = self._by_sender[ANY_ID] | self._by_sender[sender_id]344        else:345            ids = self._by_sender[ANY_ID].copy()346 347        for receiver_id in ids:348            receiver = self.receivers.get(receiver_id)349 350            if receiver is None:351                continue352 353            if isinstance(receiver, weakref.ref):354                strong = receiver()355 356                if strong is None:357                    self._disconnect(receiver_id, ANY_ID)358                    continue359 360                yield strong361            else:362                yield receiver363 364    def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None:365        """Disconnect ``receiver`` from being called when the signal is sent by366        ``sender``.367 368        :param receiver: A connected receiver callable.369        :param sender: Disconnect from only this sender. By default, disconnect370            from all senders.371        """372        sender_id: c.Hashable373 374        if sender is ANY:375            sender_id = ANY_ID376        else:377            sender_id = make_id(sender)378 379        receiver_id = make_id(receiver)380        self._disconnect(receiver_id, sender_id)381 382        if (383            "receiver_disconnected" in self.__dict__384            and self.receiver_disconnected.receivers385        ):386            self.receiver_disconnected.send(self, receiver=receiver, sender=sender)387 388    def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None:389        if sender_id == ANY_ID:390            if self._by_receiver.pop(receiver_id, None) is not None:391                for bucket in self._by_sender.values():392                    bucket.discard(receiver_id)393 394            self.receivers.pop(receiver_id, None)395        else:396            self._by_sender[sender_id].discard(receiver_id)397            self._by_receiver[receiver_id].discard(sender_id)398 399    def _make_cleanup_receiver(400        self, receiver_id: c.Hashable401    ) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]:402        """Create a callback function to disconnect a weakly referenced403        receiver when it is garbage collected.404        """405 406        def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None:407            # If the interpreter is shutting down, disconnecting can result in a408            # weird ignored exception. Don't call it in that case.409            if not sys.is_finalizing():410                self._disconnect(receiver_id, ANY_ID)411 412        return cleanup413 414    def _make_cleanup_sender(415        self, sender_id: c.Hashable416    ) -> c.Callable[[weakref.ref[t.Any]], None]:417        """Create a callback function to disconnect all receivers for a weakly418        referenced sender when it is garbage collected.419        """420        assert sender_id != ANY_ID421 422        def cleanup(ref: weakref.ref[t.Any]) -> None:423            self._weak_senders.pop(sender_id, None)424 425            for receiver_id in self._by_sender.pop(sender_id, ()):426                self._by_receiver[receiver_id].discard(sender_id)427 428        return cleanup429 430    def _cleanup_bookkeeping(self) -> None:431        """Prune unused sender/receiver bookkeeping. Not threadsafe.432 433        Connecting & disconnecting leaves behind a small amount of bookkeeping434        data. Typical workloads using Blinker, for example in most web apps,435        Flask, CLI scripts, etc., are not adversely affected by this436        bookkeeping.437 438        With a long-running process performing dynamic signal routing with high439        volume, e.g. connecting to function closures, senders are all unique440        object instances. Doing all of this over and over may cause memory usage441        to grow due to extraneous bookkeeping. (An empty ``set`` for each stale442        sender/receiver pair.)443 444        This method will prune that bookkeeping away, with the caveat that such445        pruning is not threadsafe. The risk is that cleanup of a fully446        disconnected receiver/sender pair occurs while another thread is447        connecting that same pair. If you are in the highly dynamic, unique448        receiver/sender situation that has lead you to this method, that failure449        mode is perhaps not a big deal for you.450        """451        for mapping in (self._by_sender, self._by_receiver):452            for ident, bucket in list(mapping.items()):453                if not bucket:454                    mapping.pop(ident, None)455 456    def _clear_state(self) -> None:457        """Disconnect all receivers and senders. Useful for tests."""458        self._weak_senders.clear()459        self.receivers.clear()460        self._by_sender.clear()461        self._by_receiver.clear()462 463 464class NamedSignal(Signal):465    """A named generic notification emitter. The name is not used by the signal466    itself, but matches the key in the :class:`Namespace` that it belongs to.467 468    :param name: The name of the signal within the namespace.469    :param doc: The docstring for the signal.470    """471 472    def __init__(self, name: str, doc: str | None = None) -> None:473        super().__init__(doc)474 475        #: The name of this signal.476        self.name: str = name477 478    def __repr__(self) -> str:479        base = super().__repr__()480        return f"{base[:-1]}; {self.name!r}>"  # noqa: E702481 482 483class Namespace(dict[str, NamedSignal]):484    """A dict mapping names to signals."""485 486    def signal(self, name: str, doc: str | None = None) -> NamedSignal:487        """Return the :class:`NamedSignal` for the given ``name``, creating it488        if required. Repeated calls with the same name return the same signal.489 490        :param name: The name of the signal.491        :param doc: The docstring of the signal.492        """493        if name not in self:494            self[name] = NamedSignal(name, doc)495 496        return self[name]497 498 499class _PNamespaceSignal(t.Protocol):500    def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ...501 502 503default_namespace: Namespace = Namespace()504"""A default :class:`Namespace` for creating named signals. :func:`signal`505creates a :class:`NamedSignal` in this namespace.506"""507 508signal: _PNamespaceSignal = default_namespace.signal509"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given510``name``, creating it if required. Repeated calls with the same name return the511same signal.512"""513 
codekingpro/portable-devtools · Team Ai