Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object proxies #120

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion observ/dict_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,9 @@ def readonly_dict_proxy_init(self, target, shallow=False, **kwargs):
},
)

TYPE_LOOKUP[dict] = (DictProxy, ReadonlyDictProxy)

def type_test(target):
return isinstance(target, dict)


TYPE_LOOKUP[type_test] = (DictProxy, ReadonlyDictProxy)
6 changes: 5 additions & 1 deletion observ/list_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,8 @@ def readonly_list_proxy_init(self, target, shallow=False, **kwargs):
)


TYPE_LOOKUP[list] = (ListProxy, ReadonlyListProxy)
def type_test(target):
return isinstance(target, list)


TYPE_LOOKUP[type_test] = (ListProxy, ReadonlyListProxy)
98 changes: 98 additions & 0 deletions observ/object_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from .proxy import TYPE_LOOKUP, Proxy
from .proxy_db import proxy_db
from .traps import construct_methods_traps_dict, trap_map, trap_map_readonly

######################
# UNTRAPPED ATTRIBUTES
######################

# CREATION HOOKS:
# '__init__',
# '__init_subclass__',
# '__new__',

# PICKLE HOOKS:
# '__reduce__',
# '__reduce_ex__',
# '__getstate__',

# WEAKREF STORAGE:
# '__weakref__',

# FORMATTING HOOKS:
# '__format__',

# INTERNALS:
# '__dict__',

###########
# QUESTIONS
###########
# I don't think we can maintain per-key reactivity
# like we do with dicts. For starters we can't
# query the initial state since there is no .keys()

# There are many _optional_ magic methods, such as __iter__
# How do we support those without overcomplicating the traps?



object_traps = {
"READERS": {
'__class__',
'__dir__',
'__doc__',
'__eq__',
'__ge__',
'__gt__',
'__hash__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__repr__',
'__sizeof__',
'__str__',
'__subclasshook__',
# also fires for method access :/
'__getattribute__',
},
"WRITERS": {
'__setattr__',
"__delattr__",
},
}


class ObjectProxyBase(Proxy[object]):
def _orphaned_keydeps(self):
return set(proxy_db.attrs(self)["keydep"].keys()) - set(vars(self.target).keys())


def readonly_object_proxy_init(self, target, shallow=False, **kwargs):
super(ReadonlyObjectProxy, self).__init__(
target, shallow=shallow, **{**kwargs, "readonly": True}
)


ObjectProxy = type(
"ObjectProxy",
(ObjectProxyBase,),
construct_methods_traps_dict(object, object_traps, trap_map),
)
ReadonlyObjectProxy = type(
"ReadonlyObjectProxy",
(ObjectProxyBase,),
{
"__init__": readonly_object_proxy_init,
**construct_methods_traps_dict(object, object_traps, trap_map_readonly),
},
)


def type_test(target):
# exclude builtin objects
return isinstance(target, object) and target.__module__ != object.__module__


TYPE_LOOKUP[type_test] = (ObjectProxy, ReadonlyObjectProxy)
4 changes: 2 additions & 2 deletions observ/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ def proxy(target: T, readonly=False, shallow=False) -> T:
return existing_proxy

# Create a new proxy
for target_type, (writable_proxy_type, readonly_proxy_type) in TYPE_LOOKUP.items():
if isinstance(target, target_type):
for type_test, (writable_proxy_type, readonly_proxy_type) in TYPE_LOOKUP.items():
if type_test(target):
proxy_type = readonly_proxy_type if readonly else writable_proxy_type
return proxy_type(target, readonly=readonly, shallow=shallow)

Expand Down
7 changes: 6 additions & 1 deletion observ/set_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,9 @@ def readonly_set_proxy_init(self, target, shallow=False, **kwargs):
},
)

TYPE_LOOKUP[set] = (SetProxy, ReadonlySetProxy)

def type_test(target):
return isinstance(target, set)


TYPE_LOOKUP[type_test] = (SetProxy, ReadonlySetProxy)
7 changes: 7 additions & 0 deletions observ/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .dep import Dep
from .dict_proxy import DictProxyBase
from .list_proxy import ListProxyBase
from .object_proxy import ObjectProxyBase
from .scheduler import scheduler
from .set_proxy import SetProxyBase

Expand Down Expand Up @@ -83,8 +84,14 @@ def traverse(obj, seen=None):
val_iter = iter(obj.values())
elif isinstance(obj, (list, ListProxyBase, set, SetProxyBase, tuple)):
val_iter = iter(obj)
elif isinstance(obj, (object, ObjectProxyBase)):
try:
val_iter = iter(vars(obj).values())
except TypeError:
return
else:
return

# track which objects we have already seen to support(!) full traversal
# of datastructures with cycles
# NOTE: a set would provide faster containment checks
Expand Down
16 changes: 16 additions & 0 deletions tests/test_object_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from dataclasses import dataclass

from observ.object_proxy import ObjectProxy
from observ.proxy import proxy
from observ.traps import ReadonlyError


@dataclass
class Foo:
bar: int


def test_dataclass_proxy():
data = Foo(bar=5)
proxied = proxy(data)
assert isinstance(proxied, ObjectProxy)
Loading