|
1 | 1 | import asyncio
|
| 2 | +import copyreg |
2 | 3 | import time
|
3 | 4 | from collections import defaultdict
|
4 | 5 | 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 |
6 | 7 |
|
7 | 8 | from . import core
|
8 | 9 | from .logging import log
|
|
11 | 12 |
|
12 | 13 | bindings: DefaultDict[Tuple[int, str], List] = defaultdict(list)
|
13 | 14 | bindable_properties: Dict[Tuple[int, str], Any] = {}
|
| 15 | +copyable_classes: Set[Type] = set() |
14 | 16 | active_links: List[Tuple[Any, str, Any, str, Callable[[Any], Any]]] = []
|
15 | 17 |
|
| 18 | +T = TypeVar('T') |
16 | 19 |
|
17 | 20 | def _has_attribute(obj: Union[object, Mapping], name: str) -> Any:
|
18 | 21 | if isinstance(obj, Mapping):
|
@@ -145,6 +148,8 @@ def __get__(self, owner: Any, _=None) -> Any:
|
145 | 148 |
|
146 | 149 | def __set__(self, owner: Any, value: Any) -> None:
|
147 | 150 | has_attr = hasattr(owner, '___' + self.name)
|
| 151 | + if not has_attr and type(owner) not in copyable_classes: |
| 152 | + _make_copyable(type(owner)) |
148 | 153 | value_changed = has_attr and getattr(owner, '___' + self.name) != value
|
149 | 154 | if has_attr and not value_changed:
|
150 | 155 | return
|
@@ -187,3 +192,41 @@ def reset() -> None:
|
187 | 192 | bindings.clear()
|
188 | 193 | bindable_properties.clear()
|
189 | 194 | 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) |
0 commit comments