From 18d3e2559933269aa63f4b0c67ccdb9af8a327c2 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 13 Feb 2024 09:26:25 -0500 Subject: [PATCH] more deprecations --- src/psygnal/__init__.py | 5 +-- src/psygnal/_group.py | 59 ++++++++++++++++++++------------ src/psygnal/_group_descriptor.py | 2 +- src/psygnal/utils.py | 13 +++---- tests/test_evented_model.py | 13 +++---- tests/test_group.py | 7 ++-- tests/test_group_descriptor.py | 8 ++--- 7 files changed, 63 insertions(+), 44 deletions(-) diff --git a/src/psygnal/__init__.py b/src/psygnal/__init__.py index fd2dfe36..a37dfd86 100644 --- a/src/psygnal/__init__.py +++ b/src/psygnal/__init__.py @@ -23,8 +23,8 @@ "_compiled", "debounced", "EmissionInfo", - "EmitLoopError", "emit_queued", + "EmitLoopError", "evented", "EventedModel", "get_evented_namespace", @@ -33,6 +33,7 @@ "SignalGroup", "SignalGroupDescriptor", "SignalInstance", + "SignalRelay", "throttled", ] @@ -50,7 +51,7 @@ from ._evented_decorator import evented from ._exceptions import EmitLoopError -from ._group import EmissionInfo, SignalGroup +from ._group import EmissionInfo, SignalGroup, SignalRelay from ._group_descriptor import ( SignalGroupDescriptor, get_evented_namespace, diff --git a/src/psygnal/_group.py b/src/psygnal/_group.py index 7b2470de..94cf1356 100644 --- a/src/psygnal/_group.py +++ b/src/psygnal/_group.py @@ -25,7 +25,7 @@ from psygnal._signal import Signal, SignalInstance, _SignalBlocker -__all__ = ["EmissionInfo", "SignalGroup"] +__all__ = ["EmissionInfo", "SignalGroup", "SignalRelay"] class EmissionInfo(NamedTuple): @@ -75,7 +75,7 @@ def connect_direct( ) -> Callable[[Callable], Callable] | Callable: """Connect `slot` to be called whenever *any* Signal in this group is emitted. - Params are the same as {meth}`~psygnal.SignalInstance.connect`. It's probably + Params are the same as `psygnal.SignalInstance.connect`. It's probably best to check whether `self.is_uniform()` Parameters @@ -84,14 +84,14 @@ def connect_direct( A callable to connect to this signal. If the callable accepts less arguments than the signature of this slot, then they will be discarded when calling the slot. - check_nargs : Optional[bool] + check_nargs : bool | None If `True` and the provided `slot` requires more positional arguments than the signature of this Signal, raise `TypeError`. by default `True`. - check_types : Optional[bool] + check_types : bool | None If `True`, An additional check will be performed to make sure that types declared in the slot signature are compatible with the signature declared by this signal, by default `False`. - unique : Union[bool, str] + unique : bool | str If `True`, returns without connecting if the slot has already been connected. If the literal string "raise" is passed to `unique`, then a `ValueError` will be raised if the slot is already connected. @@ -172,11 +172,23 @@ def disconnect(self, slot: Callable | None = None, missing_ok: bool = True) -> N super().disconnect(slot, missing_ok) +# NOTE +# To developers. Avoid adding public names to this class, as it is intended to be +# a container for user-determined names. If names must be added, try to prefix +# with "psygnal_" to avoid conflicts with user-defined names. @mypyc_attr(allow_interpreted_subclasses=True) -# class SignalGroup(Mapping[str, SignalInstance]): class SignalGroup: - _signals_: ClassVar[Mapping[str, Signal]] - _uniform: ClassVar[bool] = False + """A collection of signals that can be connected to as a single unit. + + Parameters + ---------- + instance : Any, optional + An object to which this SignalGroup is bound, by default None + """ + + _psygnal_signals: ClassVar[Mapping[str, Signal]] + _psygnal_instances: dict[str, SignalInstance] + _psygnal_uniform: ClassVar[bool] = False # see comment in __init__. This type annotation can be overriden by subclass # to change the public name of the SignalRelay attribute @@ -184,12 +196,13 @@ class SignalGroup: def __init__(self, instance: Any = None) -> None: cls = type(self) - if not hasattr(cls, "_signals_"): # pragma: no cover + if not hasattr(cls, "_psygnal_signals"): # pragma: no cover raise TypeError( "Cannot instantiate SignalGroup directly. Use a subclass instead." ) self._psygnal_instances: dict[str, SignalInstance] = { - name: signal.__get__(self, cls) for name, signal in cls._signals_.items() + name: signal.__get__(self, cls) + for name, signal in cls._psygnal_signals.items() } self._psygnal_relay = SignalRelay(self._psygnal_instances, instance) @@ -207,14 +220,14 @@ def __init__(self, instance: Any = None) -> None: def __init_subclass__(cls, strict: bool = False) -> None: """Finds all Signal instances on the class and add them to `cls._signals_`.""" - cls._signals_ = { + cls._psygnal_signals = { k: val for k, val in getattr(cls, "__dict__", {}).items() if isinstance(val, Signal) } - cls._uniform = _is_uniform(cls._signals_.values()) - if strict and not cls._uniform: + cls._psygnal_uniform = _is_uniform(cls._psygnal_signals.values()) + if strict and not cls._psygnal_uniform: raise TypeError( "All Signals in a strict SignalGroup must have the same signature" ) @@ -241,30 +254,34 @@ def __getattr__(self, name: str) -> Any: @property def signals(self) -> Mapping[str, SignalInstance]: # TODO: deprecate this property + warnings.warn( + "Accessing the `signals` property on a SignalGroup is deprecated. " + "Use __iter__ to iterate over all signal names, and __getitem__ or getattr " + "to access signal instances. This will be an error in a future.", + FutureWarning, + stacklevel=2, + ) return self._psygnal_instances def __len__(self) -> int: - return len(self._signals_) + return len(self._psygnal_signals) def __getitem__(self, item: str) -> SignalInstance: return self._psygnal_instances[item] def __iter__(self) -> Iterator[str]: - return iter(self._signals_) + return iter(self._psygnal_signals) def __repr__(self) -> str: """Return repr(self).""" name = self.__class__.__name__ - instance = "" - nsignals = len(self) - signals = f"{nsignals} signals" - return f"" + return f"" @classmethod def is_uniform(cls) -> bool: """Return true if all signals in the group have the same signature.""" - # TODO: Deprecate this method? - return cls._uniform + # TODO: Deprecate this meth? + return cls._psygnal_uniform def __deepcopy__(self, memo: dict[int, Any]) -> SignalGroup: # TODO: diff --git a/src/psygnal/_group_descriptor.py b/src/psygnal/_group_descriptor.py index 682ea057..2e5fd8b1 100644 --- a/src/psygnal/_group_descriptor.py +++ b/src/psygnal/_group_descriptor.py @@ -447,7 +447,7 @@ def __get__( def _create_group(self, owner: type) -> type[SignalGroup]: Group = self._signal_group or _build_dataclass_signal_group(owner, self._eqop) - if self._warn_on_no_fields and not Group._signals_: + if self._warn_on_no_fields and not Group._psygnal_signals: warnings.warn( f"No mutable fields found on class {owner}: no events will be " "emitted. (Is this a dataclass, attrs, msgspec, or pydantic model?)", diff --git a/src/psygnal/utils.py b/src/psygnal/utils.py index e24ce644..f4077832 100644 --- a/src/psygnal/utils.py +++ b/src/psygnal/utils.py @@ -1,7 +1,7 @@ """These utilities may help when using signals and evented objects.""" from __future__ import annotations -from contextlib import contextmanager +from contextlib import contextmanager, suppress from functools import partial from pathlib import Path from typing import Any, Callable, Generator, Iterator @@ -104,11 +104,12 @@ def iter_signal_instances( """ for n in dir(obj): if include_private_attrs or not n.startswith("_"): - attr = getattr(obj, n) - if isinstance(attr, SignalInstance): - yield attr - if isinstance(attr, SignalGroup): - yield attr._psygnal_relay + with suppress(AttributeError, FutureWarning): + attr = getattr(obj, n) + if isinstance(attr, SignalInstance): + yield attr + if isinstance(attr, SignalGroup): + yield attr._psygnal_relay _COMPILED_EXTS = (".so", ".pyd") diff --git a/tests/test_evented_model.py b/tests/test_evented_model.py index ebc6db41..8ae4b749 100644 --- a/tests/test_evented_model.py +++ b/tests/test_evented_model.py @@ -70,11 +70,12 @@ class User(EventedModel): # test event system assert isinstance(user.events, SignalGroup) - assert "id" in user.events.signals - assert "name" in user.events.signals + with pytest.warns(FutureWarning): + assert "id" in user.events.signals + assert "name" in user.events.signals # ClassVars are excluded from events - assert "age" not in user.events.signals + assert "age" not in user.events id_mock = Mock() name_mock = Mock() @@ -202,8 +203,8 @@ class User(EventedModel): u1_id_events = Mock() u2_id_events = Mock() - user1.events.connect(user1_events) - user1.events.connect(user1_events) + user1.events.all.connect(user1_events) + user1.events.all.connect(user1_events) user1.events.id.connect(u1_id_events) user2.events.id.connect(u2_id_events) @@ -527,7 +528,7 @@ def test_evented_model_with_property_setters(): def test_evented_model_with_property_setters_events(): t = T() - assert "c" in t.events.signals # the setter has an event + assert "c" in t.events # the setter has an event mock_a = Mock() mock_b = Mock() mock_c = Mock() diff --git a/tests/test_group.py b/tests/test_group.py index 2e69401e..16f4380e 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -17,8 +17,8 @@ def test_signal_group(): assert not MyGroup.is_uniform() group = MyGroup() assert not group.is_uniform() - assert isinstance(group.signals, dict) - assert group.signals == {"sig1": group.sig1, "sig2": group.sig2} + assert list(group) == ["sig1", "sig2"] # testing __iter__ + assert group.sig1 is group["sig1"] assert repr(group) == "" @@ -33,8 +33,7 @@ class MyStrictGroup(SignalGroup, strict=True): assert MyStrictGroup.is_uniform() group = MyStrictGroup() assert group.is_uniform() - assert isinstance(group.signals, dict) - assert set(group.signals) == {"sig1", "sig2"} + assert set(group) == {"sig1", "sig2"} with pytest.raises(TypeError) as e: diff --git a/tests/test_group_descriptor.py b/tests/test_group_descriptor.py index ccf4ab39..1b36f032 100644 --- a/tests/test_group_descriptor.py +++ b/tests/test_group_descriptor.py @@ -81,10 +81,10 @@ class Bar(Foo): # the patching of __setattr__ should only happen once # and it will happen only on the first access of .events mock_decorator.assert_not_called() - assert set(base.events.signals) == {"a"} - assert set(foo.events.signals) == {"a", "b"} - assert set(bar.events.signals) == {"a", "b", "c"} - assert set(bar2.events.signals) == {"a", "b", "c"} + assert set(base.events) == {"a"} + assert set(foo.events) == {"a", "b"} + assert set(bar.events) == {"a", "b", "c"} + assert set(bar2.events) == {"a", "b", "c"} if not _compiled: # can't patch otherwise assert mock_decorator.call_count == 1