diff options
author | robot-piglet <robot-piglet@yandex-team.com> | 2024-05-13 00:00:59 +0300 |
---|---|---|
committer | robot-piglet <robot-piglet@yandex-team.com> | 2024-05-13 00:06:28 +0300 |
commit | d6b290c39d078c44e88669fcde8da65fb4d2a03f (patch) | |
tree | 27b34ae7cbc09cec622689aea9222252cecc18cf | |
parent | d502666ab38ad7f8b15bc6e839fd00a4d07cd274 (diff) | |
download | ydb-d6b290c39d078c44e88669fcde8da65fb4d2a03f.tar.gz |
Intermediate changes
-rw-r--r-- | contrib/python/blinker/py3/.dist-info/METADATA | 84 | ||||
-rw-r--r-- | contrib/python/blinker/py3/LICENSE.txt (renamed from contrib/python/blinker/py3/LICENSE.rst) | 0 | ||||
-rw-r--r-- | contrib/python/blinker/py3/README.md | 43 | ||||
-rw-r--r-- | contrib/python/blinker/py3/README.rst | 40 | ||||
-rw-r--r-- | contrib/python/blinker/py3/blinker/__init__.py | 61 | ||||
-rw-r--r-- | contrib/python/blinker/py3/blinker/_utilities.py | 107 | ||||
-rw-r--r-- | contrib/python/blinker/py3/blinker/base.py | 365 | ||||
-rw-r--r-- | contrib/python/blinker/py3/ya.make | 2 |
8 files changed, 362 insertions, 340 deletions
diff --git a/contrib/python/blinker/py3/.dist-info/METADATA b/contrib/python/blinker/py3/.dist-info/METADATA index f96613c4b8..2eb62efbef 100644 --- a/contrib/python/blinker/py3/.dist-info/METADATA +++ b/contrib/python/blinker/py3/.dist-info/METADATA @@ -1,62 +1,60 @@ Metadata-Version: 2.1 Name: blinker -Version: 1.7.0 +Version: 1.8.0 Summary: Fast, simple object-to-object and broadcast signaling -Keywords: signal,emit,events,broadcast -Author-email: Jason Kirtland <jek@discorporate.us> +Author: Jason Kirtland Maintainer-email: Pallets Ecosystem <contact@palletsprojects.com> Requires-Python: >=3.8 -Description-Content-Type: text/x-rst +Description-Content-Type: text/markdown Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python -Classifier: Topic :: Software Development :: Libraries +Classifier: Typing :: Typed Project-URL: Chat, https://discord.gg/pallets Project-URL: Documentation, https://blinker.readthedocs.io -Project-URL: Homepage, https://blinker.readthedocs.io -Project-URL: Issue Tracker, https://github.com/pallets-eco/blinker/issues/ -Project-URL: Source Code, https://github.com/pallets-eco/blinker/ +Project-URL: Source, https://github.com/pallets-eco/blinker/ -Blinker -======= +# Blinker Blinker provides a fast dispatching system that allows any number of interested parties to subscribe to events, or "signals". + +## Pallets Community Ecosystem + +> [!IMPORTANT]\ +> This project is part of the Pallets Community Ecosystem. Pallets is the open +> source organization that maintains Flask; Pallets-Eco enables community +> maintenance of related projects. If you are interested in helping maintain +> this project, please reach out on [the Pallets Discord server][discord]. +> +> [discord]: https://discord.gg/pallets + + +## Example + Signal receivers can subscribe to specific senders or receive signals sent by any sender. -.. code-block:: pycon - - >>> from blinker import signal - >>> started = signal('round-started') - >>> def each(round): - ... print(f"Round {round}") - ... - >>> started.connect(each) - - >>> def round_two(round): - ... print("This is round two.") - ... - >>> started.connect(round_two, sender=2) - - >>> for round in range(1, 4): - ... started.send(round) - ... - Round 1! - Round 2! - This is round two. - Round 3! - - -Links ------ - -- Documentation: https://blinker.readthedocs.io/ -- Changes: https://blinker.readthedocs.io/#changes -- PyPI Releases: https://pypi.org/project/blinker/ -- Source Code: https://github.com/pallets-eco/blinker/ -- Issue Tracker: https://github.com/pallets-eco/blinker/issues/ +```pycon +>>> from blinker import signal +>>> started = signal('round-started') +>>> def each(round): +... print(f"Round {round}") +... +>>> started.connect(each) + +>>> def round_two(round): +... print("This is round two.") +... +>>> started.connect(round_two, sender=2) + +>>> for round in range(1, 4): +... started.send(round) +... +Round 1! +Round 2! +This is round two. +Round 3! +``` diff --git a/contrib/python/blinker/py3/LICENSE.rst b/contrib/python/blinker/py3/LICENSE.txt index 79c9825adb..79c9825adb 100644 --- a/contrib/python/blinker/py3/LICENSE.rst +++ b/contrib/python/blinker/py3/LICENSE.txt diff --git a/contrib/python/blinker/py3/README.md b/contrib/python/blinker/py3/README.md new file mode 100644 index 0000000000..a3969bbbe0 --- /dev/null +++ b/contrib/python/blinker/py3/README.md @@ -0,0 +1,43 @@ +# Blinker + +Blinker provides a fast dispatching system that allows any number of +interested parties to subscribe to events, or "signals". + + +## Pallets Community Ecosystem + +> [!IMPORTANT]\ +> This project is part of the Pallets Community Ecosystem. Pallets is the open +> source organization that maintains Flask; Pallets-Eco enables community +> maintenance of related projects. If you are interested in helping maintain +> this project, please reach out on [the Pallets Discord server][discord]. +> +> [discord]: https://discord.gg/pallets + + +## Example + +Signal receivers can subscribe to specific senders or receive signals +sent by any sender. + +```pycon +>>> from blinker import signal +>>> started = signal('round-started') +>>> def each(round): +... print(f"Round {round}") +... +>>> started.connect(each) + +>>> def round_two(round): +... print("This is round two.") +... +>>> started.connect(round_two, sender=2) + +>>> for round in range(1, 4): +... started.send(round) +... +Round 1! +Round 2! +This is round two. +Round 3! +``` diff --git a/contrib/python/blinker/py3/README.rst b/contrib/python/blinker/py3/README.rst deleted file mode 100644 index e3bc6d4781..0000000000 --- a/contrib/python/blinker/py3/README.rst +++ /dev/null @@ -1,40 +0,0 @@ -Blinker -======= - -Blinker provides a fast dispatching system that allows any number of -interested parties to subscribe to events, or "signals". - -Signal receivers can subscribe to specific senders or receive signals -sent by any sender. - -.. code-block:: pycon - - >>> from blinker import signal - >>> started = signal('round-started') - >>> def each(round): - ... print(f"Round {round}") - ... - >>> started.connect(each) - - >>> def round_two(round): - ... print("This is round two.") - ... - >>> started.connect(round_two, sender=2) - - >>> for round in range(1, 4): - ... started.send(round) - ... - Round 1! - Round 2! - This is round two. - Round 3! - - -Links ------ - -- Documentation: https://blinker.readthedocs.io/ -- Changes: https://blinker.readthedocs.io/#changes -- PyPI Releases: https://pypi.org/project/blinker/ -- Source Code: https://github.com/pallets-eco/blinker/ -- Issue Tracker: https://github.com/pallets-eco/blinker/issues/ diff --git a/contrib/python/blinker/py3/blinker/__init__.py b/contrib/python/blinker/py3/blinker/__init__.py index d014caa0ff..c93527eca8 100644 --- a/contrib/python/blinker/py3/blinker/__init__.py +++ b/contrib/python/blinker/py3/blinker/__init__.py @@ -1,19 +1,60 @@ -from blinker.base import ANY -from blinker.base import NamedSignal -from blinker.base import Namespace -from blinker.base import receiver_connected -from blinker.base import Signal -from blinker.base import signal -from blinker.base import WeakNamespace +from __future__ import annotations + +import typing as t + +from .base import ANY +from .base import default_namespace +from .base import NamedSignal +from .base import Namespace +from .base import Signal +from .base import signal __all__ = [ "ANY", + "default_namespace", "NamedSignal", "Namespace", "Signal", - "WeakNamespace", - "receiver_connected", "signal", ] -__version__ = "1.7.0" + +def __getattr__(name: str) -> t.Any: + import warnings + + if name == "__version__": + import importlib.metadata + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " Blinker 1.9.0. Use feature detection or" + " 'importlib.metadata.version(\"blinker\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("blinker") + + if name == "receiver_connected": + from .base import _receiver_connected + + warnings.warn( + "The global 'receiver_connected' signal is deprecated and will be" + " removed in Blinker 1.9. Use 'Signal.receiver_connected' and" + " 'Signal.receiver_disconnected' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _receiver_connected + + if name == "WeakNamespace": + from .base import _WeakNamespace + + warnings.warn( + "'WeakNamespace' is deprecated and will be removed in Blinker 1.9." + " Use 'Namespace' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _WeakNamespace + + raise AttributeError(name) diff --git a/contrib/python/blinker/py3/blinker/_utilities.py b/contrib/python/blinker/py3/blinker/_utilities.py index 4b711c67d3..784ba4e121 100644 --- a/contrib/python/blinker/py3/blinker/_utilities.py +++ b/contrib/python/blinker/py3/blinker/_utilities.py @@ -1,105 +1,52 @@ from __future__ import annotations +import inspect import typing as t from weakref import ref +from weakref import WeakMethod -from blinker._saferef import BoundMethodWeakref +T = t.TypeVar("T") -IdentityType = t.Union[t.Tuple[int, int], str, int] +class Symbol: + """A constant symbol, nicer than ``object()``. Repeated calls return the + same instance. -class _symbol: - def __init__(self, name): - """Construct a new named symbol.""" - self.__name__ = self.name = name - - def __reduce__(self): - return symbol, (self.name,) - - def __repr__(self): - return self.name - - -_symbol.__name__ = "symbol" - - -class symbol: - """A constant symbol. - - >>> symbol('foo') is symbol('foo') + >>> Symbol('foo') is Symbol('foo') True - >>> symbol('foo') + >>> Symbol('foo') foo - - A slight refinement of the MAGICCOOKIE=object() pattern. The primary - advantage of symbol() is its repr(). They are also singletons. - - Repeated calls of symbol('name') will all return the same instance. - """ - symbols = {} # type: ignore[var-annotated] + symbols: t.ClassVar[dict[str, Symbol]] = {} - def __new__(cls, name): - try: + def __new__(cls, name: str) -> Symbol: + if name in cls.symbols: return cls.symbols[name] - except KeyError: - return cls.symbols.setdefault(name, _symbol(name)) - -def hashable_identity(obj: object) -> IdentityType: - if hasattr(obj, "__func__"): - return (id(obj.__func__), id(obj.__self__)) # type: ignore[attr-defined] - elif hasattr(obj, "im_func"): - return (id(obj.im_func), id(obj.im_self)) # type: ignore[attr-defined] - elif isinstance(obj, (int, str)): + obj = super().__new__(cls) + cls.symbols[name] = obj return obj - else: - return id(obj) - -WeakTypes = (ref, BoundMethodWeakref) - - -class annotatable_weakref(ref): - """A weakref.ref that supports custom instance attributes.""" - - receiver_id: t.Optional[IdentityType] - sender_id: t.Optional[IdentityType] + def __init__(self, name: str) -> None: + self.name = name + def __repr__(self) -> str: + return self.name -def reference( # type: ignore[no-untyped-def] - object, callback=None, **annotations -) -> annotatable_weakref: - """Return an annotated weak ref.""" - if callable(object): - weak = callable_reference(object, callback) - else: - weak = annotatable_weakref(object, callback) - for key, value in annotations.items(): - setattr(weak, key, value) - return weak # type: ignore[no-any-return] + def __getnewargs__(self) -> tuple[t.Any]: + return (self.name,) -def callable_reference(object, callback=None): - """Return an annotated weak ref, supporting bound instance methods.""" - if hasattr(object, "im_self") and object.im_self is not None: - return BoundMethodWeakref(target=object, on_delete=callback) - elif hasattr(object, "__self__") and object.__self__ is not None: - return BoundMethodWeakref(target=object, on_delete=callback) - return annotatable_weakref(object, callback) +def make_id(obj: object) -> t.Hashable: + if inspect.ismethod(obj): + return id(obj.__func__), id(obj.__self__) + return id(obj) -class lazy_property: - """A @property that is only evaluated once.""" - def __init__(self, deferred): - self._deferred = deferred - self.__doc__ = deferred.__doc__ +def make_ref(obj: T, callback: t.Callable[[ref[T]], None] | None = None) -> ref[T]: + if inspect.ismethod(obj): + return WeakMethod(obj, callback) # type: ignore[arg-type, return-value] - def __get__(self, obj, cls): - if obj is None: - return self - value = self._deferred(obj) - setattr(obj, self._deferred.__name__, value) - return value + return ref(obj, callback) diff --git a/contrib/python/blinker/py3/blinker/base.py b/contrib/python/blinker/py3/blinker/base.py index b9d703586d..8aa65255b9 100644 --- a/contrib/python/blinker/py3/blinker/base.py +++ b/contrib/python/blinker/py3/blinker/base.py @@ -5,43 +5,40 @@ API client code seen in a blog post. Signals are first-class objects and each manages its own receivers and message emission. The :func:`signal` function provides singleton behavior for named signals. - """ + from __future__ import annotations import typing as t +import warnings +import weakref from collections import defaultdict from contextlib import contextmanager +from functools import cached_property from inspect import iscoroutinefunction -from warnings import warn from weakref import WeakValueDictionary -from blinker._utilities import annotatable_weakref -from blinker._utilities import hashable_identity -from blinker._utilities import IdentityType -from blinker._utilities import lazy_property -from blinker._utilities import reference -from blinker._utilities import symbol -from blinker._utilities import WeakTypes +from ._utilities import make_id +from ._utilities import make_ref +from ._utilities import Symbol if t.TYPE_CHECKING: import typing_extensions as te - T_callable = t.TypeVar("T_callable", bound=t.Callable[..., t.Any]) - + F = t.TypeVar("F", bound=t.Callable[..., t.Any]) T = t.TypeVar("T") P = te.ParamSpec("P") - AsyncWrapperType = t.Callable[[t.Callable[P, t.Awaitable[T]]], t.Callable[P, T]] - SyncWrapperType = t.Callable[[t.Callable[P, T]], t.Callable[P, t.Awaitable[T]]] + class PAsyncWrapper(t.Protocol): + def __call__(self, f: t.Callable[P, t.Awaitable[T]]) -> t.Callable[P, T]: ... + + class PSyncWrapper(t.Protocol): + def __call__(self, f: t.Callable[P, T]) -> t.Callable[P, t.Awaitable[T]]: ... -ANY = symbol("ANY") -ANY.__doc__ = 'Token for "any sender".' -ANY_ID = 0 -# NOTE: We need a reference to cast for use in weakref callbacks otherwise -# t.cast may have already been set to None during finalization. -cast = t.cast +ANY = Symbol("ANY") +"""Token for "any sender".""" +ANY_ID = 0 class Signal: @@ -51,9 +48,9 @@ class Signal: #: without an additional import. ANY = ANY - set_class: type[set] = set + set_class: type[set[t.Any]] = set - @lazy_property + @cached_property def receiver_connected(self) -> Signal: """Emitted after each :meth:`connect`. @@ -61,11 +58,10 @@ class Signal: arguments are passed through: *receiver*, *sender*, and *weak*. .. versionadded:: 1.2 - """ return Signal(doc="Emitted after a receiver connects.") - @lazy_property + @cached_property def receiver_disconnected(self) -> Signal: """Emitted after :meth:`disconnect`. @@ -85,37 +81,31 @@ class Signal: callback on weak receivers and senders. .. versionadded:: 1.2 - """ return Signal(doc="Emitted after a receiver disconnects.") def __init__(self, doc: str | None = None) -> None: """ - :param doc: optional. If provided, will be assigned to the signal's - __doc__ attribute. - + :param doc: Set the instance's ``__doc__`` attribute for documentation. """ if doc: self.__doc__ = doc + #: A mapping of connected receivers. #: #: The values of this mapping are not meaningful outside of the #: internal :class:`Signal` implementation, however the boolean value #: of the mapping is useful as an extremely efficient check to see if #: any receivers are connected to the signal. - self.receivers: dict[IdentityType, t.Callable | annotatable_weakref] = {} - self.is_muted = False - self._by_receiver: dict[IdentityType, set[IdentityType]] = defaultdict( - self.set_class - ) - self._by_sender: dict[IdentityType, set[IdentityType]] = defaultdict( - self.set_class - ) - self._weak_senders: dict[IdentityType, annotatable_weakref] = {} - - def connect( - self, receiver: T_callable, sender: t.Any = ANY, weak: bool = True - ) -> T_callable: + self.receivers: dict[ + t.Any, weakref.ref[t.Callable[..., t.Any]] | t.Callable[..., t.Any] + ] = {} + self.is_muted: bool = False + self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) + self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) + self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {} + + def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F: """Connect *receiver* to signal events sent by *sender*. :param receiver: A callable. Will be invoked by :meth:`send` with @@ -132,60 +122,51 @@ class Signal: :param weak: If true, the Signal will hold a weakref to *receiver* and automatically disconnect when *receiver* goes out of scope or is garbage collected. Defaults to True. - """ - receiver_id = hashable_identity(receiver) - receiver_ref: T_callable | annotatable_weakref + receiver_id = make_id(receiver) + sender_id = ANY_ID if sender is ANY else make_id(sender) if weak: - receiver_ref = reference(receiver, self._cleanup_receiver) - receiver_ref.receiver_id = receiver_id - else: - receiver_ref = receiver - sender_id: IdentityType - if sender is ANY: - sender_id = ANY_ID + self.receivers[receiver_id] = make_ref( + receiver, self._make_cleanup_receiver(receiver_id) + ) else: - sender_id = hashable_identity(sender) + self.receivers[receiver_id] = receiver - self.receivers.setdefault(receiver_id, receiver_ref) self._by_sender[sender_id].add(receiver_id) self._by_receiver[receiver_id].add(sender_id) - del receiver_ref if sender is not ANY and sender_id not in self._weak_senders: - # wire together a cleanup for weakref-able senders + # store a cleanup for weakref-able senders try: - sender_ref = reference(sender, self._cleanup_sender) - sender_ref.sender_id = sender_id + self._weak_senders[sender_id] = make_ref( + sender, self._make_cleanup_sender(sender_id) + ) except TypeError: pass - else: - self._weak_senders.setdefault(sender_id, sender_ref) - del sender_ref - # broadcast this connection. if receivers raise, disconnect. if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers: try: self.receiver_connected.send( self, receiver=receiver, sender=sender, weak=weak ) - except TypeError as e: + except TypeError: + # TODO no explanation or test for this self.disconnect(receiver, sender) - raise e - if receiver_connected.receivers and self is not receiver_connected: + raise + + if _receiver_connected.receivers and self is not _receiver_connected: try: - receiver_connected.send( + _receiver_connected.send( self, receiver_arg=receiver, sender_arg=sender, weak_arg=weak ) - except TypeError as e: + except TypeError: self.disconnect(receiver, sender) - raise e + raise + return receiver - def connect_via( - self, sender: t.Any, weak: bool = False - ) -> t.Callable[[T_callable], T_callable]: + def connect_via(self, sender: t.Any, weak: bool = False) -> t.Callable[[F], F]: """Connect the decorated function as a receiver for *sender*. :param sender: Any object or :obj:`ANY`. The decorated function @@ -204,10 +185,9 @@ class Signal: .. versionadded:: 1.1 - """ - def decorator(fn: T_callable) -> T_callable: + def decorator(fn: F) -> F: self.connect(fn, sender, weak) return fn @@ -215,7 +195,7 @@ class Signal: @contextmanager def connected_to( - self, receiver: t.Callable, sender: t.Any = ANY + self, receiver: t.Callable[..., t.Any], sender: t.Any = ANY ) -> t.Generator[None, None, None]: """Execute a block with the signal temporarily connected to *receiver*. @@ -237,6 +217,7 @@ class Signal: """ self.connect(receiver, sender=sender, weak=False) + try: yield None finally: @@ -248,15 +229,14 @@ class Signal: Useful for test purposes. """ self.is_muted = True + try: yield None - except Exception as e: - raise e finally: self.is_muted = False def temporarily_connected_to( - self, receiver: t.Callable, sender: t.Any = ANY + self, receiver: t.Callable[..., t.Any], sender: t.Any = ANY ) -> t.ContextManager[None]: """An alias for :meth:`connected_to`. @@ -265,23 +245,25 @@ class Signal: .. versionadded:: 0.9 - .. versionchanged:: 1.1 - Renamed to :meth:`connected_to`. ``temporarily_connected_to`` was - deprecated in 1.2 and will be removed in a subsequent version. - + .. deprecated:: 1.1 + Renamed to ``connected_to``. Will be removed in Blinker 1.9. """ - warn( - "temporarily_connected_to is deprecated; use connected_to instead.", + warnings.warn( + "'temporarily_connected_to' is renamed to 'connected_to'. The old name is" + " deprecated and will be removed in Blinker 1.9.", DeprecationWarning, + stacklevel=2, ) return self.connected_to(receiver, sender) def send( self, - *sender: t.Any, - _async_wrapper: AsyncWrapperType | None = None, + sender: t.Any | None = None, + /, + *, + _async_wrapper: PAsyncWrapper | None = None, **kwargs: t.Any, - ) -> list[tuple[t.Callable, t.Any]]: + ) -> list[tuple[t.Callable[..., t.Any], t.Any]]: """Emit this signal on behalf of *sender*, passing on ``kwargs``. Returns a list of 2-tuples, pairing receivers with their return @@ -297,23 +279,29 @@ class Signal: if self.is_muted: return [] - sender = self._extract_sender(sender) results = [] + for receiver in self.receivers_for(sender): if iscoroutinefunction(receiver): if _async_wrapper is None: - raise RuntimeError("Cannot send to a coroutine function") - receiver = _async_wrapper(receiver) - result = receiver(sender, **kwargs) + raise RuntimeError("Cannot send to a coroutine function.") + + result = _async_wrapper(receiver)(sender, **kwargs) + else: + result = receiver(sender, **kwargs) + results.append((receiver, result)) + return results async def send_async( self, - *sender: t.Any, - _sync_wrapper: SyncWrapperType | None = None, + sender: t.Any | None = None, + /, + *, + _sync_wrapper: PSyncWrapper | None = None, **kwargs: t.Any, - ) -> list[tuple[t.Callable, t.Any]]: + ) -> list[tuple[t.Callable[..., t.Any], t.Any]]: """Emit this signal on behalf of *sender*, passing on ``kwargs``. Returns a list of 2-tuples, pairing receivers with their return @@ -329,39 +317,20 @@ class Signal: if self.is_muted: return [] - sender = self._extract_sender(sender) results = [] + for receiver in self.receivers_for(sender): if not iscoroutinefunction(receiver): if _sync_wrapper is None: - raise RuntimeError("Cannot send to a non-coroutine function") - receiver = _sync_wrapper(receiver) - result = await receiver(sender, **kwargs) - results.append((receiver, result)) - return results + raise RuntimeError("Cannot send to a non-coroutine function.") - def _extract_sender(self, sender: t.Any) -> t.Any: - if not self.receivers: - # Ensure correct signature even on no-op sends, disable with -O - # for lowest possible cost. - if __debug__ and sender and len(sender) > 1: - raise TypeError( - f"send() accepts only one positional argument, {len(sender)} given" - ) - return [] + result = await _sync_wrapper(receiver)(sender, **kwargs) + else: + result = await receiver(sender, **kwargs) - # Using '*sender' rather than 'sender=None' allows 'sender' to be - # used as a keyword argument- i.e. it's an invisible name in the - # function signature. - if len(sender) == 0: - sender = None - elif len(sender) > 1: - raise TypeError( - f"send() accepts only one positional argument, {len(sender)} given" - ) - else: - sender = sender[0] - return sender + results.append((receiver, result)) + + return results def has_receivers_for(self, sender: t.Any) -> bool: """True if there is probably a receiver for *sender*. @@ -373,36 +342,48 @@ class Signal: """ if not self.receivers: return False + if self._by_sender[ANY_ID]: return True + if sender is ANY: return False - return hashable_identity(sender) in self._by_sender + + return make_id(sender) in self._by_sender def receivers_for( self, sender: t.Any - ) -> t.Generator[t.Callable[[t.Any], t.Any], None, None]: + ) -> t.Generator[t.Callable[..., t.Any], None, None]: """Iterate all live receivers listening for *sender*.""" # TODO: test receivers_for(ANY) - if self.receivers: - sender_id = hashable_identity(sender) - if sender_id in self._by_sender: - ids = self._by_sender[ANY_ID] | self._by_sender[sender_id] - else: - ids = self._by_sender[ANY_ID].copy() - for receiver_id in ids: - receiver = self.receivers.get(receiver_id) - if receiver is None: + if not self.receivers: + return + + sender_id = make_id(sender) + + if sender_id in self._by_sender: + ids = self._by_sender[ANY_ID] | self._by_sender[sender_id] + else: + ids = self._by_sender[ANY_ID].copy() + + for receiver_id in ids: + receiver = self.receivers.get(receiver_id) + + if receiver is None: + continue + + if isinstance(receiver, weakref.ref): + strong = receiver() + + if strong is None: + self._disconnect(receiver_id, ANY_ID) continue - if isinstance(receiver, WeakTypes): - strong = receiver() - if strong is None: - self._disconnect(receiver_id, ANY_ID) - continue - receiver = strong - yield receiver # type: ignore[misc] - - def disconnect(self, receiver: t.Callable, sender: t.Any = ANY) -> None: + + yield strong + else: + yield receiver + + def disconnect(self, receiver: t.Callable[..., t.Any], sender: t.Any = ANY) -> None: """Disconnect *receiver* from this signal's events. :param receiver: a previously :meth:`connected<connect>` callable @@ -411,12 +392,14 @@ class Signal: to disconnect from all senders. Defaults to ``ANY``. """ - sender_id: IdentityType + sender_id: t.Hashable + if sender is ANY: sender_id = ANY_ID else: - sender_id = hashable_identity(sender) - receiver_id = hashable_identity(receiver) + sender_id = make_id(sender) + + receiver_id = make_id(receiver) self._disconnect(receiver_id, sender_id) if ( @@ -425,27 +408,40 @@ class Signal: ): self.receiver_disconnected.send(self, receiver=receiver, sender=sender) - def _disconnect(self, receiver_id: IdentityType, sender_id: IdentityType) -> None: + def _disconnect(self, receiver_id: t.Hashable, sender_id: t.Hashable) -> None: if sender_id == ANY_ID: - if self._by_receiver.pop(receiver_id, False): + if self._by_receiver.pop(receiver_id, None) is not None: for bucket in self._by_sender.values(): bucket.discard(receiver_id) + self.receivers.pop(receiver_id, None) else: self._by_sender[sender_id].discard(receiver_id) self._by_receiver[receiver_id].discard(sender_id) - def _cleanup_receiver(self, receiver_ref: annotatable_weakref) -> None: + def _make_cleanup_receiver( + self, receiver_id: t.Hashable + ) -> t.Callable[[weakref.ref[t.Callable[..., t.Any]]], None]: """Disconnect a receiver from all senders.""" - self._disconnect(cast(IdentityType, receiver_ref.receiver_id), ANY_ID) - def _cleanup_sender(self, sender_ref: annotatable_weakref) -> None: + def cleanup(ref: weakref.ref[t.Callable[..., t.Any]]) -> None: + self._disconnect(receiver_id, ANY_ID) + + return cleanup + + def _make_cleanup_sender( + self, sender_id: t.Hashable + ) -> t.Callable[[weakref.ref[t.Any]], None]: """Disconnect all receivers from a sender.""" - sender_id = cast(IdentityType, sender_ref.sender_id) assert sender_id != ANY_ID - self._weak_senders.pop(sender_id, None) - for receiver_id in self._by_sender.pop(sender_id, ()): - self._by_receiver[receiver_id].discard(sender_id) + + def cleanup(ref: weakref.ref[t.Any]) -> None: + self._weak_senders.pop(sender_id, None) + + for receiver_id in self._by_sender.pop(sender_id, ()): + self._by_receiver[receiver_id].discard(sender_id) + + return cleanup def _cleanup_bookkeeping(self) -> None: """Prune unused sender/receiver bookkeeping. Not threadsafe. @@ -469,9 +465,9 @@ class Signal: failure mode is perhaps not a big deal for you. """ for mapping in (self._by_sender, self._by_receiver): - for _id, bucket in list(mapping.items()): + for ident, bucket in list(mapping.items()): if not bucket: - mapping.pop(_id, None) + mapping.pop(ident, None) def _clear_state(self) -> None: """Throw away all signal state. Useful for unit tests.""" @@ -481,7 +477,7 @@ class Signal: self._by_receiver.clear() -receiver_connected = Signal( +_receiver_connected = Signal( """\ Sent by a :class:`Signal` after a receiver connects. @@ -491,12 +487,9 @@ Sent by a :class:`Signal` after a receiver connects. :keyword weak_arg: true if the connection to receiver_arg is a weak reference .. deprecated:: 1.2 - -As of 1.2, individual signals have their own private -:attr:`~Signal.receiver_connected` and -:attr:`~Signal.receiver_disconnected` signals with a slightly simplified -call signature. This global signal is planned to be removed in 1.6. - + Individual signals have their own :attr:`~Signal.receiver_connected` and + :attr:`~Signal.receiver_disconnected` signals with a slightly simplified + call signature. This global signal will be removed in Blinker 1.9. """ ) @@ -505,24 +498,23 @@ class NamedSignal(Signal): """A named generic notification emitter.""" def __init__(self, name: str, doc: str | None = None) -> None: - Signal.__init__(self, doc) + super().__init__(doc) #: The name of this signal. - self.name = name + self.name: str = name def __repr__(self) -> str: - base = Signal.__repr__(self) + base = super().__repr__() return f"{base[:-1]}; {self.name!r}>" # noqa: E702 -class Namespace(dict): +class Namespace(dict): # type: ignore[type-arg] """A mapping of signal names to signals.""" def signal(self, name: str, doc: str | None = None) -> NamedSignal: """Return the :class:`NamedSignal` *name*, creating it if required. Repeated calls to this function will return the same signal object. - """ try: return self[name] # type: ignore[no-any-return] @@ -531,7 +523,7 @@ class Namespace(dict): return result # type: ignore[no-any-return] -class WeakNamespace(WeakValueDictionary): +class _WeakNamespace(WeakValueDictionary): # type: ignore[type-arg] """A weak mapping of signal names to signals. Automatically cleans up unused Signals when the last reference goes out @@ -540,8 +532,19 @@ class WeakNamespace(WeakValueDictionary): .. versionadded:: 1.3 + .. deprecated:: 1.3 + Will be removed in Blinker 1.9. """ + def __init__(self) -> None: + warnings.warn( + "'WeakNamespace' is deprecated and will be removed in Blinker 1.9." + " Use 'Namespace' instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__() + def signal(self, name: str, doc: str | None = None) -> NamedSignal: """Return the :class:`NamedSignal` *name*, creating it if required. @@ -555,4 +558,34 @@ class WeakNamespace(WeakValueDictionary): return result # type: ignore[no-any-return] -signal = Namespace().signal +default_namespace = Namespace() +"""A default namespace for creating named signals. :func:`signal` creates a +:class:`NamedSignal` in this namespace. +""" + +signal = default_namespace.signal +"""Create a :class:`NamedSignal` in :data:`default_namespace`. Repeated calls +with the same name will return the same signal. +""" + + +def __getattr__(name: str) -> t.Any: + if name == "reciever_connected": + warnings.warn( + "The global 'reciever_connected' signal is deprecated and will be" + " removed in Blinker 1.9. Use 'Signal.receiver_connected' and" + " 'Signal.reciever_disconnected' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _receiver_connected + + if name == "WeakNamespace": + warnings.warn( + "'WeakNamespace' is deprecated and will be removed in Blinker 1.9." + " Use 'Namespace' instead.", + DeprecationWarning, + stacklevel=2, + ) + + raise AttributeError(name) diff --git a/contrib/python/blinker/py3/ya.make b/contrib/python/blinker/py3/ya.make index f1ac7057e7..0520c07319 100644 --- a/contrib/python/blinker/py3/ya.make +++ b/contrib/python/blinker/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(1.7.0) +VERSION(1.8.0) LICENSE(MIT) |