Skip to content
Merged
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
10 changes: 8 additions & 2 deletions observ/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from functools import partial, wraps
from itertools import count
from typing import Any, Callable, Generic, Optional, TypeVar, Union
from weakref import WeakSet, ref
from weakref import ref

from .dep import Dep
from .proxy import Proxy, proxy
Expand Down Expand Up @@ -219,7 +219,13 @@ def __init__(
# or a list of proxies
if deep is None:
deep = True
self._deps, self._new_deps = WeakSet(), WeakSet()
# Plain sets: WeakSet operations are implemented in Python and
# dominate the cost of re-collecting deps on every evaluation.
# Strong references are safe here: deps don't reference watchers
# strongly (Dep._subs is a WeakSet), and a dep whose container
# was garbage collected is dropped on the next cleanup_deps()
# or when the watcher is deactivated or collected.
self._deps, self._new_deps = set(), set()
self._tasks = set()

self.sync = sync
Expand Down
33 changes: 32 additions & 1 deletion tests/test_deps.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from observ import computed, reactive
from unittest.mock import Mock

from observ import computed, reactive, watch
from observ.proxy import proxy_db


Expand Down Expand Up @@ -53,3 +55,32 @@ def prop():

state.clear()
assert len(proxy_db.attrs(state)["keydep"]) == 3


def test_deps_released_after_reevaluation():
# Watchers hold strong references to their deps, so check that
# deps of containers that the watched expression no longer visits
# are released when the watcher re-evaluates
state = reactive({"foo": {"bar": 5}})
watcher = watch(lambda: state, Mock(), sync=True, deep=True)

# The deep watcher depends on the outer and the nested container
assert len(watcher._deps) == 2

del state["foo"]

# The sync watcher re-evaluated and dropped
# the dep of the nested container
assert len(watcher._deps) == 1


def test_deps_released_on_deactivation():
state = reactive({"foo": 5})
watcher = watch(lambda: state["foo"], Mock(), sync=True)

assert len(watcher._deps) > 0

# Deactivating the watcher releases its deps
watcher()

assert len(watcher._deps) == 0
Loading