pyglove 0.5.0.dev202510020810__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 (40) 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/monitoring.py +213 -90
  5. pyglove/core/monitoring_test.py +82 -29
  6. pyglove/core/symbolic/__init__.py +7 -0
  7. pyglove/core/symbolic/base.py +89 -35
  8. pyglove/core/symbolic/base_test.py +3 -3
  9. pyglove/core/symbolic/dict.py +31 -12
  10. pyglove/core/symbolic/dict_test.py +49 -0
  11. pyglove/core/symbolic/list.py +17 -3
  12. pyglove/core/symbolic/list_test.py +24 -2
  13. pyglove/core/symbolic/object.py +3 -1
  14. pyglove/core/symbolic/object_test.py +13 -10
  15. pyglove/core/symbolic/ref.py +19 -7
  16. pyglove/core/symbolic/ref_test.py +94 -7
  17. pyglove/core/symbolic/unknown_symbols.py +147 -0
  18. pyglove/core/symbolic/unknown_symbols_test.py +100 -0
  19. pyglove/core/typing/annotation_conversion.py +8 -1
  20. pyglove/core/typing/annotation_conversion_test.py +14 -19
  21. pyglove/core/typing/class_schema.py +24 -1
  22. pyglove/core/typing/json_schema.py +221 -8
  23. pyglove/core/typing/json_schema_test.py +508 -12
  24. pyglove/core/typing/type_conversion.py +17 -3
  25. pyglove/core/typing/type_conversion_test.py +7 -2
  26. pyglove/core/typing/value_specs.py +5 -1
  27. pyglove/core/typing/value_specs_test.py +5 -0
  28. pyglove/core/utils/__init__.py +1 -0
  29. pyglove/core/utils/contextual.py +9 -4
  30. pyglove/core/utils/contextual_test.py +10 -0
  31. pyglove/core/utils/json_conversion.py +360 -63
  32. pyglove/core/utils/json_conversion_test.py +146 -13
  33. pyglove/core/views/html/controls/tab.py +33 -0
  34. pyglove/core/views/html/controls/tab_test.py +37 -0
  35. pyglove/ext/evolution/base_test.py +1 -1
  36. {pyglove-0.5.0.dev202510020810.dist-info → pyglove-0.5.0.dev202512280810.dist-info}/METADATA +8 -1
  37. {pyglove-0.5.0.dev202510020810.dist-info → pyglove-0.5.0.dev202512280810.dist-info}/RECORD +40 -38
  38. {pyglove-0.5.0.dev202510020810.dist-info → pyglove-0.5.0.dev202512280810.dist-info}/WHEEL +0 -0
  39. {pyglove-0.5.0.dev202510020810.dist-info → pyglove-0.5.0.dev202512280810.dist-info}/licenses/LICENSE +0 -0
  40. {pyglove-0.5.0.dev202510020810.dist-info → pyglove-0.5.0.dev202512280810.dist-info}/top_level.txt +0 -0
@@ -503,10 +503,12 @@ class Symbolic(
503
503
  return default
504
504
 
505
505
  def _sym_inferred(self, key: Union[str, int], **kwargs) -> Any:
506
- v = self.sym_getattr(key)
507
- if isinstance(v, Inferential):
508
- v = v.infer(**kwargs)
509
- return v
506
+ return self._infer_if_applicable(self.sym_getattr(key), **kwargs)
507
+
508
+ def _infer_if_applicable(self, value: Any, **kwargs) -> Any:
509
+ if isinstance(value, Inferential):
510
+ return value.infer(**kwargs)
511
+ return value
510
512
 
511
513
  @abc.abstractmethod
512
514
  def sym_keys(self) -> Iterator[Union[str, int]]:
@@ -944,7 +946,7 @@ class Symbolic(
944
946
 
945
947
  def to_json(self, **kwargs) -> utils.JSONValueType:
946
948
  """Alias for `sym_jsonify`."""
947
- return to_json(self, **kwargs)
949
+ return utils.to_json(self, **kwargs)
948
950
 
949
951
  def to_json_str(self, json_indent: Optional[int] = None, **kwargs) -> str:
950
952
  """Serializes current object into a JSON string."""
@@ -1983,10 +1985,12 @@ def is_abstract(x: Any) -> bool:
1983
1985
  def contains(
1984
1986
  x: Any,
1985
1987
  value: Any = None,
1986
- type: Optional[Union[ # pylint: disable=redefined-builtin
1988
+ type: Union[ # pylint: disable=redefined-builtin
1987
1989
  Type[Any],
1988
- Tuple[Type[Any]]]]=None
1989
- ) -> bool:
1990
+ Tuple[Type[Any], ...],
1991
+ None,
1992
+ ]=None,
1993
+ ) -> bool:
1990
1994
  """Returns if a value contains values of specific type.
1991
1995
 
1992
1996
  Example::
@@ -2035,10 +2039,12 @@ def contains(
2035
2039
  def from_json(
2036
2040
  json_value: Any,
2037
2041
  *,
2042
+ context: Optional[utils.JSONConversionContext] = None,
2043
+ auto_symbolic: bool = True,
2044
+ auto_import: bool = True,
2045
+ convert_unknown: bool = False,
2038
2046
  allow_partial: bool = False,
2039
2047
  root_path: Optional[utils.KeyPath] = None,
2040
- auto_import: bool = True,
2041
- auto_dict: bool = False,
2042
2048
  value_spec: Optional[pg_typing.ValueSpec] = None,
2043
2049
  **kwargs,
2044
2050
  ) -> Any:
@@ -2059,14 +2065,23 @@ def from_json(
2059
2065
 
2060
2066
  Args:
2061
2067
  json_value: Input JSON value.
2062
- allow_partial: Whether to allow elements of the list to be partial.
2063
- root_path: KeyPath of loaded object in its object tree.
2068
+ context: JSON conversion context.
2069
+ auto_symbolic: If True, list and dict will be automatically converted to
2070
+ `pg.List` and `pg.Dict`. Otherwise, they will be plain lists
2071
+ and dicts.
2064
2072
  auto_import: If True, when a '_type' is not registered, PyGlove will
2065
2073
  identify its parent module and automatically import it. For example,
2066
2074
  if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
2067
2075
  find the class 'A' within the imported module.
2068
- auto_dict: If True, dict with '_type' that cannot be loaded will remain
2069
- as dict, with '_type' renamed to 'type_name'.
2076
+ convert_unknown: If True, when a '_type' is not registered and cannot
2077
+ be imported, PyGlove will create objects of:
2078
+ - `pg.symbolic.UnknownType` for unknown types;
2079
+ - `pg.symbolic.UnknownTypedObject` for objects of unknown types;
2080
+ - `pg.symbolic.UnknownFunction` for unknown functions;
2081
+ - `pg.symbolic.UnknownMethod` for unknown methods.
2082
+ If False, TypeError will be raised.
2083
+ allow_partial: Whether to allow elements of the list to be partial.
2084
+ root_path: KeyPath of loaded object in its object tree.
2070
2085
  value_spec: The value spec for the symbolic list or dict.
2071
2086
  **kwargs: Allow passing through keyword arguments to from_json of specific
2072
2087
  types.
@@ -2082,10 +2097,23 @@ def from_json(
2082
2097
  if isinstance(json_value, Symbolic):
2083
2098
  return json_value
2084
2099
 
2100
+ if context is None:
2101
+ if (isinstance(json_value, dict) and (
2102
+ context_node := json_value.get(utils.JSONConvertible.CONTEXT_KEY))):
2103
+ context = utils.JSONConversionContext.from_json(
2104
+ context_node,
2105
+ auto_import=auto_import,
2106
+ convert_unknown=convert_unknown,
2107
+ **kwargs
2108
+ )
2109
+ json_value = json_value[utils.JSONConvertible.ROOT_VALUE_KEY]
2110
+ else:
2111
+ context = utils.JSONConversionContext()
2112
+
2085
2113
  typename_resolved = kwargs.pop('_typename_resolved', False)
2086
2114
  if not typename_resolved:
2087
2115
  json_value = utils.json_conversion.resolve_typenames(
2088
- json_value, auto_import=auto_import, auto_dict=auto_dict
2116
+ json_value, auto_import, convert_unknown
2089
2117
  )
2090
2118
 
2091
2119
  def _load_child(k, v):
@@ -2094,6 +2122,7 @@ def from_json(
2094
2122
  root_path=utils.KeyPath(k, root_path),
2095
2123
  _typename_resolved=True,
2096
2124
  allow_partial=allow_partial,
2125
+ context=context,
2097
2126
  **kwargs,
2098
2127
  )
2099
2128
 
@@ -2109,24 +2138,42 @@ def from_json(
2109
2138
  )
2110
2139
  )
2111
2140
  return tuple(_load_child(i, v) for i, v in enumerate(json_value[1:]))
2112
- return Symbolic.ListType.from_json( # pytype: disable=attribute-error
2141
+ if json_value and json_value[0] == utils.JSONConvertible.SYMBOLIC_MARKER:
2142
+ auto_symbolic = True
2143
+ if auto_symbolic:
2144
+ from_json_fn = Symbolic.ListType.from_json # pytype: disable=attribute-error
2145
+ else:
2146
+ from_json_fn = utils.from_json
2147
+ return from_json_fn(
2113
2148
  json_value,
2149
+ context=context,
2114
2150
  value_spec=value_spec,
2115
2151
  root_path=root_path,
2116
2152
  allow_partial=allow_partial,
2117
2153
  **kwargs,
2118
2154
  )
2119
2155
  elif isinstance(json_value, dict):
2156
+ if utils.JSONConvertible.REF_KEY in json_value:
2157
+ x = context.get_shared(
2158
+ json_value[utils.JSONConvertible.REF_KEY]
2159
+ ).value
2160
+ return x
2120
2161
  if utils.JSONConvertible.TYPE_NAME_KEY not in json_value:
2121
- return Symbolic.DictType.from_json( # pytype: disable=attribute-error
2122
- json_value,
2123
- value_spec=value_spec,
2124
- root_path=root_path,
2125
- allow_partial=allow_partial,
2126
- **kwargs,
2162
+ auto_symbolic = json_value.get(
2163
+ utils.JSONConvertible.SYMBOLIC_MARKER, auto_symbolic
2127
2164
  )
2165
+ if auto_symbolic:
2166
+ return Symbolic.DictType.from_json( # pytype: disable=attribute-error
2167
+ json_value,
2168
+ context=context,
2169
+ value_spec=value_spec,
2170
+ root_path=root_path,
2171
+ allow_partial=allow_partial,
2172
+ **kwargs,
2173
+ )
2128
2174
  return utils.from_json(
2129
2175
  json_value,
2176
+ context=context,
2130
2177
  _typename_resolved=True,
2131
2178
  root_path=root_path,
2132
2179
  allow_partial=allow_partial,
@@ -2138,10 +2185,12 @@ def from_json(
2138
2185
  def from_json_str(
2139
2186
  json_str: str,
2140
2187
  *,
2188
+ context: Optional[utils.JSONConversionContext] = None,
2189
+ auto_import: bool = True,
2190
+ convert_unknown: bool = False,
2141
2191
  allow_partial: bool = False,
2142
2192
  root_path: Optional[utils.KeyPath] = None,
2143
- auto_import: bool = True,
2144
- auto_dict: bool = False,
2193
+ value_spec: Optional[pg_typing.ValueSpec] = None,
2145
2194
  **kwargs,
2146
2195
  ) -> Any:
2147
2196
  """Deserialize (maybe) symbolic object from JSON string.
@@ -2161,15 +2210,22 @@ def from_json_str(
2161
2210
 
2162
2211
  Args:
2163
2212
  json_str: JSON string.
2164
- allow_partial: If True, allow a partial symbolic object to be created.
2165
- Otherwise error will be raised on partial value.
2166
- root_path: The symbolic path used for the deserialized root object.
2213
+ context: JSON conversion context.
2167
2214
  auto_import: If True, when a '_type' is not registered, PyGlove will
2168
2215
  identify its parent module and automatically import it. For example,
2169
2216
  if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
2170
2217
  find the class 'A' within the imported module.
2171
- auto_dict: If True, dict with '_type' that cannot be loaded will remain
2172
- as dict, with '_type' renamed to 'type_name'.
2218
+ convert_unknown: If True, when a '_type' is not registered and cannot
2219
+ be imported, PyGlove will create objects of:
2220
+ - `pg.symbolic.UnknownType` for unknown types;
2221
+ - `pg.symbolic.UnknownTypedObject` for objects of unknown types;
2222
+ - `pg.symbolic.UnknownFunction` for unknown functions;
2223
+ - `pg.symbolic.UnknownMethod` for unknown methods.
2224
+ If False, TypeError will be raised.
2225
+ allow_partial: If True, allow a partial symbolic object to be created.
2226
+ Otherwise error will be raised on partial value.
2227
+ root_path: The symbolic path used for the deserialized root object.
2228
+ value_spec: The value spec for the symbolic list or dict.
2173
2229
  **kwargs: Additional keyword arguments that will be passed to
2174
2230
  ``pg.from_json``.
2175
2231
 
@@ -2193,10 +2249,12 @@ def from_json_str(
2193
2249
 
2194
2250
  return from_json(
2195
2251
  _decode_int_keys(json.loads(json_str)),
2252
+ context=context,
2253
+ auto_import=auto_import,
2254
+ convert_unknown=convert_unknown,
2196
2255
  allow_partial=allow_partial,
2197
2256
  root_path=root_path,
2198
- auto_import=auto_import,
2199
- auto_dict=auto_dict,
2257
+ value_spec=value_spec,
2200
2258
  **kwargs
2201
2259
  )
2202
2260
 
@@ -2232,10 +2290,6 @@ def to_json(value: Any, **kwargs) -> Any:
2232
2290
  Returns:
2233
2291
  JSON value.
2234
2292
  """
2235
- # NOTE(daiyip): special handling `sym_jsonify` since symbolized
2236
- # classes may have conflicting `to_json` method in their existing classes.
2237
- if isinstance(value, Symbolic):
2238
- return value.sym_jsonify(**kwargs)
2239
2293
  return utils.to_json(value, **kwargs)
2240
2294
 
2241
2295
 
@@ -20,9 +20,9 @@ from pyglove.core import typing as pg_typing
20
20
  from pyglove.core import utils
21
21
  from pyglove.core import views
22
22
  from pyglove.core.symbolic import base
23
- from pyglove.core.symbolic.dict import Dict
24
- from pyglove.core.symbolic.inferred import ValueFromParentChain
25
- from pyglove.core.symbolic.object import Object
23
+ from pyglove.core.symbolic.dict import Dict # pylint: disable=g-importing-member
24
+ from pyglove.core.symbolic.inferred import ValueFromParentChain # pylint: disable=g-importing-member
25
+ from pyglove.core.symbolic.object import Object # pylint: disable=g-importing-member
26
26
 
27
27
 
28
28
  class FieldUpdateTest(unittest.TestCase):
@@ -156,6 +156,8 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
156
156
  # Not okay:
157
157
  d.a.f2.abc = 1
158
158
  """
159
+ # Remove symbolic marker if present.
160
+ json_value.pop(utils.JSONConvertible.SYMBOLIC_MARKER, None)
159
161
  return cls(
160
162
  {
161
163
  k: base.from_json(
@@ -236,7 +238,8 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
236
238
  accessor_writable=True,
237
239
  # We delay seal operation until members are filled.
238
240
  sealed=False,
239
- root_path=root_path)
241
+ root_path=root_path
242
+ )
240
243
 
241
244
  dict.__init__(self)
242
245
  self._value_spec = None
@@ -247,9 +250,10 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
247
250
  for k, v in kwargs.items():
248
251
  dict_obj[k] = v
249
252
 
253
+ iter_items = getattr(dict_obj, 'sym_items', dict_obj.items)
250
254
  if value_spec:
251
255
  if pass_through:
252
- for k, v in dict_obj.items():
256
+ for k, v in iter_items():
253
257
  super().__setitem__(k, self._relocate_if_symbolic(k, v))
254
258
 
255
259
  # NOTE(daiyip): when pass_through is on, we simply trust input
@@ -258,11 +262,11 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
258
262
  # repeated validation and transformation.
259
263
  self._value_spec = value_spec
260
264
  else:
261
- for k, v in dict_obj.items():
265
+ for k, v in iter_items():
262
266
  super().__setitem__(k, self._formalized_value(k, None, v))
263
267
  self.use_value_spec(value_spec, allow_partial)
264
268
  else:
265
- for k, v in dict_obj.items():
269
+ for k, v in iter_items():
266
270
  self._set_item_without_permission_check(k, v)
267
271
 
268
272
  # NOTE(daiyip): We set onchange callback at the end of init to avoid
@@ -537,7 +541,7 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
537
541
  raise KeyError(self._error_message(
538
542
  f'Key must be string or int type. Encountered {key!r}.'))
539
543
 
540
- old_value = self.get(key, pg_typing.MISSING_VALUE)
544
+ old_value = self.sym_getattr(key, pg_typing.MISSING_VALUE)
541
545
  if old_value is value:
542
546
  return None
543
547
 
@@ -644,6 +648,13 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
644
648
  except AttributeError as e:
645
649
  raise KeyError(key) from e
646
650
 
651
+ def get(self, key: Union[str, int], default: Any = None) -> Any:
652
+ """Get item in this Dict."""
653
+ try:
654
+ return self.sym_inferred(key)
655
+ except AttributeError:
656
+ return default
657
+
647
658
  def __setitem__(self, key: Union[str, int], value: Any) -> None:
648
659
  """Set item in this Dict.
649
660
 
@@ -751,11 +762,13 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
751
762
 
752
763
  def items(self) -> Iterator[Tuple[Union[str, int], Any]]: # pytype: disable=signature-mismatch
753
764
  """Returns an iterator of (key, value) items in current dict."""
754
- return self.sym_items()
765
+ for k, v in self.sym_items():
766
+ yield k, self._infer_if_applicable(v)
755
767
 
756
768
  def values(self) -> Iterator[Any]: # pytype: disable=signature-mismatch
757
769
  """Returns an iterator of values in current dict.."""
758
- return self.sym_values()
770
+ for v in self.sym_values():
771
+ yield self._infer_if_applicable(v)
759
772
 
760
773
  def copy(self) -> 'Dict':
761
774
  """Overridden copy using symbolic copy."""
@@ -824,12 +837,15 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
824
837
  hide_default_values: bool = False,
825
838
  exclude_keys: Optional[Sequence[Union[str, int]]] = None,
826
839
  use_inferred: bool = False,
840
+ omit_symbolic_marker: bool = True,
827
841
  **kwargs,
828
842
  ) -> utils.JSONValueType:
829
843
  """Converts current object to a dict with plain Python objects."""
830
844
  exclude_keys = set(exclude_keys or [])
845
+ json_repr = {}
846
+ if not omit_symbolic_marker:
847
+ json_repr[utils.JSONConvertible.SYMBOLIC_MARKER] = True
831
848
  if self._value_spec and self._value_spec.schema:
832
- json_repr = dict()
833
849
  matched_keys, _ = self._value_spec.schema.resolve(self.keys()) # pytype: disable=attribute-error
834
850
  for key_spec, keys in matched_keys.items():
835
851
  # NOTE(daiyip): The key values of frozen field can safely be excluded
@@ -851,20 +867,23 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
851
867
  hide_frozen=hide_frozen,
852
868
  hide_default_values=hide_default_values,
853
869
  use_inferred=use_inferred,
854
- **kwargs)
855
- return json_repr
870
+ omit_symbolic_marker=omit_symbolic_marker,
871
+ **kwargs
872
+ )
856
873
  else:
857
- return {
874
+ json_repr.update({
858
875
  k: base.to_json(
859
876
  self.sym_inferred(k, default=v) if (
860
877
  use_inferred and isinstance(v, base.Inferential)) else v,
861
878
  hide_frozen=hide_frozen,
862
879
  hide_default_values=hide_default_values,
863
880
  use_inferred=use_inferred,
881
+ omit_symbolic_marker=omit_symbolic_marker,
864
882
  **kwargs)
865
883
  for k, v in self.sym_items()
866
884
  if k not in exclude_keys
867
- }
885
+ })
886
+ return json_repr
868
887
 
869
888
  def custom_apply(
870
889
  self,
@@ -415,6 +415,19 @@ class DictTest(unittest.TestCase):
415
415
  with self.assertRaisesRegex(KeyError, 'Key \'y1\' is not allowed'):
416
416
  sd.y1 = 4
417
417
 
418
+ def test_get(self):
419
+ sd = Dict(a=1)
420
+ self.assertEqual(sd.get('a'), 1)
421
+ self.assertIsNone(sd.get('x'))
422
+ self.assertEqual(sd.get('x', 2), 2)
423
+
424
+ # Test inferred values.
425
+ sd = Dict(x=inferred.ValueFromParentChain())
426
+ self.assertIsNone(sd.get('x'))
427
+
428
+ _ = Dict(sd=sd, x=1)
429
+ self.assertEqual(sd.get('x'), 1)
430
+
418
431
  def test_getattr(self):
419
432
  sd = Dict(a=1)
420
433
  self.assertEqual(sd.a, 1)
@@ -713,6 +726,11 @@ class DictTest(unittest.TestCase):
713
726
  ]))
714
727
  self.assertEqual(list(sd.values()), [2, 1, 3])
715
728
 
729
+ # Test values with inferred values.
730
+ sd = Dict(x=1, y=inferred.ValueFromParentChain())
731
+ _ = Dict(sd=sd, y=2)
732
+ self.assertEqual(list(sd.values()), [1, 2])
733
+
716
734
  def test_items(self):
717
735
  sd = Dict(b={'c': True, 'd': []}, a=0)
718
736
  self.assertEqual(list(sd.items()), [('b', {'c': True, 'd': []}), ('a', 0)])
@@ -725,6 +743,11 @@ class DictTest(unittest.TestCase):
725
743
  ]))
726
744
  self.assertEqual(list(sd.items()), [('b', 2), ('a', 1), ('c', 3)])
727
745
 
746
+ # Test items with inferred values.
747
+ sd = Dict(x=1, y=inferred.ValueFromParentChain())
748
+ _ = Dict(sd=sd, y=2)
749
+ self.assertEqual(list(sd.items()), [('x', 1), ('y', 2)])
750
+
728
751
  def test_non_default(self):
729
752
  sd = Dict(a=1)
730
753
  self.assertEqual(len(sd.non_default_values()), 1)
@@ -923,6 +946,14 @@ class DictTest(unittest.TestCase):
923
946
  sd.sym_jsonify(),
924
947
  {'x': 1, 'y': inferred.ValueFromParentChain().to_json()},
925
948
  )
949
+ self.assertEqual(
950
+ sd.sym_jsonify(omit_symbolic_marker=False),
951
+ {
952
+ '__symbolic__': True,
953
+ 'x': 1,
954
+ 'y': inferred.ValueFromParentChain().to_json()
955
+ },
956
+ )
926
957
 
927
958
  def test_sym_rebind(self):
928
959
  # Refer to RebindTest for more detailed tests.
@@ -1941,6 +1972,24 @@ class SerializationTest(unittest.TestCase):
1941
1972
  self.assertEqual(sd.to_json_str(), '{"x": 1, "y": 2.0}')
1942
1973
  self.assertEqual(base.from_json_str(sd.to_json_str(), value_spec=spec), sd)
1943
1974
 
1975
+ def test_auto_symbolic(self):
1976
+ value = base.from_json({'x': 1}, auto_symbolic=True)
1977
+ self.assertIsInstance(value, Dict)
1978
+
1979
+ value = base.from_json({'x': 1}, auto_symbolic=False)
1980
+ self.assertNotIsInstance(value, Dict)
1981
+
1982
+ def test_omit_symbolic_marker(self):
1983
+ sd = Dict(x=1)
1984
+ self.assertEqual(sd.to_json(omit_symbolic_marker=True), {'x': 1})
1985
+ self.assertEqual(
1986
+ sd.to_json(omit_symbolic_marker=False),
1987
+ {'__symbolic__': True, 'x': 1}
1988
+ )
1989
+ sd = base.from_json({'__symbolic__': True, 'x': 1}, auto_symbolic=False)
1990
+ self.assertIsInstance(sd, Dict)
1991
+ self.assertEqual(sd, {'x': 1})
1992
+
1944
1993
  def test_hide_frozen(self):
1945
1994
 
1946
1995
  class A(pg_object.Object):
@@ -137,6 +137,9 @@ class List(list, base.Symbolic, pg_typing.CustomTyping):
137
137
  Returns:
138
138
  A schema-less symbolic list, but its items maybe symbolic.
139
139
  """
140
+ # Remove symbolic marker if present.
141
+ if json_value and json_value[0] == utils.JSONConvertible.SYMBOLIC_MARKER:
142
+ json_value.pop(0)
140
143
  return cls(
141
144
  [
142
145
  base.from_json(
@@ -770,15 +773,26 @@ class List(list, base.Symbolic, pg_typing.CustomTyping):
770
773
  return (proceed_with_standard_apply, self)
771
774
 
772
775
  def sym_jsonify(
773
- self, use_inferred: bool = False, **kwargs
776
+ self,
777
+ use_inferred: bool = False,
778
+ omit_symbolic_marker: bool = True,
779
+ **kwargs
774
780
  ) -> utils.JSONValueType:
775
781
  """Converts current list to a list of plain Python objects."""
776
782
  def json_item(idx):
777
783
  v = self.sym_getattr(idx)
778
784
  if use_inferred and isinstance(v, base.Inferential):
779
785
  v = self.sym_inferred(idx, default=v)
780
- return base.to_json(v, use_inferred=use_inferred, **kwargs)
781
- return [json_item(i) for i in range(len(self))]
786
+ return base.to_json(
787
+ v,
788
+ use_inferred=use_inferred,
789
+ omit_symbolic_marker=omit_symbolic_marker,
790
+ **kwargs
791
+ )
792
+ json_value = [json_item(i) for i in range(len(self))]
793
+ if not omit_symbolic_marker:
794
+ json_value.insert(0, utils.JSONConvertible.SYMBOLIC_MARKER)
795
+ return json_value
782
796
 
783
797
  def format(
784
798
  self,
@@ -506,8 +506,7 @@ class ListTest(unittest.TestCase):
506
506
  def test_index(self):
507
507
  sl = List([0, 1, 2, 1])
508
508
  self.assertEqual(sl.index(1), 1)
509
- with self.assertRaisesRegex(
510
- ValueError, '3 is not in list'):
509
+ with self.assertRaisesRegex(ValueError, '.* not in list'):
511
510
  _ = sl.index(3)
512
511
 
513
512
  # Index of inferred value is based on its symbolic form.
@@ -799,6 +798,10 @@ class ListTest(unittest.TestCase):
799
798
  self.assertEqual(
800
799
  sl.sym_jsonify(), [0, inferred.ValueFromParentChain().to_json()]
801
800
  )
801
+ self.assertEqual(
802
+ sl.sym_jsonify(omit_symbolic_marker=False),
803
+ ['__symbolic__', 0, inferred.ValueFromParentChain().to_json()]
804
+ )
802
805
 
803
806
  def test_sym_rebind(self):
804
807
  # Refer to RebindTest for more detailed tests.
@@ -1633,6 +1636,25 @@ class SerializationTest(unittest.TestCase):
1633
1636
  'Tuple should have at least one element besides \'__tuple__\'.'):
1634
1637
  base.from_json_str('["__tuple__"]')
1635
1638
 
1639
+ def test_auto_symbolic(self):
1640
+ value = base.from_json([1, 2, 3], auto_symbolic=True)
1641
+ self.assertIsInstance(value, List)
1642
+
1643
+ value = base.from_json([1, 2, 3], auto_symbolic=False)
1644
+ self.assertNotIsInstance(value, List)
1645
+
1646
+ def test_omit_symbolic_marker(self):
1647
+ sl = List([0])
1648
+ self.assertEqual(sl.to_json(omit_symbolic_marker=True), [0])
1649
+ self.assertEqual(
1650
+ sl.to_json(omit_symbolic_marker=False),
1651
+ ['__symbolic__', 0]
1652
+ )
1653
+ sl = base.from_json(['__symbolic__', 0], auto_symbolic=False)
1654
+ self.assertEqual(sl.to_json(omit_symbolic_marker=True), [0])
1655
+ self.assertIsInstance(sl, List)
1656
+ self.assertEqual(sl, [0])
1657
+
1636
1658
  def test_hide_default_values(self):
1637
1659
  sl = List.partial(
1638
1660
  [dict(x=1)],
@@ -339,7 +339,8 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
339
339
 
340
340
  # Set `__serialization_key__` before JSONConvertible.__init_subclass__
341
341
  # is called.
342
- setattr(cls, '__serialization_key__', cls.__type_name__)
342
+ if '__serialization_key__' not in cls.__dict__:
343
+ setattr(cls, '__serialization_key__', cls.__type_name__)
343
344
 
344
345
  super().__init_subclass__()
345
346
 
@@ -994,6 +995,7 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
994
995
  self.__class__.__serialization_key__
995
996
  )
996
997
  }
998
+ kwargs['omit_symbolic_marker'] = True
997
999
  json_dict.update(self._sym_attributes.to_json(**kwargs))
998
1000
  return json_dict
999
1001
 
@@ -38,6 +38,7 @@ from pyglove.core.symbolic.object import use_init_args as pg_use_init_args
38
38
  from pyglove.core.symbolic.origin import Origin
39
39
  from pyglove.core.symbolic.pure_symbolic import NonDeterministic
40
40
  from pyglove.core.symbolic.pure_symbolic import PureSymbolic
41
+ from pyglove.core.symbolic.unknown_symbols import UnknownTypedObject
41
42
  from pyglove.core.views.html import tree_view # pylint: disable=unused-import
42
43
 
43
44
 
@@ -3158,7 +3159,7 @@ class SerializationTest(unittest.TestCase):
3158
3159
  Q.partial(P.partial()).to_json_str(), allow_partial=True),
3159
3160
  Q.partial(P.partial()))
3160
3161
 
3161
- def test_serialization_with_auto_dict(self):
3162
+ def test_serialization_with_convert_unknown(self):
3162
3163
 
3163
3164
  class P(Object):
3164
3165
  auto_register = False
@@ -3181,15 +3182,17 @@ class SerializationTest(unittest.TestCase):
3181
3182
  }
3182
3183
  )
3183
3184
  self.assertEqual(
3184
- base.from_json_str(Q(P(1), y='foo').to_json_str(), auto_dict=True),
3185
- {
3186
- 'p': {
3187
- 'type_name': P.__type_name__,
3188
- 'x': 1
3189
- },
3190
- 'y': 'foo',
3191
- 'type_name': Q.__type_name__,
3192
- }
3185
+ base.from_json_str(
3186
+ Q(P(1), y='foo').to_json_str(), convert_unknown=True
3187
+ ),
3188
+ UnknownTypedObject(
3189
+ type_name=Q.__type_name__,
3190
+ p=UnknownTypedObject(
3191
+ type_name=P.__type_name__,
3192
+ x=1
3193
+ ),
3194
+ y='foo'
3195
+ )
3193
3196
  )
3194
3197
 
3195
3198
  def test_serialization_with_converter(self):
@@ -138,9 +138,7 @@ class Ref(
138
138
  del child_transform
139
139
  # Check if the field being assigned could accept the referenced value.
140
140
  # We do not do any transformation, thus not passing the child transform.
141
- value_spec.apply(
142
- self._value,
143
- allow_partial=allow_partial)
141
+ value_spec.apply(self._value, allow_partial=allow_partial)
144
142
  return (False, self)
145
143
 
146
144
  def _sym_clone(self, deep: bool, memo: Any = None) -> 'Ref':
@@ -152,10 +150,24 @@ class Ref(
152
150
  def sym_eq(self, other: Any) -> bool:
153
151
  return isinstance(other, Ref) and self.value is other.value
154
152
 
155
- def sym_jsonify(self, *, save_ref_value: bool = False, **kwargs: Any) -> Any:
156
- if save_ref_value:
157
- return base.to_json(self._value, save_ref_value=save_ref_value, **kwargs)
158
- raise TypeError(f'{self!r} cannot be serialized at the moment.')
153
+ def sym_jsonify(
154
+ self,
155
+ *,
156
+ context: utils.JSONConversionContext,
157
+ **kwargs: Any
158
+ ) -> Any:
159
+ # Disable auto_symbolic for Ref value. This allows Ref to create a sub-tree
160
+ # for reference sharing.
161
+ kwargs['omit_symbolic_marker'] = False
162
+ return {
163
+ utils.JSONConvertible.TYPE_NAME_KEY: self.__class__.__type_name__,
164
+ 'value': context.serialize_maybe_shared(self._value, **kwargs)
165
+ }
166
+
167
+ @classmethod
168
+ def from_json(cls, json: Any, **kwargs):
169
+ kwargs['auto_symbolic'] = False
170
+ return super().from_json(json, **kwargs)
159
171
 
160
172
  def __getstate__(self):
161
173
  raise TypeError(f'{self!r} cannot be pickled at the moment.')