|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
3 | 3 | import asyncio
|
| 4 | +import copyreg |
4 | 5 | import dataclasses
|
5 | 6 | import time
|
6 | 7 | from collections import defaultdict
|
|
33 | 34 |
|
34 | 35 | bindings: DefaultDict[Tuple[int, str], List] = defaultdict(list)
|
35 | 36 | bindable_properties: Dict[Tuple[int, str], Any] = {}
|
| 37 | +copyable_classes: Set[Type] = set() |
36 | 38 | active_links: List[Tuple[Any, str, Any, str, Callable[[Any], Any]]] = []
|
37 | 39 |
|
38 |
| -T = TypeVar('T', bound=type) |
39 |
| - |
| 40 | +TC = TypeVar('TC', bound=type) |
| 41 | +T = TypeVar('T') |
40 | 42 |
|
41 | 43 | def _has_attribute(obj: Union[object, Mapping], name: str) -> Any:
|
42 | 44 | if isinstance(obj, Mapping):
|
@@ -169,6 +171,8 @@ def __get__(self, owner: Any, _=None) -> Any:
|
169 | 171 |
|
170 | 172 | def __set__(self, owner: Any, value: Any) -> None:
|
171 | 173 | has_attr = hasattr(owner, '___' + self.name)
|
| 174 | + if not has_attr and type(owner) not in copyable_classes: |
| 175 | + _make_copyable(type(owner)) |
172 | 176 | value_changed = has_attr and getattr(owner, '___' + self.name) != value
|
173 | 177 | if has_attr and not value_changed:
|
174 | 178 | return
|
@@ -214,7 +218,7 @@ def reset() -> None:
|
214 | 218 |
|
215 | 219 |
|
216 | 220 | @dataclass_transform()
|
217 |
| -def bindable_dataclass(cls: Optional[T] = None, /, *, |
| 221 | +def bindable_dataclass(cls: Optional[TC] = None, /, *, |
218 | 222 | bindable_fields: Optional[Iterable[str]] = None,
|
219 | 223 | **kwargs: Any) -> Union[Type[DataclassInstance], IdentityFunction]:
|
220 | 224 | """A decorator that transforms a class into a dataclass with bindable fields.
|
@@ -252,3 +256,41 @@ def wrap(cls_):
|
252 | 256 | bindable_property.__set_name__(dataclass, field_name)
|
253 | 257 | setattr(dataclass, field_name, bindable_property)
|
254 | 258 | return dataclass
|
| 259 | + |
| 260 | + |
| 261 | +def _register_bindables(original_obj: T, copy_obj: T) -> None: |
| 262 | + """Ensure BindableProperties of an object copy are registered correctly. |
| 263 | +
|
| 264 | + :param original_obj: The object that was copied. |
| 265 | + :param original_obj: The object copy. |
| 266 | + """ |
| 267 | + for attr_name in dir(original_obj): |
| 268 | + if (id(original_obj), attr_name) in bindable_properties: |
| 269 | + bindable_properties[(id(copy_obj), attr_name)] = copy_obj |
| 270 | + |
| 271 | + |
| 272 | +def _register_bindables_pickle_function(obj: T) -> Tuple[Callable[..., T], Tuple[Any, ...]]: |
| 273 | + """Construct the "reduce tuple" of an object with a wrapped pickle function, that registers bindable attributes" |
| 274 | +
|
| 275 | + :param obj: The object to be reduced. |
| 276 | + """ |
| 277 | + reduced = obj.__reduce__() |
| 278 | + if isinstance(reduced, str): |
| 279 | + raise ValueError('Unexpected __reduce__() return type: str') |
| 280 | + creator = reduced[0] |
| 281 | + |
| 282 | + def creator_with_hook(*args, **kwargs) -> T: |
| 283 | + obj_copy = creator(*args, **kwargs) |
| 284 | + _register_bindables(obj, obj_copy) |
| 285 | + return obj_copy |
| 286 | + |
| 287 | + return (creator_with_hook,) + reduced[1:] |
| 288 | + |
| 289 | + |
| 290 | +def _make_copyable(cls: Type[T]) -> None: |
| 291 | + """Modify the way `copy` module handles class instances so that `BindableProperty` attributes preserve bindability. |
| 292 | +
|
| 293 | + :param cls: The class to modify. |
| 294 | + """ |
| 295 | + copyreg.pickle(cls, _register_bindables_pickle_function) |
| 296 | + copyable_classes.add(cls) |
0 commit comments