Skip to content

Commit 6f6ba0f

Browse files
andybayerAndreas Bayerfalkoschindler
authored
Automatic unregistering of BindableProperty objects (#4122)
First draft to fix the issue reported in #4109. Replaces the values in the `binding.bindable_properties` data structure, which acts as a "registry" for available bindable properties, with `weakref.finalize` objects. Previously, there was a permanent reference to the owner of the `BindableProperty`, which was never cleared unless explicitly removed with `binding.remove`. I also added some very basic tests for this behavior. You may need to refactor these slightly. *What I did not test, and what in theory should still be a problem, is when 2 models have bindable properties and one model binds to a value of the other. Then permanent references to the models are kept in `binding.bindings`, which are never automatically cleaned up.* --------- Co-authored-by: Andreas Bayer <[email protected]> Co-authored-by: Falko Schindler <[email protected]>
1 parent 786e24f commit 6f6ba0f

File tree

2 files changed

+40
-4
lines changed

2 files changed

+40
-4
lines changed

nicegui/binding.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import dataclasses
55
import time
6+
import weakref
67
from collections import defaultdict
78
from typing import (
89
TYPE_CHECKING,
@@ -32,7 +33,7 @@
3233
MAX_PROPAGATION_TIME = 0.01
3334

3435
bindings: DefaultDict[Tuple[int, str], List] = defaultdict(list)
35-
bindable_properties: Dict[Tuple[int, str], Any] = {}
36+
bindable_properties: Dict[Tuple[int, str], weakref.finalize] = {}
3637
active_links: List[Tuple[Any, str, Any, str, Callable[[Any], Any]]] = []
3738

3839
T = TypeVar('T', bound=type)
@@ -173,7 +174,8 @@ def __set__(self, owner: Any, value: Any) -> None:
173174
if has_attr and not value_changed:
174175
return
175176
setattr(owner, '___' + self.name, value)
176-
bindable_properties[(id(owner), self.name)] = owner
177+
key = (id(owner), str(self.name))
178+
bindable_properties.setdefault(key, weakref.finalize(owner, lambda: bindable_properties.pop(key, None)))
177179
_propagate(owner, self.name)
178180
if value_changed and self._change_handler is not None:
179181
self._change_handler(owner, value)
@@ -198,9 +200,10 @@ def remove(objects: Iterable[Any]) -> None:
198200
]
199201
if not binding_list:
200202
del bindings[key]
201-
for (obj_id, name), obj in list(bindable_properties.items()):
202-
if id(obj) in object_ids:
203+
for (obj_id, name), finalizer in list(bindable_properties.items()):
204+
if obj_id in object_ids:
203205
del bindable_properties[(obj_id, name)]
206+
finalizer.detach()
204207

205208

206209
def reset() -> None:

tests/test_binding.py

+33
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import weakref
12
from typing import Dict, Optional, Tuple
23

34
from selenium.webdriver.common.keys import Keys
@@ -125,3 +126,35 @@ class TestClass:
125126
assert len(binding.bindings) == 2
126127
assert len(binding.active_links) == 1
127128
assert binding.active_links[0][1] == 'not_bindable'
129+
130+
131+
def test_automatic_cleanup(screen: Screen):
132+
class Model:
133+
value = binding.BindableProperty()
134+
135+
def __init__(self, value: str) -> None:
136+
self.value = value
137+
138+
def create_model_and_label(value: str) -> Tuple[Model, weakref.ref, ui.label]:
139+
model = Model(value)
140+
label = ui.label(value).bind_text(model, 'value')
141+
return id(model), weakref.ref(model), label
142+
143+
model_id1, ref1, label1 = create_model_and_label('first label')
144+
model_id2, ref2, _label2 = create_model_and_label('second label')
145+
146+
def is_alive(ref: weakref.ref) -> bool:
147+
return ref() is not None
148+
149+
def has_bindable_property(model_id: int) -> bool:
150+
return any(obj_id == model_id for obj_id, _ in binding.bindable_properties)
151+
152+
screen.open('/')
153+
screen.should_contain('first label')
154+
screen.should_contain('second label')
155+
assert is_alive(ref1) and has_bindable_property(model_id1)
156+
assert is_alive(ref2) and has_bindable_property(model_id2)
157+
158+
binding.remove([label1])
159+
assert not is_alive(ref1) and not has_bindable_property(model_id1)
160+
assert is_alive(ref2) and has_bindable_property(model_id2)

0 commit comments

Comments
 (0)