Skip to content

Commit

Permalink
support magic methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Korijn committed Jan 5, 2024
1 parent 52fd892 commit 2485964
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 1 deletion.
152 changes: 151 additions & 1 deletion observ/object_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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})
Expand Down
8 changes: 8 additions & 0 deletions tests/test_object_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")


33 changes: 33 additions & 0 deletions tests/test_usage.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import dataclass
from typing import Iterable
from unittest.mock import Mock

Expand Down Expand Up @@ -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
Expand All @@ -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.<locals>.Foo(bar=10)"


def test_watch_get_non_existing():
a = reactive({})
Expand Down

0 comments on commit 2485964

Please sign in to comment.