pyglove 0.5.0.dev202510200810__py3-none-any.whl → 0.5.0.dev202512080812__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyglove might be problematic. Click here for more details.
- pyglove/core/geno/base.py +7 -3
- pyglove/core/io/file_system.py +452 -2
- pyglove/core/io/file_system_test.py +442 -0
- pyglove/core/symbolic/__init__.py +7 -0
- pyglove/core/symbolic/base.py +89 -35
- pyglove/core/symbolic/base_test.py +3 -3
- pyglove/core/symbolic/dict.py +31 -12
- pyglove/core/symbolic/dict_test.py +49 -0
- pyglove/core/symbolic/list.py +17 -3
- pyglove/core/symbolic/list_test.py +24 -2
- pyglove/core/symbolic/object.py +3 -1
- pyglove/core/symbolic/object_test.py +13 -10
- pyglove/core/symbolic/ref.py +19 -7
- pyglove/core/symbolic/ref_test.py +94 -7
- pyglove/core/symbolic/unknown_symbols.py +147 -0
- pyglove/core/symbolic/unknown_symbols_test.py +100 -0
- pyglove/core/typing/annotation_conversion.py +8 -1
- pyglove/core/typing/annotation_conversion_test.py +14 -19
- pyglove/core/typing/class_schema.py +4 -1
- pyglove/core/typing/type_conversion.py +17 -3
- pyglove/core/typing/type_conversion_test.py +7 -2
- pyglove/core/typing/value_specs.py +5 -1
- pyglove/core/typing/value_specs_test.py +5 -0
- pyglove/core/utils/__init__.py +1 -0
- pyglove/core/utils/json_conversion.py +360 -63
- pyglove/core/utils/json_conversion_test.py +146 -13
- pyglove/core/views/html/controls/tab.py +33 -0
- pyglove/core/views/html/controls/tab_test.py +37 -0
- pyglove/ext/evolution/base_test.py +1 -1
- {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202512080812.dist-info}/METADATA +8 -1
- {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202512080812.dist-info}/RECORD +34 -32
- {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202512080812.dist-info}/WHEEL +0 -0
- {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202512080812.dist-info}/licenses/LICENSE +0 -0
- {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202512080812.dist-info}/top_level.txt +0 -0
|
@@ -17,6 +17,7 @@ import abc
|
|
|
17
17
|
import base64
|
|
18
18
|
import collections
|
|
19
19
|
import contextlib
|
|
20
|
+
import dataclasses
|
|
20
21
|
import importlib
|
|
21
22
|
import inspect
|
|
22
23
|
import marshal
|
|
@@ -25,6 +26,7 @@ import types
|
|
|
25
26
|
import typing
|
|
26
27
|
from typing import Any, Callable, ContextManager, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union
|
|
27
28
|
|
|
29
|
+
|
|
28
30
|
# Nestable[T] is a (maybe) nested structure of T, which could be T, a Dict
|
|
29
31
|
# a List or a Tuple of Nestable[T]. We use a Union to fool PyType checker to
|
|
30
32
|
# make Nestable[T] a valid type annotation without type check.
|
|
@@ -186,6 +188,19 @@ class JSONConvertible(metaclass=abc.ABCMeta):
|
|
|
186
188
|
# Marker (as the first element of a list) for serializing tuples.
|
|
187
189
|
TUPLE_MARKER = '__tuple__'
|
|
188
190
|
|
|
191
|
+
# Marker (as the first element of a list or key of a dict) for symbolic
|
|
192
|
+
# lists and dicts.
|
|
193
|
+
SYMBOLIC_MARKER = '__symbolic__'
|
|
194
|
+
|
|
195
|
+
# Marker for references to shared objects.
|
|
196
|
+
REF_KEY = '__ref__'
|
|
197
|
+
|
|
198
|
+
# Marker for root value when JSONConversionContext is used.
|
|
199
|
+
ROOT_VALUE_KEY = '__root__'
|
|
200
|
+
|
|
201
|
+
# Marker for JSONConversionContext.
|
|
202
|
+
CONTEXT_KEY = '__context__'
|
|
203
|
+
|
|
189
204
|
# Type converter that converts a complex type to basic JSON value type.
|
|
190
205
|
# When this field is set by users, the converter will be invoked when a
|
|
191
206
|
# complex value cannot be serialized by existing methods.
|
|
@@ -215,7 +230,12 @@ class JSONConvertible(metaclass=abc.ABCMeta):
|
|
|
215
230
|
return cls(**init_args)
|
|
216
231
|
|
|
217
232
|
@abc.abstractmethod
|
|
218
|
-
def to_json(
|
|
233
|
+
def to_json(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
context: Optional['JSONConversionContext'] = None,
|
|
237
|
+
**kwargs
|
|
238
|
+
) -> JSONValueType:
|
|
219
239
|
"""Returns a plain Python value as a representation for this object.
|
|
220
240
|
|
|
221
241
|
A plain Python value are basic python types that can be serialized into
|
|
@@ -224,6 +244,7 @@ class JSONConvertible(metaclass=abc.ABCMeta):
|
|
|
224
244
|
Python values as their values.
|
|
225
245
|
|
|
226
246
|
Args:
|
|
247
|
+
context: JSON conversion context.
|
|
227
248
|
**kwargs: Keyword arguments as flags to control JSON conversion.
|
|
228
249
|
|
|
229
250
|
Returns:
|
|
@@ -389,8 +410,14 @@ class _OpaqueObject(JSONConvertible):
|
|
|
389
410
|
}, **kwargs)
|
|
390
411
|
|
|
391
412
|
@classmethod
|
|
392
|
-
def from_json(
|
|
393
|
-
|
|
413
|
+
def from_json(
|
|
414
|
+
cls,
|
|
415
|
+
json_value: JSONValueType,
|
|
416
|
+
*args,
|
|
417
|
+
context: Optional['JSONConversionContext'] = None,
|
|
418
|
+
**kwargs
|
|
419
|
+
) -> Any:
|
|
420
|
+
del args, context, kwargs
|
|
394
421
|
assert isinstance(json_value, dict) and 'value' in json_value, json_value
|
|
395
422
|
encoder = cls(json_value['value'], encoded=True)
|
|
396
423
|
return encoder.value
|
|
@@ -401,7 +428,169 @@ def registered_types() -> Iterable[Tuple[str, Type[JSONConvertible]]]:
|
|
|
401
428
|
return JSONConvertible.registered_types()
|
|
402
429
|
|
|
403
430
|
|
|
404
|
-
|
|
431
|
+
class JSONConversionContext(JSONConvertible):
|
|
432
|
+
"""JSON conversion context.
|
|
433
|
+
|
|
434
|
+
JSONConversionContext is introduced to handle serialization scenarios where
|
|
435
|
+
operations cannot be performed in a single pass. For example: Serialization
|
|
436
|
+
and deserialization of shared objects across different locations.
|
|
437
|
+
|
|
438
|
+
# Shared object serialization/deserialization.
|
|
439
|
+
|
|
440
|
+
In PyGlove, only values referenced by `pg.Ref` and non-PyGlove managed objects
|
|
441
|
+
are sharable. This ensures that multiple references to the same object are
|
|
442
|
+
serialized only once. During deserialization, the object is created just once
|
|
443
|
+
and shared among all references.
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
@dataclasses.dataclass
|
|
447
|
+
class ObjectEntry:
|
|
448
|
+
value: Any
|
|
449
|
+
serialized: Optional[JSONValueType]
|
|
450
|
+
ref_index: int
|
|
451
|
+
ref_count: int
|
|
452
|
+
|
|
453
|
+
def __init__(self,) -> None:
|
|
454
|
+
self._shared_objects: list[JSONConversionContext.ObjectEntry] = []
|
|
455
|
+
self._id_to_shared_object = {}
|
|
456
|
+
|
|
457
|
+
def get_shared(self, ref_index: int) -> ObjectEntry:
|
|
458
|
+
"""Gets the shared object of a ref index."""
|
|
459
|
+
return self._shared_objects[ref_index]
|
|
460
|
+
|
|
461
|
+
def add_shared(self, shared: ObjectEntry) -> None:
|
|
462
|
+
self._shared_objects.append(shared)
|
|
463
|
+
self._id_to_shared_object[id(shared.value)] = shared
|
|
464
|
+
|
|
465
|
+
def next_shared_index(self) -> int:
|
|
466
|
+
"""Returns the next shared index."""
|
|
467
|
+
return len(self._shared_objects)
|
|
468
|
+
|
|
469
|
+
def serialize_maybe_shared(
|
|
470
|
+
self,
|
|
471
|
+
value: Any,
|
|
472
|
+
json_fn: Optional[Callable[..., JSONValueType]] = None,
|
|
473
|
+
**kwargs
|
|
474
|
+
) -> JSONValueType:
|
|
475
|
+
"""Track maybe shared objects and returns their JSON representation."""
|
|
476
|
+
if json_fn is None:
|
|
477
|
+
json_fn = lambda **kwargs: to_json(value, **kwargs)
|
|
478
|
+
kwargs.pop('context', None)
|
|
479
|
+
value_id = id(value)
|
|
480
|
+
shared_object = self._id_to_shared_object.get(value_id)
|
|
481
|
+
if shared_object is None:
|
|
482
|
+
serialized = json_fn(context=self, **kwargs)
|
|
483
|
+
|
|
484
|
+
# It's possible that maybe_shared_json is called recursively on the same
|
|
485
|
+
# object, thus we need to check for self-references explicitly.
|
|
486
|
+
if (isinstance(serialized, dict)
|
|
487
|
+
and JSONConvertible.REF_KEY in serialized
|
|
488
|
+
and len(serialized) == 1):
|
|
489
|
+
return serialized
|
|
490
|
+
|
|
491
|
+
shared_object = self.ObjectEntry(
|
|
492
|
+
value=value,
|
|
493
|
+
serialized=serialized,
|
|
494
|
+
ref_index=self.next_shared_index(),
|
|
495
|
+
ref_count=0,
|
|
496
|
+
)
|
|
497
|
+
self._shared_objects.append(shared_object)
|
|
498
|
+
self._id_to_shared_object[value_id] = shared_object
|
|
499
|
+
shared_object.ref_count += 1
|
|
500
|
+
return {
|
|
501
|
+
JSONConvertible.REF_KEY: shared_object.ref_index
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
def _maybe_deref(self, serialized: Any, ref_index_map: dict[int, int]) -> Any:
|
|
505
|
+
"""In-place dereference ref-1 shared objects in an object tree.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
serialized: The object tree to dereference.
|
|
509
|
+
ref_index_map: A map from the original index of shared objects to their
|
|
510
|
+
indices after the ref-1 shared objects are trimmed.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
The (maybe) dereferenced object tree.
|
|
514
|
+
"""
|
|
515
|
+
if isinstance(serialized, dict):
|
|
516
|
+
ref_index = serialized.get(JSONConvertible.REF_KEY)
|
|
517
|
+
if ref_index is None:
|
|
518
|
+
for k, x in serialized.items():
|
|
519
|
+
serialized[k] = self._maybe_deref(x, ref_index_map)
|
|
520
|
+
else:
|
|
521
|
+
shared = self.get_shared(ref_index)
|
|
522
|
+
if shared.ref_count == 1:
|
|
523
|
+
ref_serialized = self._maybe_deref(shared.serialized, ref_index_map)
|
|
524
|
+
if isinstance(ref_serialized, dict):
|
|
525
|
+
serialized.pop(JSONConvertible.REF_KEY)
|
|
526
|
+
serialized.update(ref_serialized)
|
|
527
|
+
return serialized
|
|
528
|
+
return ref_serialized
|
|
529
|
+
else:
|
|
530
|
+
serialized[JSONConvertible.REF_KEY] = ref_index_map[shared.ref_index]
|
|
531
|
+
elif isinstance(serialized, list):
|
|
532
|
+
for i, x in enumerate(serialized):
|
|
533
|
+
serialized[i] = self._maybe_deref(x, ref_index_map)
|
|
534
|
+
return serialized
|
|
535
|
+
|
|
536
|
+
def to_json(self, *, root: Any, **kwargs) -> JSONValueType:
|
|
537
|
+
"""Serializes a root node with the context to JSON value."""
|
|
538
|
+
# `ref_index_map` stores the original index of shared objects to their
|
|
539
|
+
# indices after the ref-1 shared objects are trimmed.
|
|
540
|
+
ref_index_map = {}
|
|
541
|
+
|
|
542
|
+
shared_objects = []
|
|
543
|
+
for i, v in enumerate(self._shared_objects):
|
|
544
|
+
ref_index_map[i] = len(shared_objects)
|
|
545
|
+
if v.ref_count != 1:
|
|
546
|
+
shared_objects.append(v.value)
|
|
547
|
+
|
|
548
|
+
root = self._maybe_deref(root, ref_index_map)
|
|
549
|
+
|
|
550
|
+
serialized_shared_objects = [
|
|
551
|
+
v.serialized for v in self._shared_objects if v.ref_count != 1
|
|
552
|
+
]
|
|
553
|
+
if not shared_objects:
|
|
554
|
+
return root
|
|
555
|
+
serialized = {}
|
|
556
|
+
if shared_objects:
|
|
557
|
+
serialized[JSONConvertible.CONTEXT_KEY] = {
|
|
558
|
+
'shared_objects': [
|
|
559
|
+
self._maybe_deref(x, ref_index_map)
|
|
560
|
+
for x in serialized_shared_objects
|
|
561
|
+
],
|
|
562
|
+
}
|
|
563
|
+
serialized[JSONConvertible.ROOT_VALUE_KEY] = root
|
|
564
|
+
return serialized
|
|
565
|
+
|
|
566
|
+
@classmethod
|
|
567
|
+
def from_json(
|
|
568
|
+
cls, json_value: JSONValueType, **kwargs
|
|
569
|
+
) -> 'JSONConversionContext':
|
|
570
|
+
"""Deserializes a JSONConvertible value from JSON value."""
|
|
571
|
+
context = cls()
|
|
572
|
+
if isinstance(json_value, dict):
|
|
573
|
+
# Shared objects are serialized in a bottom-up order, thus dependent
|
|
574
|
+
# shared objects must be deserialized first.
|
|
575
|
+
if shared_objects_json := json_value.get('shared_objects'):
|
|
576
|
+
for v in shared_objects_json:
|
|
577
|
+
context.add_shared(
|
|
578
|
+
cls.ObjectEntry(
|
|
579
|
+
value=from_json(v, context=context, **kwargs),
|
|
580
|
+
serialized=v,
|
|
581
|
+
ref_index=context.next_shared_index(),
|
|
582
|
+
ref_count=0,
|
|
583
|
+
)
|
|
584
|
+
)
|
|
585
|
+
return context
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def to_json(
|
|
589
|
+
value: Any,
|
|
590
|
+
*,
|
|
591
|
+
context: Optional[JSONConversionContext] = None,
|
|
592
|
+
**kwargs
|
|
593
|
+
) -> Any:
|
|
405
594
|
"""Serializes a (maybe) JSONConvertible value into a plain Python object.
|
|
406
595
|
|
|
407
596
|
Args:
|
|
@@ -413,22 +602,50 @@ def to_json(value: Any, **kwargs) -> Any:
|
|
|
413
602
|
* Tuple types;
|
|
414
603
|
* Dict types.
|
|
415
604
|
|
|
605
|
+
context: JSON conversion context.
|
|
416
606
|
**kwargs: Keyword arguments to pass to value.to_json if value is
|
|
417
607
|
JSONConvertible.
|
|
418
608
|
|
|
419
609
|
Returns:
|
|
420
610
|
JSON value.
|
|
421
611
|
"""
|
|
612
|
+
if context is None:
|
|
613
|
+
is_root = True
|
|
614
|
+
context = JSONConversionContext()
|
|
615
|
+
else:
|
|
616
|
+
is_root = False
|
|
617
|
+
|
|
422
618
|
if isinstance(value, (type(None), bool, int, float, str)):
|
|
619
|
+
# Primitive types serialize by values.
|
|
423
620
|
v = value
|
|
424
621
|
elif isinstance(value, JSONConvertible):
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
622
|
+
# Non-symbolic objects serialize by references.
|
|
623
|
+
v = context.serialize_maybe_shared(
|
|
624
|
+
value,
|
|
625
|
+
json_fn=getattr(value, 'sym_jsonify', value.to_json),
|
|
626
|
+
**kwargs
|
|
627
|
+
)
|
|
428
628
|
elif isinstance(value, list):
|
|
429
|
-
|
|
629
|
+
# Standard lists serialize by references.
|
|
630
|
+
v = context.serialize_maybe_shared(
|
|
631
|
+
value,
|
|
632
|
+
json_fn=lambda **kwargs: [to_json(x, **kwargs) for x in value],
|
|
633
|
+
**kwargs
|
|
634
|
+
)
|
|
430
635
|
elif isinstance(value, dict):
|
|
431
|
-
|
|
636
|
+
# Standard dicts serialize by references.
|
|
637
|
+
v = context.serialize_maybe_shared(
|
|
638
|
+
value,
|
|
639
|
+
json_fn=lambda **kwargs: {
|
|
640
|
+
k: to_json(v, **kwargs) for k, v in value.items() # pytype: disable=attribute-error
|
|
641
|
+
},
|
|
642
|
+
**kwargs
|
|
643
|
+
)
|
|
644
|
+
elif isinstance(value, tuple):
|
|
645
|
+
# Tuples serialize by values.
|
|
646
|
+
v = [JSONConvertible.TUPLE_MARKER] + [
|
|
647
|
+
to_json(item, context=context, **kwargs) for item in value
|
|
648
|
+
]
|
|
432
649
|
elif isinstance(value, (type, typing.GenericAlias)): # pytype: disable=module-attr
|
|
433
650
|
v = _type_to_json(value)
|
|
434
651
|
elif inspect.isbuiltin(value):
|
|
@@ -448,40 +665,71 @@ def to_json(value: Any, **kwargs) -> Any:
|
|
|
448
665
|
if JSONConvertible.TYPE_CONVERTER is not None:
|
|
449
666
|
converter = JSONConvertible.TYPE_CONVERTER(type(value)) # pylint: disable=not-callable
|
|
450
667
|
if converter:
|
|
451
|
-
v = to_json(converter(value))
|
|
668
|
+
v = to_json(converter(value), context=context, **kwargs)
|
|
452
669
|
converted = True
|
|
453
670
|
if not converted:
|
|
454
|
-
|
|
671
|
+
# Opaque objects serialize by references.
|
|
672
|
+
v = context.serialize_maybe_shared(
|
|
673
|
+
value,
|
|
674
|
+
json_fn=lambda **kwargs: _OpaqueObject(value).to_json(**kwargs),
|
|
675
|
+
**kwargs
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
if is_root:
|
|
679
|
+
return context.to_json(root=v, **kwargs)
|
|
455
680
|
return v
|
|
456
681
|
|
|
457
682
|
|
|
458
683
|
def from_json(
|
|
459
684
|
json_value: JSONValueType,
|
|
460
685
|
*,
|
|
686
|
+
context: Optional[JSONConversionContext] = None,
|
|
461
687
|
auto_import: bool = True,
|
|
462
|
-
|
|
463
|
-
**kwargs
|
|
688
|
+
convert_unknown: bool = False,
|
|
689
|
+
**kwargs
|
|
690
|
+
) -> Any:
|
|
464
691
|
"""Deserializes a (maybe) JSONConvertible value from JSON value.
|
|
465
692
|
|
|
466
693
|
Args:
|
|
467
694
|
json_value: Input JSON value.
|
|
695
|
+
context: Serialization context.
|
|
468
696
|
auto_import: If True, when a '_type' is not registered, PyGlove will
|
|
469
697
|
identify its parent module and automatically import it. For example,
|
|
470
698
|
if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
|
|
471
699
|
find the class 'A' within the imported module.
|
|
472
|
-
|
|
473
|
-
|
|
700
|
+
convert_unknown: If True, when a '_type' is not registered and cannot
|
|
701
|
+
be imported, PyGlove will create objects of:
|
|
702
|
+
- `pg.symbolic.UnknownType` for unknown types;
|
|
703
|
+
- `pg.symbolic.UnknownTypedObject` for objects of unknown types;
|
|
704
|
+
- `pg.symbolic.UnknownFunction` for unknown functions;
|
|
705
|
+
- `pg.symbolic.UnknownMethod` for unknown methods.
|
|
706
|
+
If False, TypeError will be raised.
|
|
474
707
|
**kwargs: Keyword arguments that will be passed to JSONConvertible.__init__.
|
|
475
708
|
|
|
476
709
|
Returns:
|
|
477
710
|
Deserialized value.
|
|
478
711
|
"""
|
|
712
|
+
if context is None:
|
|
713
|
+
if (isinstance(json_value, dict)
|
|
714
|
+
and (context_node := json_value.get(JSONConvertible.CONTEXT_KEY))):
|
|
715
|
+
context = JSONConversionContext.from_json(
|
|
716
|
+
context_node,
|
|
717
|
+
auto_import=auto_import,
|
|
718
|
+
convert_unknown=convert_unknown,
|
|
719
|
+
**kwargs
|
|
720
|
+
)
|
|
721
|
+
json_value = json_value[JSONConvertible.ROOT_VALUE_KEY]
|
|
722
|
+
else:
|
|
723
|
+
context = JSONConversionContext()
|
|
724
|
+
|
|
479
725
|
typename_resolved = kwargs.pop('_typename_resolved', False)
|
|
480
726
|
if not typename_resolved:
|
|
481
|
-
json_value = resolve_typenames(
|
|
727
|
+
json_value = resolve_typenames(
|
|
728
|
+
json_value, auto_import=auto_import, convert_unknown=convert_unknown
|
|
729
|
+
)
|
|
482
730
|
|
|
483
731
|
def child_from(v):
|
|
484
|
-
return from_json(v, _typename_resolved=True, **kwargs)
|
|
732
|
+
return from_json(v, context=context, _typename_resolved=True, **kwargs)
|
|
485
733
|
|
|
486
734
|
if isinstance(json_value, list):
|
|
487
735
|
if json_value and json_value[0] == JSONConvertible.TUPLE_MARKER:
|
|
@@ -493,18 +741,21 @@ def from_json(
|
|
|
493
741
|
return tuple([child_from(v) for v in json_value[1:]])
|
|
494
742
|
return [child_from(v) for v in json_value]
|
|
495
743
|
elif isinstance(json_value, dict):
|
|
744
|
+
if JSONConvertible.REF_KEY in json_value:
|
|
745
|
+
v = context.get_shared(json_value[JSONConvertible.REF_KEY]).value
|
|
746
|
+
return v
|
|
496
747
|
if JSONConvertible.TYPE_NAME_KEY not in json_value:
|
|
497
748
|
return {k: child_from(v) for k, v in json_value.items()}
|
|
498
749
|
factory_fn = json_value.pop(JSONConvertible.TYPE_NAME_KEY)
|
|
499
750
|
assert factory_fn is not None
|
|
500
|
-
return factory_fn(json_value, **kwargs)
|
|
751
|
+
return factory_fn(json_value, context=context, **kwargs)
|
|
501
752
|
return json_value
|
|
502
753
|
|
|
503
754
|
|
|
504
755
|
def resolve_typenames(
|
|
505
756
|
json_value: JSONValueType,
|
|
506
757
|
auto_import: bool = True,
|
|
507
|
-
|
|
758
|
+
convert_unknown: bool = False,
|
|
508
759
|
) -> JSONValueType:
|
|
509
760
|
"""Inplace resolves the "_type" keys with their factories in a JSON tree."""
|
|
510
761
|
|
|
@@ -516,11 +767,11 @@ def resolve_typenames(
|
|
|
516
767
|
return False
|
|
517
768
|
type_name = v[JSONConvertible.TYPE_NAME_KEY]
|
|
518
769
|
if type_name == 'type':
|
|
519
|
-
factory_fn = _type_from_json
|
|
770
|
+
factory_fn = _type_from_json(convert_unknown)
|
|
520
771
|
elif type_name == 'function':
|
|
521
|
-
factory_fn = _function_from_json
|
|
772
|
+
factory_fn = _function_from_json(convert_unknown)
|
|
522
773
|
elif type_name == 'method':
|
|
523
|
-
factory_fn = _method_from_json
|
|
774
|
+
factory_fn = _method_from_json(convert_unknown)
|
|
524
775
|
else:
|
|
525
776
|
cls = JSONConvertible.class_from_typename(type_name)
|
|
526
777
|
if cls is None:
|
|
@@ -529,32 +780,38 @@ def resolve_typenames(
|
|
|
529
780
|
cls = _load_symbol(type_name)
|
|
530
781
|
assert inspect.isclass(cls), cls
|
|
531
782
|
except (ModuleNotFoundError, AttributeError) as e:
|
|
532
|
-
if not
|
|
783
|
+
if not convert_unknown:
|
|
533
784
|
raise TypeError(
|
|
534
785
|
f'Cannot load class {type_name!r}.\n'
|
|
535
|
-
'Try pass `
|
|
536
|
-
'without depending on the type.'
|
|
786
|
+
'Try pass `convert_unknown=True` to load the object into '
|
|
787
|
+
'`pg.symbolic.UnknownObject` without depending on the type.'
|
|
537
788
|
) from e
|
|
538
|
-
elif not
|
|
789
|
+
elif not convert_unknown:
|
|
539
790
|
raise TypeError(
|
|
540
791
|
f'Type name \'{type_name}\' is not registered '
|
|
541
792
|
'with a `pg.JSONConvertible` subclass.\n'
|
|
542
|
-
'Try pass `auto_import=True` to load the type from its module
|
|
543
|
-
'or pass `auto_dict=True` to load the object into a dict '
|
|
544
|
-
'without depending on the type.'
|
|
793
|
+
'Try pass `auto_import=True` to load the type from its module.'
|
|
545
794
|
)
|
|
546
795
|
|
|
547
796
|
factory_fn = getattr(cls, 'from_json', None)
|
|
548
|
-
if cls is not None and factory_fn is None and not
|
|
797
|
+
if cls is not None and factory_fn is None and not convert_unknown:
|
|
549
798
|
raise TypeError(
|
|
550
799
|
f'{cls} is not a `pg.JSONConvertible` subclass.'
|
|
551
|
-
'Try pass `
|
|
552
|
-
'without depending on the type.'
|
|
800
|
+
'Try pass `convert_unknown=True` to load the object into a '
|
|
801
|
+
'`pg.symbolic.UnknownObject` without depending on the type.'
|
|
553
802
|
)
|
|
554
803
|
|
|
555
|
-
if factory_fn is None and
|
|
556
|
-
|
|
557
|
-
|
|
804
|
+
if factory_fn is None and convert_unknown:
|
|
805
|
+
type_name = v[JSONConvertible.TYPE_NAME_KEY]
|
|
806
|
+
def _factory_fn(json_value: Dict[str, Any], **kwargs):
|
|
807
|
+
del kwargs
|
|
808
|
+
# See `pg.symbolic.UnknownObject` for details.
|
|
809
|
+
unknown_object_cls = JSONConvertible.class_from_typename(
|
|
810
|
+
'unknown_object'
|
|
811
|
+
)
|
|
812
|
+
return unknown_object_cls(type_name=type_name, **json_value) # pytype: disable=wrong-keyword-args
|
|
813
|
+
|
|
814
|
+
v[JSONConvertible.TYPE_NAME_KEY] = _factory_fn
|
|
558
815
|
return True
|
|
559
816
|
assert factory_fn is not None
|
|
560
817
|
|
|
@@ -569,7 +826,7 @@ def resolve_typenames(
|
|
|
569
826
|
if _resolve_typename(v):
|
|
570
827
|
# Only resolve children when _types in this tree is not resolved
|
|
571
828
|
# previously
|
|
572
|
-
for x in v.values():
|
|
829
|
+
for x in getattr(v, 'sym_values', v.values)():
|
|
573
830
|
_visit(x)
|
|
574
831
|
|
|
575
832
|
_visit(json_value)
|
|
@@ -753,39 +1010,79 @@ def _load_symbol(type_name: str) -> Any:
|
|
|
753
1010
|
return symbol
|
|
754
1011
|
|
|
755
1012
|
|
|
756
|
-
def _type_from_json(
|
|
1013
|
+
def _type_from_json(convert_unknown: bool) -> Callable[..., Any]:
|
|
757
1014
|
"""Loads a type from a JSON dict."""
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1015
|
+
def _fn(json_value: Dict[str, str], **kwargs) -> Type[Any]:
|
|
1016
|
+
del kwargs
|
|
1017
|
+
try:
|
|
1018
|
+
t = _load_symbol(json_value['name'])
|
|
1019
|
+
if 'args' in json_value:
|
|
1020
|
+
return _bind_type_args(
|
|
1021
|
+
t, from_json(json_value['args'], _typename_resolved=True)
|
|
1022
|
+
)
|
|
1023
|
+
return t
|
|
1024
|
+
except (ModuleNotFoundError, AttributeError) as e:
|
|
1025
|
+
if not convert_unknown:
|
|
1026
|
+
raise TypeError(
|
|
1027
|
+
f'Cannot load type {json_value["name"]!r}.\n'
|
|
1028
|
+
'Try pass `convert_unknown=True` to load the object '
|
|
1029
|
+
'into `pg.UnknownType` without depending on the type.'
|
|
1030
|
+
) from e
|
|
1031
|
+
# See `pg.symbolic.UnknownType` for details.
|
|
1032
|
+
json_value[JSONConvertible.TYPE_NAME_KEY] = 'unknown_type'
|
|
1033
|
+
return from_json(json_value)
|
|
1034
|
+
return _fn
|
|
765
1035
|
|
|
766
1036
|
|
|
767
1037
|
def _function_from_json(
|
|
768
|
-
|
|
1038
|
+
convert_unknown: bool
|
|
1039
|
+
) -> Callable[..., types.FunctionType]:
|
|
769
1040
|
"""Loads a function from a JSON dict."""
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
code
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1041
|
+
def _fn(json_value: Dict[str, str], **kwargs) -> types.FunctionType:
|
|
1042
|
+
del kwargs
|
|
1043
|
+
function_name = json_value['name']
|
|
1044
|
+
if 'code' in json_value:
|
|
1045
|
+
code = marshal.loads(
|
|
1046
|
+
base64.decodebytes(json_value['code'].encode('utf-8')))
|
|
1047
|
+
defaults = from_json(json_value['defaults'], _typename_resolved=True)
|
|
1048
|
+
return types.FunctionType(
|
|
1049
|
+
code=code,
|
|
1050
|
+
globals=globals(),
|
|
1051
|
+
argdefs=defaults,
|
|
1052
|
+
)
|
|
1053
|
+
else:
|
|
1054
|
+
try:
|
|
1055
|
+
return _load_symbol(function_name)
|
|
1056
|
+
except (ModuleNotFoundError, AttributeError) as e:
|
|
1057
|
+
if not convert_unknown:
|
|
1058
|
+
raise TypeError(
|
|
1059
|
+
f'Cannot load function {function_name!r}.\n'
|
|
1060
|
+
'Try pass `convert_unknown=True` to load the object into '
|
|
1061
|
+
'`pg.UnknownFunction` without depending on the type.'
|
|
1062
|
+
) from e
|
|
1063
|
+
json_value[JSONConvertible.TYPE_NAME_KEY] = 'unknown_function'
|
|
1064
|
+
return from_json(json_value)
|
|
1065
|
+
return _fn
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _method_from_json(
|
|
1069
|
+
convert_unknown: bool
|
|
1070
|
+
) -> Callable[..., types.MethodType]:
|
|
786
1071
|
"""Loads a class method from a JSON dict."""
|
|
787
|
-
|
|
788
|
-
|
|
1072
|
+
def _fn(json_value: Dict[str, str], **kwargs) -> types.MethodType:
|
|
1073
|
+
del kwargs
|
|
1074
|
+
try:
|
|
1075
|
+
return _load_symbol(json_value['name'])
|
|
1076
|
+
except (ModuleNotFoundError, AttributeError) as e:
|
|
1077
|
+
if not convert_unknown:
|
|
1078
|
+
raise TypeError(
|
|
1079
|
+
f'Cannot load method {json_value["name"]!r}.\n'
|
|
1080
|
+
'Try pass `convert_unknown=True` to load the object '
|
|
1081
|
+
'into `pg.UnknownMethod` without depending on the type.'
|
|
1082
|
+
) from e
|
|
1083
|
+
json_value[JSONConvertible.TYPE_NAME_KEY] = 'unknown_method'
|
|
1084
|
+
return from_json(json_value)
|
|
1085
|
+
return _fn
|
|
789
1086
|
|
|
790
1087
|
|
|
791
1088
|
def _bind_type_args(t, args):
|