From 24859641affe04cc358adba47ea8ba447350263b Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Fri, 5 Jan 2024 14:43:07 +0100 Subject: [PATCH] support magic methods --- observ/object_proxy.py | 152 ++++++++++++++++++++++++++++++++++++- tests/test_object_proxy.py | 8 ++ tests/test_usage.py | 33 ++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) diff --git a/observ/object_proxy.py b/observ/object_proxy.py index 4acaee4..0d240b7 100644 --- a/observ/object_proxy.py +++ b/observ/object_proxy.py @@ -7,7 +7,7 @@ from .traps import ReadonlyError -class ObjectProxy(Proxy): +class ObjectProxyBase(Proxy): def __getattribute__(self, name): if name in Proxy.__slots__: return super().__getattribute__(name) @@ -86,6 +86,156 @@ def __delattr__(self, name): return retval +def passthrough(method): + def trap(self, *args, **kwargs): + fn = getattr(self.__target__, method, None) + if fn is None: + raise TypeError(f"object of type '{type(self)}' has no {method}") + return fn(*args, **kwargs) + + return trap + + +magic_methods = [ + "__abs__", + "__add__", + "__aenter__", + "__aexit__", + "__aiter__", + "__and__", + "__anext__", + "__annotations__", + "__await__", + "__bases__", + "__bool__", + "__buffer__", + "__bytes__", + "__call__", + "__ceil__", + "__class__", + "__class_getitem__", + # '__classcell__', + "__closure__", + "__code__", + "__complex__", + "__contains__", + "__defaults__", + # '__del__', + # '__delattr__', + "__delete__", + "__delitem__", + "__dict__", + "__dir__", + "__divmod__", + "__doc__", + "__enter__", + "__eq__", + "__exit__", + "__file__", + "__float__", + "__floor__", + "__floordiv__", + "__format__", + "__func__", + "__future__", + "__ge__", + "__get__", + # "__getattr__", + # '__getattribute__', + "__getitem__", + "__globals__", + "__gt__", + "__hash__", + "__iadd__", + "__iand__", + "__ifloordiv__", + "__ilshift__", + "__imatmul__", + "__imod__", + "__import__", + "__imul__", + "__index__", + # '__init__', + "__init_subclass__", + "__instancecheck__", + "__int__", + "__invert__", + "__ior__", + "__ipow__", + "__irshift__", + "__isub__", + "__iter__", + "__itruediv__", + "__ixor__", + "__kwdefaults__", + "__le__", + "__len__", + "__length_hint__", + "__lshift__", + "__lt__", + "__match_args__", + "__matmul__", + "__missing__", + "__mod__", + "__module__", + "__mro__", + "__mro_entries__", + "__mul__", + "__name__", + "__ne__", + "__neg__", + # '__new__', + "__next__", + "__objclass__", + "__or__", + "__pos__", + "__pow__", + "__prepare__", + # '__qualname__', + "__radd__", + "__rand__", + "__rdivmod__", + "__release_buffer__", + "__repr__", + "__reversed__", + "__rfloordiv__", + "__rlshift__", + "__rmatmul__", + "__rmod__", + "__rmul__", + "__ror__", + "__round__", + "__rpow__", + "__rrshift__", + "__rshift__", + "__rsub__", + "__rtruediv__", + "__rxor__", + "__self__", + "__set__", + "__set_name__", + # '__setattr__', + "__setitem__", + # '__slots__', + "__str__", + "__sub__", + "__subclasscheck__", + "__traceback__", + "__truediv__", + "__trunc__", + "__type_params__", + "__weakref__", + "__xor__", +] + + +ObjectProxy = type( + "ObjectProxy", + (ObjectProxyBase,), + {method: passthrough(method) for method in magic_methods}, +) + + class ReadonlyObjectProxy(ObjectProxy): def __init__(self, target, shallow=False, **kwargs): super().__init__(target, shallow=shallow, **{**kwargs, "readonly": True}) diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index f71c28b..c67724a 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -75,3 +75,11 @@ def test_readonly_proxy(): another_proxied.bar = 25 assert proxied.bar == 25 + + with pytest.raises(ReadonlyError): + delattr(readonly_proxy, "baz") + delattr(another_proxied, "baz") + + assert not hasattr(proxied, "baz") + + diff --git a/tests/test_usage.py b/tests/test_usage.py index 8e6cff4..14aae14 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import Iterable from unittest.mock import Mock @@ -596,6 +597,9 @@ def test_usage_class_instances(): class Foo: def __init__(self): self.foo = 5 + + def __len__(self): + return self.foo a = reactive([1, 2, Foo()]) called = 0 @@ -621,6 +625,35 @@ def _callback(): a[2].foo = 10 assert called == 3 + # magic methods are supported + foo_len = computed(lambda: len(a[2])) + assert foo_len() == 10 + + +def test_usage_dataclass(): + @dataclass + class Foo: + bar: int + + a = reactive(Foo(bar=5)) + called = 0 + + def _callback(): + nonlocal called + called += 1 + + watcher = watch(lambda: a, _callback, sync=True, deep=True) + assert not watcher.dirty + assert called == 0 + + # write something + a.bar = 10 + assert called == 1 + + # magic methods are supported + str_foo = computed(lambda: repr(a)) + assert str_foo() == "test_usage_dataclass..Foo(bar=10)" + def test_watch_get_non_existing(): a = reactive({})