Skip to content

Commit

Permalink
add emit_old_value
Browse files Browse the repository at this point in the history
  • Loading branch information
getzze committed Jan 31, 2024
1 parent b0d2057 commit 4f73d95
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 7 deletions.
7 changes: 7 additions & 0 deletions src/psygnal/_evented_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def evented(
equality_operators: Optional[Dict[str, EqOperator]] = None,
warn_on_no_fields: bool = ...,
cache_on_instance: bool = ...,
emit_old_value: bool = ...,
) -> T:
...

Expand All @@ -44,6 +45,7 @@ def evented(
equality_operators: Optional[Dict[str, EqOperator]] = None,
warn_on_no_fields: bool = ...,
cache_on_instance: bool = ...,
emit_old_value: bool = ...,
) -> Callable[[T], T]:
...

Expand All @@ -55,6 +57,7 @@ def evented(
equality_operators: Optional[Dict[str, EqOperator]] = None,
warn_on_no_fields: bool = True,
cache_on_instance: bool = True,
emit_old_value: bool = False,
) -> Union[Callable[[T], T], T]:
"""A decorator to add events to a dataclass.
Expand Down Expand Up @@ -94,6 +97,9 @@ def evented(
access, but means that the owner instance will no longer be pickleable. If
`False`, the SignalGroup instance will *still* be cached, but not on the
instance itself.
emit_old_value: bool
When the field is mutated, emit two parameters, the new value and the old value, by
default False
Returns
-------
Expand Down Expand Up @@ -128,6 +134,7 @@ def _decorate(cls: T) -> T:
equality_operators=equality_operators,
warn_on_no_fields=warn_on_no_fields,
cache_on_instance=cache_on_instance,
emit_old_value=emit_old_value,
)
# as a decorator, this will have already been called
descriptor.__set_name__(cls, events_namespace)
Expand Down
26 changes: 19 additions & 7 deletions src/psygnal/_group_descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,37 +176,41 @@ class Foo:


class _changes_emitted:
def __init__(self, obj: object, field: str, signal: SignalInstance) -> None:
def __init__(self, obj: object, field: str, signal: SignalInstance, emit_old_value: bool = False) -> None:
self.obj = obj
self.field = field
self.signal = signal
self.emit_old_value = emit_old_value

def __enter__(self) -> None:
self._prev = getattr(self.obj, self.field, _NULL)

def __exit__(self, *args: Any) -> None:
new: Any = getattr(self.obj, self.field, _NULL)
if not _check_field_equality(type(self.obj), self.field, self._prev, new):
self.signal.emit(new)
if self.emit_old_value:
self.signal.emit(new, self._prev)
else:
self.signal.emit(new)


SetAttr = Callable[[Any, str, Any], None]


@overload
def evented_setattr(signal_group_name: str, super_setattr: SetAttr) -> SetAttr:
def evented_setattr(signal_group_name: str, super_setattr: SetAttr, emit_old_value: bool = False) -> SetAttr:
...


@overload
def evented_setattr(
signal_group_name: str, super_setattr: Literal[None] | None = None
signal_group_name: str, super_setattr: Literal[None] | None = None, emit_old_value: bool = False
) -> Callable[[SetAttr], SetAttr]:
...


def evented_setattr(
signal_group_name: str, super_setattr: SetAttr | None = None
signal_group_name: str, super_setattr: SetAttr | None = None, emit_old_value: bool = False
) -> SetAttr | Callable[[SetAttr], SetAttr]:
"""Create a new __setattr__ method that emits events when fields change.
Expand Down Expand Up @@ -235,6 +239,9 @@ def __getattr__(self, name: str) -> SignalInstanceProtocol: ...
default "_psygnal_group_".
super_setattr: Callable
The original __setattr__ method for the class.
emit_old_value: bool
When the field is mutated, emit two parameters, the new value and the old value, by
default False
"""

def _inner(super_setattr: SetAttr) -> SetAttr:
Expand All @@ -253,7 +260,7 @@ def _setattr_and_emit_(self: object, name: str, value: Any) -> None:
if group is None or signal is None or len(signal) < 2 and not len(group):
return super_setattr(self, name, value)

with _changes_emitted(self, name, signal):
with _changes_emitted(self, name, signal, emit_old_value):
super_setattr(self, name, value)

setattr(_setattr_and_emit_, PATCHED_BY_PSYGNAL, True)
Expand Down Expand Up @@ -327,6 +334,9 @@ def __setattr__(self, name: str, value: Any) -> None:
events when fields change. If `False`, no `__setattr__` method will be
created. (This will prevent signal emission, and assumes you are using a
different mechanism to emit signals when fields change.)
emit_old_value: bool
When the field is mutated, emit two parameters, the new value and the old value, by
default False
Examples
--------
Expand Down Expand Up @@ -355,13 +365,15 @@ def __init__(
warn_on_no_fields: bool = True,
cache_on_instance: bool = True,
patch_setattr: bool = True,
emit_old_value: bool = False,
):
self._signal_group = signal_group_class
self._name: str | None = None
self._eqop = tuple(equality_operators.items()) if equality_operators else None
self._warn_on_no_fields = warn_on_no_fields
self._cache_on_instance = cache_on_instance
self._patch_setattr = patch_setattr
self._emit_old_value = emit_old_value

def __set_name__(self, owner: type, name: str) -> None:
"""Called when this descriptor is added to class `owner` as attribute `name`."""
Expand All @@ -380,7 +392,7 @@ def _do_patch_setattr(self, owner: type) -> None:
try:
# assign a new __setattr__ method to the class
owner.__setattr__ = evented_setattr( # type: ignore
self._name, owner.__setattr__ # type: ignore
self._name, owner.__setattr__, self._emit_old_value # type: ignore
)
except Exception as e: # pragma: no cover
# not sure what might cause this ... but it will have consequences
Expand Down

0 comments on commit 4f73d95

Please sign in to comment.