Skip to content

Commit

Permalink
more deprecations
Browse files Browse the repository at this point in the history
  • Loading branch information
tlambert03 committed Feb 13, 2024
1 parent a2e88ea commit 18d3e25
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 44 deletions.
5 changes: 3 additions & 2 deletions src/psygnal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"_compiled",
"debounced",
"EmissionInfo",
"EmitLoopError",
"emit_queued",
"EmitLoopError",
"evented",
"EventedModel",
"get_evented_namespace",
Expand All @@ -33,6 +33,7 @@
"SignalGroup",
"SignalGroupDescriptor",
"SignalInstance",
"SignalRelay",
"throttled",
]

Expand All @@ -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,
Expand Down
59 changes: 38 additions & 21 deletions src/psygnal/_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from psygnal._signal import Signal, SignalInstance, _SignalBlocker

__all__ = ["EmissionInfo", "SignalGroup"]
__all__ = ["EmissionInfo", "SignalGroup", "SignalRelay"]


class EmissionInfo(NamedTuple):
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -172,24 +172,37 @@ 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
all: SignalRelay

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)

Expand All @@ -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"
)
Expand All @@ -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"<SignalGroup {name!r} with {signals}{instance}>"
return f"<SignalGroup {name!r} with {len(self)} signals>"

@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:
Expand Down
2 changes: 1 addition & 1 deletion src/psygnal/_group_descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?)",
Expand Down
13 changes: 7 additions & 6 deletions src/psygnal/utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")
Expand Down
13 changes: 7 additions & 6 deletions tests/test_evented_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 3 additions & 4 deletions tests/test_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) == "<SignalGroup 'MyGroup' with 2 signals>"

Expand All @@ -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:

Expand Down
8 changes: 4 additions & 4 deletions tests/test_group_descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 18d3e25

Please sign in to comment.