Skip to content

Commit

Permalink
Use __slots__ in Dep, Watcher, ProxyDb and Scheduler, and make all va…
Browse files Browse the repository at this point in the history
…riable lookups local (#101)

* use __slots__ in Dep

* use __slots__ in Watcher

* use __slots__ in ProxyDb

* use __slots__ in Scheduler

* make all variable lookups local
  • Loading branch information
Korijn authored Nov 3, 2023
1 parent 9a234e4 commit b780c35
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 50 deletions.
1 change: 1 addition & 0 deletions observ/dep.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@


class Dep:
__slots__ = ["_subs", "__weakref__"]
stack: List["Watcher"] = [] # noqa: F821

def __init__(self) -> None:
Expand Down
4 changes: 3 additions & 1 deletion observ/proxy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from functools import partial

from .dep import Dep
from .proxy_db import proxy_db


Expand All @@ -16,14 +17,15 @@ class Proxy:
"""

__hash__ = None
__slots__ = ["target", "readonly", "shallow", "proxy_db", "__weakref__"]
__slots__ = ["target", "readonly", "shallow", "proxy_db", "Dep", "__weakref__"]

def __init__(self, target, readonly=False, shallow=False):
self.target = target
self.readonly = readonly
self.shallow = shallow
self.proxy_db = proxy_db
self.proxy_db.reference(self)
self.Dep = Dep

def __del__(self):
self.proxy_db.dereference(self)
Expand Down
2 changes: 2 additions & 0 deletions observ/proxy_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class ProxyDb:
removed from the collection.
"""

__slots__ = ["db"]

def __init__(self):
self.db = {}
gc.callbacks.append(self.cleanup)
Expand Down
12 changes: 12 additions & 0 deletions observ/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@


class Scheduler:
__slots__ = [
"_queue",
"_queue_indices",
"flushing",
"has",
"circular",
"index",
"waiting",
"request_flush",
"detect_cycles",
]

def __init__(self):
self._queue = []
self._queue_indices = []
Expand Down
6 changes: 3 additions & 3 deletions observ/traps.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def read_trap(method, obj_cls):

@wraps(fn)
def trap(self, *args, **kwargs):
if Dep.stack:
if self.Dep.stack:
self.proxy_db.attrs(self)["dep"].depend()
value = fn(self.target, *args, **kwargs)
if self.shallow:
Expand All @@ -33,7 +33,7 @@ def iterate_trap(method, obj_cls):

@wraps(fn)
def trap(self, *args, **kwargs):
if Dep.stack:
if self.Dep.stack:
self.proxy_db.attrs(self)["dep"].depend()
iterator = fn(self.target, *args, **kwargs)
if self.shallow:
Expand All @@ -54,7 +54,7 @@ def read_key_trap(method, obj_cls):

@wraps(fn)
def trap(self, *args, **kwargs):
if Dep.stack:
if self.Dep.stack:
key = args[0]
keydeps = self.proxy_db.attrs(self)["keydep"]
if key not in keydeps:
Expand Down
28 changes: 23 additions & 5 deletions observ/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def decorator_computed(fn: T) -> T:
def getter():
if watcher.dirty:
watcher.evaluate()
if Dep.stack:
if watcher.Dep.stack:
watcher.depend()
return watcher.value

Expand Down Expand Up @@ -114,6 +114,23 @@ class WrongNumberOfArgumentsError(TypeError):


class Watcher:
__slots__ = [
"id",
"fn",
"_deps",
"_new_deps",
"sync",
"callback",
"no_recurse",
"deep",
"lazy",
"dirty",
"value",
"Dep",
"_number_of_callback_args",
"__weakref__",
]

def __init__(
self,
fn: Callable[[], Any] | Proxy | list[Proxy],
Expand All @@ -128,6 +145,7 @@ def __init__(
deep: Deep watch the watched value
callback: Method to call when value has changed
"""
self.Dep = Dep
self.id = next(_ids)
if callable(fn):
if is_bound_method(fn):
Expand Down Expand Up @@ -159,7 +177,7 @@ def update(self) -> None:
self.dirty = True
return

if Dep.stack and Dep.stack[-1] is self and self.no_recurse:
if self.Dep.stack and self.Dep.stack[-1] is self and self.no_recurse:
return
if self.sync:
self.run()
Expand Down Expand Up @@ -236,13 +254,13 @@ def _run_callback(self, *args) -> None:
del frames

def get(self) -> Any:
Dep.stack.append(self)
self.Dep.stack.append(self)
try:
value = self.fn()
if self.deep:
traverse(value)
finally:
Dep.stack.pop()
self.Dep.stack.pop()
self.cleanup_deps()
return value

Expand All @@ -262,7 +280,7 @@ def cleanup_deps(self) -> None:
def depend(self) -> None:
"""This function is used by other watchers to depend on everything
this watcher depends on."""
if Dep.stack:
if self.Dep.stack:
for dep in self._deps:
dep.depend()

Expand Down
111 changes: 70 additions & 41 deletions tests/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,19 @@
"__reduce__",
"__reduce_ex__",
"__hash__",
"_orphaned_keydeps",
"__class_getitem__",
# __del__ is custom method on Proxy
"__del__",
"__getstate__",
# custom method on DictProxyBase
"_orphaned_keydeps",
# Following attributes are part of Proxy.__slots__
"__slots__",
"target",
"readonly",
"shallow",
"proxy_db",
"Dep",
"__weakref__",
}

Expand Down Expand Up @@ -90,12 +92,13 @@ def test_list_notify():
"__iadd__": ([5],),
"__imul__": (5,),
}

for name in COLLECTIONS[ListProxy]["WRITERS"]:
coll = ListProxy([3, 2])
mock = Mock()
proxy_db.attrs(coll)["dep"].notify = mock
proxy_db.attrs(coll)["dep"].add_sub(mock)
getattr(coll, name)(*args[name])
mock.assert_called_once()
mock.update.assert_called_once()


def test_list_depend():
Expand Down Expand Up @@ -123,13 +126,12 @@ def test_list_depend():
"__sizeof__": (),
}
for name in COLLECTIONS[ListProxy]["READERS"]:
Dep.stack.append(Mock())
m = Mock()
Dep.stack.append(m)
try:
coll = ListProxy([2])
mock = Mock()
proxy_db.attrs(coll)["dep"].depend = mock
getattr(coll, name)(*args[name])
mock.assert_called()
m.add_dep.assert_called_once_with(proxy_db.attrs(coll)["dep"])
finally:
Dep.stack.pop()

Expand All @@ -149,9 +151,9 @@ def test_set_notify():
for name in COLLECTIONS[SetProxy]["WRITERS"]:
coll = SetProxy({2})
mock = Mock()
proxy_db.attrs(coll)["dep"].notify = mock
proxy_db.attrs(coll)["dep"].add_sub(mock)
getattr(coll, name)(*args[name])
mock.assert_called_once()
mock.update.assert_called_once()


def test_set_depend():
Expand Down Expand Up @@ -191,13 +193,12 @@ def test_set_depend():
"__xor__": ({3},),
}
for name in COLLECTIONS[SetProxy]["READERS"]:
Dep.stack.append(Mock())
m = Mock()
Dep.stack.append(m)
try:
coll = SetProxy({2})
mock = Mock()
proxy_db.attrs(coll)["dep"].depend = mock
getattr(coll, name)(*args[name])
mock.assert_called()
m.add_dep.assert_called_once_with(proxy_db.attrs(coll)["dep"])
finally:
Dep.stack.pop()

Expand All @@ -207,36 +208,52 @@ def test_dict_notify():
"update": ({5: 6},),
"__ior__": ({5: 6},),
}

mocks = {}

def new_mock(key=None):
mocks[key] = Mock()
return mocks[key]

for name in COLLECTIONS[DictProxy]["WRITERS"]:
mocks.clear()
coll = DictProxy({2: 3})
old_keys = set(coll.keys())
proxy_db.attrs(coll)["dep"].notify = Mock()
proxy_db.attrs(coll)["dep"].add_sub(new_mock())
for key in proxy_db.attrs(coll)["keydep"].keys():
proxy_db.attrs(coll)["keydep"][key].notify = Mock()
proxy_db.attrs(coll)["keydep"][key].add_sub(new_mock(key))
getattr(coll, name)(*args[name])
proxy_db.attrs(coll)["dep"].notify.assert_called_once()
mocks[None].update.assert_called_once()
for key in old_keys:
proxy_db.attrs(coll)["keydep"][key].notify.assert_not_called()
assert not mocks[key].method_calls


def test_dict_keynotify():
args = {
"setdefault": (3, 5),
"__setitem__": (2, 4),
}

mocks = {}

def new_mock(key=None):
mocks[key] = Mock()
return mocks[key]

for name in COLLECTIONS[DictProxy]["KEYWRITERS"]:
mocks.clear()
coll = DictProxy({2: 3})
key = args[name][0]
is_new_key = key not in proxy_db.attrs(coll)["keydep"]
proxy_db.attrs(coll)["dep"].notify = Mock()
proxy_db.attrs(coll)["dep"].add_sub(new_mock())
for k in proxy_db.attrs(coll)["keydep"].keys():
proxy_db.attrs(coll)["keydep"][k].notify = Mock()
proxy_db.attrs(coll)["keydep"][k].add_sub(new_mock(k))
getattr(coll, name)(*args[name])
proxy_db.attrs(coll)["dep"].notify.assert_called_once()
mocks[None].update.assert_called_once()
if is_new_key:
assert isinstance(proxy_db.attrs(coll)["keydep"][key], Dep)
else:
proxy_db.attrs(coll)["keydep"][key].notify.assert_called_once()
mocks[key].update.assert_called_once()


def test_dict_depend():
Expand All @@ -262,15 +279,14 @@ def test_dict_depend():
"__or__": ({},),
"__ior__": ({},),
}

for name in COLLECTIONS[DictProxy]["READERS"]:
Dep.stack.append(Mock())
m = Mock()
Dep.stack.append(m)
try:
coll = DictProxy({2: 3})
proxy_db.attrs(coll)["dep"].depend = Mock()
for k in proxy_db.attrs(coll)["keydep"].keys():
proxy_db.attrs(coll)["keydep"][k].depend = Mock()
getattr(coll, name)(*args[name])
proxy_db.attrs(coll)["dep"].depend.assert_called()
m.add_dep.assert_called_once_with(proxy_db.attrs(coll)["dep"])
finally:
Dep.stack.pop()

Expand All @@ -282,15 +298,14 @@ def test_dict_keydepend():
"__getitem__": (2,),
}
for name in COLLECTIONS[DictProxy]["KEYREADERS"]:
Dep.stack.append(None)
m = Mock()
Dep.stack.append(m)
try:
coll = DictProxy({2: 3})
proxy_db.attrs(coll)["dep"].depend = Mock()
for k in proxy_db.attrs(coll)["keydep"].keys():
proxy_db.attrs(coll)["keydep"][k].depend = Mock()
getattr(coll, name)(*args[name])
proxy_db.attrs(coll)["dep"].depend.assert_not_called()
proxy_db.attrs(coll)["keydep"][args[name][0]].depend.assert_called_once()
m.add_dep.assert_called_once_with(
proxy_db.attrs(coll)["keydep"][args[name][0]]
)
finally:
Dep.stack.pop()

Expand All @@ -300,13 +315,21 @@ def test_dict_delete_notify():
"clear": (),
"popitem": (),
}

mocks = {}

def new_mock(key=None):
mocks[key] = Mock()
return mocks[key]

for name in COLLECTIONS[DictProxy]["DELETERS"]:
mocks.clear()
coll = DictProxy({2: 3})
proxy_db.attrs(coll)["dep"].notify = Mock()
proxy_db.attrs(coll)["dep"].add_sub(new_mock())
for key in proxy_db.attrs(coll)["keydep"].keys():
proxy_db.attrs(coll)["keydep"][key].notify = Mock()
proxy_db.attrs(coll)["keydep"][key].add_sub(new_mock(key))
getattr(coll, name)(*args[name])
proxy_db.attrs(coll)["dep"].notify.assert_called_once()
mocks[None].update.assert_called_once()
assert len(proxy_db.attrs(coll)["keydep"]) == 0


Expand All @@ -315,15 +338,21 @@ def test_dict_delete_keynotify():
"pop": (2,),
"__delitem__": (2,),
}

mocks = {}

def new_mock(key=None):
mocks[key] = Mock()
return mocks[key]

for name in COLLECTIONS[DictProxy]["KEYDELETERS"]:
mocks.clear()
coll = DictProxy({2: 3})
key = args[name][0]
proxy_db.attrs(coll)["dep"].notify = Mock()
keymock = Mock()
proxy_db.attrs(coll)["keydep"][key] = keymock
proxy_db.attrs(coll)["dep"].add_sub(new_mock())
for k in proxy_db.attrs(coll)["keydep"].keys():
proxy_db.attrs(coll)["keydep"][k].notify = Mock()
proxy_db.attrs(coll)["keydep"][k].add_sub(new_mock(k))
getattr(coll, name)(*args[name])
proxy_db.attrs(coll)["dep"].notify.assert_called_once()
keymock.notify.assert_called_once()
mocks[None].update.assert_called_once()
mocks[key].update.assert_called_once()
assert len(proxy_db.attrs(coll)["keydep"]) == 0

0 comments on commit b780c35

Please sign in to comment.