Skip to content

Commit bc67683

Browse files
committed
Copyable classes with bindable properties
1 parent 89ff381 commit bc67683

File tree

2 files changed

+64
-2
lines changed

2 files changed

+64
-2
lines changed

nicegui/binding.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import asyncio
2+
import copyreg
23
import time
34
from collections import defaultdict
45
from collections.abc import Mapping
5-
from typing import Any, Callable, DefaultDict, Dict, Iterable, List, Optional, Set, Tuple, Union
6+
from typing import Any, Callable, DefaultDict, Dict, Iterable, List, Optional, Set, Tuple, Type, TypeVar, Union
67

78
from . import core
89
from .logging import log
@@ -11,8 +12,10 @@
1112

1213
bindings: DefaultDict[Tuple[int, str], List] = defaultdict(list)
1314
bindable_properties: Dict[Tuple[int, str], Any] = {}
15+
copyable_classes: Set[Type] = set()
1416
active_links: List[Tuple[Any, str, Any, str, Callable[[Any], Any]]] = []
1517

18+
T = TypeVar('T')
1619

1720
def _has_attribute(obj: Union[object, Mapping], name: str) -> Any:
1821
if isinstance(obj, Mapping):
@@ -145,6 +148,8 @@ def __get__(self, owner: Any, _=None) -> Any:
145148

146149
def __set__(self, owner: Any, value: Any) -> None:
147150
has_attr = hasattr(owner, '___' + self.name)
151+
if not has_attr and type(owner) not in copyable_classes:
152+
_make_copyable(type(owner))
148153
value_changed = has_attr and getattr(owner, '___' + self.name) != value
149154
if has_attr and not value_changed:
150155
return
@@ -187,3 +192,41 @@ def reset() -> None:
187192
bindings.clear()
188193
bindable_properties.clear()
189194
active_links.clear()
195+
196+
197+
def _register_bindables(original_obj: T, copy_obj: T) -> None:
198+
"""Ensure BindableProperties of an object copy are registered correctly.
199+
200+
:param original_obj: The object that was copied.
201+
:param original_obj: The object copy.
202+
"""
203+
for attr_name in dir(original_obj):
204+
if (id(original_obj), attr_name) in bindable_properties:
205+
bindable_properties[(id(copy_obj), attr_name)] = copy_obj
206+
207+
208+
def _register_bindables_pickle_function(obj: T) -> Tuple[Callable[..., T], Tuple[Any, ...]]:
209+
"""Construct the "reduce tuple" of an object with a wrapped pickle function, that registers bindable attributes"
210+
211+
:param obj: The object to be reduced.
212+
"""
213+
reduced = obj.__reduce__()
214+
if isinstance(reduced, str):
215+
raise ValueError('Unexpected __reduce__() return type: str')
216+
creator = reduced[0]
217+
218+
def creator_with_hook(*args, **kwargs) -> T:
219+
obj_copy = creator(*args, **kwargs)
220+
_register_bindables(obj, obj_copy)
221+
return obj_copy
222+
223+
return (creator_with_hook,) + reduced[1:]
224+
225+
226+
def _make_copyable(cls: Type[T]) -> None:
227+
"""Modify the way `copy` module handles class instances so that `BindableProperty` attributes preserve bindability.
228+
229+
:param cls: The class to modify.
230+
"""
231+
copyreg.pickle(cls, _register_bindables_pickle_function)
232+
copyable_classes.add(cls)

tests/test_binding.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import copy
12
from typing import Dict, Optional, Tuple
23

34
from selenium.webdriver.common.keys import Keys
45

5-
from nicegui import ui
6+
from nicegui import binding, ui
67
from nicegui.testing import Screen
78

89

@@ -105,3 +106,21 @@ def test_missing_target_attribute(screen: Screen):
105106

106107
screen.open('/')
107108
screen.should_contain("text='Hello'")
109+
110+
111+
def test_copy_instance_with_bindable_property():
112+
class TestClass:
113+
x = binding.BindableProperty()
114+
y = binding.BindableProperty()
115+
116+
def __init__(self):
117+
self.x = 1
118+
self.y = 2
119+
120+
original = TestClass()
121+
duplicate = copy.copy(original)
122+
123+
assert (id(original), 'x') in binding.bindable_properties
124+
assert (id(original), 'y') in binding.bindable_properties
125+
assert (id(duplicate), 'x') in binding.bindable_properties
126+
assert (id(duplicate), 'y') in binding.bindable_properties

0 commit comments

Comments
 (0)