Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
base.py622 linesDownload Raw Back to blinker
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 
codekingpro/portable-devtools · Team Ai