pyglove 0.5.0.dev202510200810__py3-none-any.whl → 0.5.0.dev202512280810__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.

Files changed (34) hide show
  1. pyglove/core/geno/base.py +7 -3
  2. pyglove/core/io/file_system.py +452 -2
  3. pyglove/core/io/file_system_test.py +442 -0
  4. pyglove/core/symbolic/__init__.py +7 -0
  5. pyglove/core/symbolic/base.py +89 -35
  6. pyglove/core/symbolic/base_test.py +3 -3
  7. pyglove/core/symbolic/dict.py +31 -12
  8. pyglove/core/symbolic/dict_test.py +49 -0
  9. pyglove/core/symbolic/list.py +17 -3
  10. pyglove/core/symbolic/list_test.py +24 -2
  11. pyglove/core/symbolic/object.py +3 -1
  12. pyglove/core/symbolic/object_test.py +13 -10
  13. pyglove/core/symbolic/ref.py +19 -7
  14. pyglove/core/symbolic/ref_test.py +94 -7
  15. pyglove/core/symbolic/unknown_symbols.py +147 -0
  16. pyglove/core/symbolic/unknown_symbols_test.py +100 -0
  17. pyglove/core/typing/annotation_conversion.py +8 -1
  18. pyglove/core/typing/annotation_conversion_test.py +14 -19
  19. pyglove/core/typing/class_schema.py +4 -1
  20. pyglove/core/typing/type_conversion.py +17 -3
  21. pyglove/core/typing/type_conversion_test.py +7 -2
  22. pyglove/core/typing/value_specs.py +5 -1
  23. pyglove/core/typing/value_specs_test.py +5 -0
  24. pyglove/core/utils/__init__.py +1 -0
  25. pyglove/core/utils/json_conversion.py +360 -63
  26. pyglove/core/utils/json_conversion_test.py +146 -13
  27. pyglove/core/views/html/controls/tab.py +33 -0
  28. pyglove/core/views/html/controls/tab_test.py +37 -0
  29. pyglove/ext/evolution/base_test.py +1 -1
  30. {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202512280810.dist-info}/METADATA +8 -1
  31. {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202512280810.dist-info}/RECORD +34 -32
  32. {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202512280810.dist-info}/WHEEL +0 -0
  33. {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202512280810.dist-info}/licenses/LICENSE +0 -0
  34. {pyglove-0.5.0.dev202510200810.dist-info → pyglove-0.5.0.dev202512280810.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(self, **kwargs) -> JSONValueType:
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(cls, json_value: JSONValueType, *args, **kwargs) -> Any:
393
- del args, kwargs
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
- def to_json(value: Any, **kwargs) -> Any:
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
- v = value.to_json(**kwargs)
426
- elif isinstance(value, tuple):
427
- v = [JSONConvertible.TUPLE_MARKER] + to_json(list(value), **kwargs)
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
- v = [to_json(item, **kwargs) for item in value]
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
- v = {k: to_json(v, **kwargs) for k, v in value.items()}
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
- v = _OpaqueObject(value).to_json(**kwargs)
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
- auto_dict: bool = False,
463
- **kwargs) -> Any:
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
- auto_dict: If True, dict with '_type' that cannot be loaded will remain
473
- as dict, with '_type' renamed to 'type_name'.
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(json_value, auto_import, auto_dict)
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
- auto_dict: bool = False
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 auto_dict:
783
+ if not convert_unknown:
533
784
  raise TypeError(
534
785
  f'Cannot load class {type_name!r}.\n'
535
- 'Try pass `auto_dict=True` to load the object into a dict '
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 auto_dict:
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 auto_dict:
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 `auto_dict=True` to load the object into a dict '
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 auto_dict:
556
- v['type_name'] = type_name
557
- v.pop(JSONConvertible.TYPE_NAME_KEY)
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(json_value: Dict[str, str], **kwargs) -> Type[Any]:
1013
+ def _type_from_json(convert_unknown: bool) -> Callable[..., Any]:
757
1014
  """Loads a type from a JSON dict."""
758
- del kwargs
759
- t = _load_symbol(json_value['name'])
760
- if 'args' in json_value:
761
- return _bind_type_args(
762
- t, from_json(json_value['args'], _typename_resolved=True)
763
- )
764
- return t
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
- json_value: Dict[str, str], **kwargs) -> types.FunctionType:
1038
+ convert_unknown: bool
1039
+ ) -> Callable[..., types.FunctionType]:
769
1040
  """Loads a function from a JSON dict."""
770
- del kwargs
771
- function_name = json_value['name']
772
- if 'code' in json_value:
773
- code = marshal.loads(
774
- base64.decodebytes(json_value['code'].encode('utf-8')))
775
- defaults = from_json(json_value['defaults'], _typename_resolved=True)
776
- return types.FunctionType(
777
- code=code,
778
- globals=globals(),
779
- argdefs=defaults,
780
- )
781
- else:
782
- return _load_symbol(function_name)
783
-
784
-
785
- def _method_from_json(json_value: Dict[str, str], **kwargs) -> types.MethodType:
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
- del kwargs
788
- return _load_symbol(json_value['name'])
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):