From bc89c33b2061004b9f80c36f8dd580563f3b569e Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 12 Apr 2023 14:25:36 -0700 Subject: lazy annotations --- CHANGES.rst | 8 +++++ pyproject.toml | 1 - src/blinker/__init__.py | 3 +- src/blinker/_saferef.py | 2 +- src/blinker/_utilities.py | 25 ++++++++------- src/blinker/base.py | 80 +++++++++++++++++++++++++---------------------- 6 files changed, 66 insertions(+), 53 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index acf469f..2ab8fed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,11 @@ +Version 1.6.2 +------------- + +Unreleased + +- Type annotations are not evaluated at runtime. typing-extensions is not a runtime + dependency. :pr:`94` + Version 1.6.1 ------------- diff --git a/pyproject.toml b/pyproject.toml index ca0f5af..c58c26d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ classifiers = [ ] requires-python = ">= 3.7" dynamic = ["version"] -dependencies = ["typing-extensions>=4.2"] [project.urls] Homepage = "https://blinker.readthedocs.io" diff --git a/src/blinker/__init__.py b/src/blinker/__init__.py index 671732f..204b1ca 100644 --- a/src/blinker/__init__.py +++ b/src/blinker/__init__.py @@ -16,5 +16,4 @@ __all__ = [ "signal", ] - -__version__ = "1.6.1" +__version__ = "1.6.2.dev" diff --git a/src/blinker/_saferef.py b/src/blinker/_saferef.py index 39ac90c..dcb70c1 100644 --- a/src/blinker/_saferef.py +++ b/src/blinker/_saferef.py @@ -108,7 +108,7 @@ class BoundMethodWeakref: produce the same BoundMethodWeakref instance. """ - _all_instances = weakref.WeakValueDictionary() # type: ignore + _all_instances = weakref.WeakValueDictionary() # type: ignore[var-annotated] def __new__(cls, target, on_delete=None, *arguments, **named): """Create new instance or return current instance. diff --git a/src/blinker/_utilities.py b/src/blinker/_utilities.py index 9201046..068d94c 100644 --- a/src/blinker/_utilities.py +++ b/src/blinker/_utilities.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import inspect import sys @@ -40,7 +42,7 @@ class symbol: """ - symbols = {} # type: ignore + symbols = {} # type: ignore[var-annotated] def __new__(cls, name): try: @@ -51,9 +53,9 @@ class symbol: def hashable_identity(obj: object) -> IdentityType: if hasattr(obj, "__func__"): - return (id(obj.__func__), id(obj.__self__)) # type: ignore + 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 + return (id(obj.im_func), id(obj.im_self)) # type: ignore[attr-defined] elif isinstance(obj, (int, str)): return obj else: @@ -70,7 +72,7 @@ class annotatable_weakref(ref): sender_id: t.Optional[IdentityType] -def reference( # type: ignore +def reference( # type: ignore[no-untyped-def] object, callback=None, **annotations ) -> annotatable_weakref: """Return an annotated weak ref.""" @@ -80,7 +82,7 @@ def reference( # type: ignore weak = annotatable_weakref(object, callback) for key, value in annotations.items(): setattr(weak, key, value) - return weak # type: ignore + return weak # type: ignore[no-any-return] def callable_reference(object, callback=None): @@ -118,7 +120,7 @@ def is_coroutine_function(func: t.Any) -> bool: # such that it isn't determined as a coroutine function # without an explicit check. try: - from unittest.mock import AsyncMock # type: ignore + from unittest.mock import AsyncMock # type: ignore[attr-defined] if isinstance(func, AsyncMock): return True @@ -132,8 +134,9 @@ def is_coroutine_function(func: t.Any) -> bool: func = func.func if not inspect.isfunction(func): return False - result = bool(func.__code__.co_flags & inspect.CO_COROUTINE) - return ( - result - or getattr(func, "_is_coroutine", None) is asyncio.coroutines._is_coroutine # type: ignore # noqa: B950 - ) + + if func.__code__.co_flags & inspect.CO_COROUTINE: + return True + + acic = asyncio.coroutines._is_coroutine # type: ignore[attr-defined] + return getattr(func, "_is_coroutine", None) is acic diff --git a/src/blinker/base.py b/src/blinker/base.py index 997fef5..80e24e2 100644 --- a/src/blinker/base.py +++ b/src/blinker/base.py @@ -7,14 +7,14 @@ 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 from collections import defaultdict from contextlib import contextmanager from warnings import warn from weakref import WeakValueDictionary -import typing_extensions as te - from blinker._utilities import annotatable_weakref from blinker._utilities import hashable_identity from blinker._utilities import IdentityType @@ -24,18 +24,20 @@ from blinker._utilities import reference from blinker._utilities import symbol from blinker._utilities import WeakTypes +if t.TYPE_CHECKING: + import typing_extensions as te -ANY = symbol("ANY") -ANY.__doc__ = 'Token for "any sender".' -ANY_ID = 0 + T_callable = t.TypeVar("T_callable", bound=t.Callable[..., t.Any]) -T_callable = t.TypeVar("T_callable", bound=t.Callable) + T = t.TypeVar("T") + P = te.ParamSpec("P") -T = t.TypeVar("T") -P = te.ParamSpec("P") + AsyncWrapperType = t.Callable[[t.Callable[P, T]], t.Callable[P, t.Awaitable[T]]] + SyncWrapperType = t.Callable[[t.Callable[P, t.Awaitable[T]]], t.Callable[P, T]] -AsyncWrapperType = t.Callable[[t.Callable[P, T]], t.Callable[P, t.Awaitable[T]]] -SyncWrapperType = t.Callable[[t.Callable[P, t.Awaitable[T]]], t.Callable[P, T]] +ANY = symbol("ANY") +ANY.__doc__ = 'Token for "any sender".' +ANY_ID = 0 class Signal: @@ -46,7 +48,7 @@ class Signal: ANY = ANY @lazy_property - def receiver_connected(self) -> "Signal": + def receiver_connected(self) -> Signal: """Emitted after each :meth:`connect`. The signal sender is the signal instance, and the :meth:`connect` @@ -58,7 +60,7 @@ class Signal: return Signal(doc="Emitted after a receiver connects.") @lazy_property - def receiver_disconnected(self) -> "Signal": + def receiver_disconnected(self) -> Signal: """Emitted after :meth:`disconnect`. The sender is the signal instance, and the :meth:`disconnect` arguments @@ -81,7 +83,7 @@ class Signal: """ return Signal(doc="Emitted after a receiver disconnects.") - def __init__(self, doc: t.Optional[str] = None) -> None: + def __init__(self, doc: str | None = None) -> None: """ :param doc: optional. If provided, will be assigned to the signal's __doc__ attribute. @@ -95,13 +97,11 @@ class Signal: #: 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: t.Dict[ - IdentityType, t.Union[t.Callable, annotatable_weakref] - ] = {} + self.receivers: dict[IdentityType, t.Callable | annotatable_weakref] = {} self.is_muted = False - self._by_receiver: t.Dict[IdentityType, t.Set[IdentityType]] = defaultdict(set) - self._by_sender: t.Dict[IdentityType, t.Set[IdentityType]] = defaultdict(set) - self._weak_senders: t.Dict[IdentityType, annotatable_weakref] = {} + self._by_receiver: dict[IdentityType, set[IdentityType]] = defaultdict(set) + self._by_sender: dict[IdentityType, set[IdentityType]] = defaultdict(set) + self._weak_senders: dict[IdentityType, annotatable_weakref] = {} def connect( self, receiver: T_callable, sender: t.Any = ANY, weak: bool = True @@ -125,7 +125,8 @@ class Signal: """ receiver_id = hashable_identity(receiver) - receiver_ref: t.Union[annotatable_weakref, T_callable] + receiver_ref: T_callable | annotatable_weakref + if weak: receiver_ref = reference(receiver, self._cleanup_receiver) receiver_ref.receiver_id = receiver_id @@ -271,9 +272,9 @@ class Signal: def send( self, *sender: t.Any, - _async_wrapper: t.Optional[AsyncWrapperType] = None, + _async_wrapper: AsyncWrapperType | None = None, **kwargs: t.Any, - ) -> t.List[t.Tuple[t.Callable, t.Any]]: + ) -> list[tuple[t.Callable, t.Any]]: """Emit this signal on behalf of *sender*, passing on ``kwargs``. Returns a list of 2-tuples, pairing receivers with their return @@ -296,15 +297,16 @@ class Signal: if _async_wrapper is None: raise RuntimeError("Cannot send to a coroutine function") receiver = _async_wrapper(receiver) - results.append((receiver, receiver(sender, **kwargs))) # type: ignore + result = receiver(sender, **kwargs) # type: ignore[call-arg] + results.append((receiver, result)) return results async def send_async( self, *sender: t.Any, - _sync_wrapper: t.Optional[SyncWrapperType] = None, + _sync_wrapper: SyncWrapperType | None = None, **kwargs: t.Any, - ) -> t.List[t.Tuple[t.Callable, t.Any]]: + ) -> list[tuple[t.Callable, t.Any]]: """Emit this signal on behalf of *sender*, passing on ``kwargs``. Returns a list of 2-tuples, pairing receivers with their return @@ -326,11 +328,12 @@ class Signal: if not is_coroutine_function(receiver): if _sync_wrapper is None: raise RuntimeError("Cannot send to a non-coroutine function") - receiver = _sync_wrapper(receiver) # type: ignore - results.append((receiver, await receiver(sender, **kwargs))) # type: ignore + receiver = _sync_wrapper(receiver) # type: ignore[arg-type] + result = await receiver(sender, **kwargs) # type: ignore[call-arg, misc] + results.append((receiver, result)) return results - def _extract_sender(self, sender): + 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. @@ -371,7 +374,7 @@ class Signal: def receivers_for( self, sender: t.Any - ) -> t.Generator[t.Union[t.Callable, annotatable_weakref], None, None]: + ) -> t.Generator[t.Callable | annotatable_weakref, None, None]: """Iterate all live receivers listening for *sender*.""" # TODO: test receivers_for(ANY) if self.receivers: @@ -390,8 +393,7 @@ class Signal: self._disconnect(receiver_id, ANY_ID) continue receiver = strong - receiver = t.cast(t.Union[t.Callable, annotatable_weakref], receiver) - yield receiver + yield receiver # type: ignore[misc] def disconnect(self, receiver: t.Callable, sender: t.Any = ANY) -> None: """Disconnect *receiver* from this signal's events. @@ -495,7 +497,7 @@ call signature. This global signal is planned to be removed in 1.6. class NamedSignal(Signal): """A named generic notification emitter.""" - def __init__(self, name: str, doc: t.Optional[str] = None) -> None: + def __init__(self, name: str, doc: str | None = None) -> None: Signal.__init__(self, doc) #: The name of this signal. @@ -509,16 +511,17 @@ class NamedSignal(Signal): class Namespace(dict): """A mapping of signal names to signals.""" - def signal(self, name: str, doc: t.Optional[str] = None) -> NamedSignal: + 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 + return self[name] # type: ignore[no-any-return] except KeyError: - return self.setdefault(name, NamedSignal(name, doc)) # type: ignore + result = self.setdefault(name, NamedSignal(name, doc)) + return result # type: ignore[no-any-return] class WeakNamespace(WeakValueDictionary): @@ -532,16 +535,17 @@ class WeakNamespace(WeakValueDictionary): """ - def signal(self, name: str, doc: t.Optional[str] = None) -> NamedSignal: + 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 + return self[name] # type: ignore[no-any-return] except KeyError: - return self.setdefault(name, NamedSignal(name, doc)) # type: ignore + result = self.setdefault(name, NamedSignal(name, doc)) + return result # type: ignore[no-any-return] signal = Namespace().signal -- cgit v1.2.1