pyglove 0.5.0.dev202510220812__py3-none-any.whl → 0.5.0.dev202510230811__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 CHANGED
@@ -1447,6 +1447,7 @@ class DNA(symbolic.Object):
1447
1447
  *,
1448
1448
  allow_partial: bool = False,
1449
1449
  root_path: Optional[utils.KeyPath] = None,
1450
+ **kwargs,
1450
1451
  ) -> 'DNA':
1451
1452
  """Class method that load a DNA from a JSON value.
1452
1453
 
@@ -1454,6 +1455,7 @@ class DNA(symbolic.Object):
1454
1455
  json_value: Input JSON value, only JSON dict is acceptable.
1455
1456
  allow_partial: Whether to allow elements of the list to be partial.
1456
1457
  root_path: KeyPath of loaded object in its object tree.
1458
+ **kwargs: Keyword arguments that will be passed to symbolic.from_json.
1457
1459
 
1458
1460
  Returns:
1459
1461
  A DNA object.
@@ -1463,16 +1465,18 @@ class DNA(symbolic.Object):
1463
1465
  # NOTE(daiyip): DNA.parse will validate the input. Therefore, we can
1464
1466
  # disable runtime type check during constructing the DNA objects.
1465
1467
  with symbolic.enable_type_check(False):
1466
- dna = DNA.parse(symbolic.from_json(json_value.get('value')))
1468
+ dna = DNA.parse(symbolic.from_json(json_value.get('value'), **kwargs))
1467
1469
  if 'metadata' in json_value:
1468
1470
  dna.rebind(
1469
- metadata=symbolic.from_json(json_value.get('metadata')),
1471
+ metadata=symbolic.from_json(json_value.get('metadata'), **kwargs),
1470
1472
  raise_on_no_change=False, skip_notification=True)
1471
1473
  else:
1472
1474
  dna = super(DNA, cls).from_json(
1473
1475
  json_value,
1474
1476
  allow_partial=allow_partial,
1475
- root_path=root_path) # pytype: disable=bad-return-type
1477
+ root_path=root_path,
1478
+ **kwargs,
1479
+ ) # pytype: disable=bad-return-type
1476
1480
  assert isinstance(dna, DNA)
1477
1481
  if cloneable_metadata_keys:
1478
1482
  dna._cloneable_metadata_keys = set(cloneable_metadata_keys) # pylint: disable=protected-access
@@ -946,7 +946,7 @@ class Symbolic(
946
946
 
947
947
  def to_json(self, **kwargs) -> utils.JSONValueType:
948
948
  """Alias for `sym_jsonify`."""
949
- return to_json(self, **kwargs)
949
+ return utils.to_json(self, **kwargs)
950
950
 
951
951
  def to_json_str(self, json_indent: Optional[int] = None, **kwargs) -> str:
952
952
  """Serializes current object into a JSON string."""
@@ -1985,10 +1985,12 @@ def is_abstract(x: Any) -> bool:
1985
1985
  def contains(
1986
1986
  x: Any,
1987
1987
  value: Any = None,
1988
- type: Optional[Union[ # pylint: disable=redefined-builtin
1988
+ type: Union[ # pylint: disable=redefined-builtin
1989
1989
  Type[Any],
1990
- Tuple[Type[Any]]]]=None
1991
- ) -> bool:
1990
+ Tuple[Type[Any], ...],
1991
+ None,
1992
+ ]=None,
1993
+ ) -> bool:
1992
1994
  """Returns if a value contains values of specific type.
1993
1995
 
1994
1996
  Example::
@@ -2037,10 +2039,12 @@ def contains(
2037
2039
  def from_json(
2038
2040
  json_value: Any,
2039
2041
  *,
2040
- allow_partial: bool = False,
2041
- root_path: Optional[utils.KeyPath] = None,
2042
+ context: Optional[utils.JSONConversionContext] = None,
2043
+ auto_symbolic: bool = True,
2042
2044
  auto_import: bool = True,
2043
2045
  auto_dict: bool = False,
2046
+ allow_partial: bool = False,
2047
+ root_path: Optional[utils.KeyPath] = None,
2044
2048
  value_spec: Optional[pg_typing.ValueSpec] = None,
2045
2049
  **kwargs,
2046
2050
  ) -> Any:
@@ -2061,14 +2065,18 @@ def from_json(
2061
2065
 
2062
2066
  Args:
2063
2067
  json_value: Input JSON value.
2064
- allow_partial: Whether to allow elements of the list to be partial.
2065
- 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.
2066
2072
  auto_import: If True, when a '_type' is not registered, PyGlove will
2067
2073
  identify its parent module and automatically import it. For example,
2068
2074
  if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
2069
2075
  find the class 'A' within the imported module.
2070
2076
  auto_dict: If True, dict with '_type' that cannot be loaded will remain
2071
2077
  as dict, with '_type' renamed to 'type_name'.
2078
+ allow_partial: Whether to allow elements of the list to be partial.
2079
+ root_path: KeyPath of loaded object in its object tree.
2072
2080
  value_spec: The value spec for the symbolic list or dict.
2073
2081
  **kwargs: Allow passing through keyword arguments to from_json of specific
2074
2082
  types.
@@ -2084,10 +2092,18 @@ def from_json(
2084
2092
  if isinstance(json_value, Symbolic):
2085
2093
  return json_value
2086
2094
 
2095
+ if context is None:
2096
+ if (isinstance(json_value, dict) and (
2097
+ context_node := json_value.get(utils.JSONConvertible.CONTEXT_KEY))):
2098
+ context = utils.JSONConversionContext.from_json(context_node, **kwargs)
2099
+ json_value = json_value[utils.JSONConvertible.ROOT_VALUE_KEY]
2100
+ else:
2101
+ context = utils.JSONConversionContext()
2102
+
2087
2103
  typename_resolved = kwargs.pop('_typename_resolved', False)
2088
2104
  if not typename_resolved:
2089
2105
  json_value = utils.json_conversion.resolve_typenames(
2090
- json_value, auto_import=auto_import, auto_dict=auto_dict
2106
+ json_value, auto_import, auto_dict
2091
2107
  )
2092
2108
 
2093
2109
  def _load_child(k, v):
@@ -2096,6 +2112,7 @@ def from_json(
2096
2112
  root_path=utils.KeyPath(k, root_path),
2097
2113
  _typename_resolved=True,
2098
2114
  allow_partial=allow_partial,
2115
+ context=context,
2099
2116
  **kwargs,
2100
2117
  )
2101
2118
 
@@ -2111,24 +2128,42 @@ def from_json(
2111
2128
  )
2112
2129
  )
2113
2130
  return tuple(_load_child(i, v) for i, v in enumerate(json_value[1:]))
2114
- return Symbolic.ListType.from_json( # pytype: disable=attribute-error
2131
+ if json_value and json_value[0] == utils.JSONConvertible.SYMBOLIC_MARKER:
2132
+ auto_symbolic = True
2133
+ if auto_symbolic:
2134
+ from_json_fn = Symbolic.ListType.from_json # pytype: disable=attribute-error
2135
+ else:
2136
+ from_json_fn = utils.from_json
2137
+ return from_json_fn(
2115
2138
  json_value,
2139
+ context=context,
2116
2140
  value_spec=value_spec,
2117
2141
  root_path=root_path,
2118
2142
  allow_partial=allow_partial,
2119
2143
  **kwargs,
2120
2144
  )
2121
2145
  elif isinstance(json_value, dict):
2146
+ if utils.JSONConvertible.REF_KEY in json_value:
2147
+ x = context.get_shared(
2148
+ json_value[utils.JSONConvertible.REF_KEY]
2149
+ ).value
2150
+ return x
2122
2151
  if utils.JSONConvertible.TYPE_NAME_KEY not in json_value:
2123
- return Symbolic.DictType.from_json( # pytype: disable=attribute-error
2124
- json_value,
2125
- value_spec=value_spec,
2126
- root_path=root_path,
2127
- allow_partial=allow_partial,
2128
- **kwargs,
2152
+ auto_symbolic = json_value.get(
2153
+ utils.JSONConvertible.SYMBOLIC_MARKER, auto_symbolic
2129
2154
  )
2155
+ if auto_symbolic:
2156
+ return Symbolic.DictType.from_json( # pytype: disable=attribute-error
2157
+ json_value,
2158
+ context=context,
2159
+ value_spec=value_spec,
2160
+ root_path=root_path,
2161
+ allow_partial=allow_partial,
2162
+ **kwargs,
2163
+ )
2130
2164
  return utils.from_json(
2131
2165
  json_value,
2166
+ context=context,
2132
2167
  _typename_resolved=True,
2133
2168
  root_path=root_path,
2134
2169
  allow_partial=allow_partial,
@@ -2140,10 +2175,12 @@ def from_json(
2140
2175
  def from_json_str(
2141
2176
  json_str: str,
2142
2177
  *,
2143
- allow_partial: bool = False,
2144
- root_path: Optional[utils.KeyPath] = None,
2178
+ context: Optional[utils.JSONConversionContext] = None,
2145
2179
  auto_import: bool = True,
2146
2180
  auto_dict: bool = False,
2181
+ allow_partial: bool = False,
2182
+ root_path: Optional[utils.KeyPath] = None,
2183
+ value_spec: Optional[pg_typing.ValueSpec] = None,
2147
2184
  **kwargs,
2148
2185
  ) -> Any:
2149
2186
  """Deserialize (maybe) symbolic object from JSON string.
@@ -2163,15 +2200,17 @@ def from_json_str(
2163
2200
 
2164
2201
  Args:
2165
2202
  json_str: JSON string.
2166
- allow_partial: If True, allow a partial symbolic object to be created.
2167
- Otherwise error will be raised on partial value.
2168
- root_path: The symbolic path used for the deserialized root object.
2203
+ context: JSON conversion context.
2169
2204
  auto_import: If True, when a '_type' is not registered, PyGlove will
2170
2205
  identify its parent module and automatically import it. For example,
2171
2206
  if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
2172
2207
  find the class 'A' within the imported module.
2173
2208
  auto_dict: If True, dict with '_type' that cannot be loaded will remain
2174
2209
  as dict, with '_type' renamed to 'type_name'.
2210
+ allow_partial: If True, allow a partial symbolic object to be created.
2211
+ Otherwise error will be raised on partial value.
2212
+ root_path: The symbolic path used for the deserialized root object.
2213
+ value_spec: The value spec for the symbolic list or dict.
2175
2214
  **kwargs: Additional keyword arguments that will be passed to
2176
2215
  ``pg.from_json``.
2177
2216
 
@@ -2195,10 +2234,12 @@ def from_json_str(
2195
2234
 
2196
2235
  return from_json(
2197
2236
  _decode_int_keys(json.loads(json_str)),
2198
- allow_partial=allow_partial,
2199
- root_path=root_path,
2237
+ context=context,
2200
2238
  auto_import=auto_import,
2201
2239
  auto_dict=auto_dict,
2240
+ allow_partial=allow_partial,
2241
+ root_path=root_path,
2242
+ value_spec=value_spec,
2202
2243
  **kwargs
2203
2244
  )
2204
2245
 
@@ -2234,10 +2275,6 @@ def to_json(value: Any, **kwargs) -> Any:
2234
2275
  Returns:
2235
2276
  JSON value.
2236
2277
  """
2237
- # NOTE(daiyip): special handling `sym_jsonify` since symbolized
2238
- # classes may have conflicting `to_json` method in their existing classes.
2239
- if isinstance(value, Symbolic):
2240
- return value.sym_jsonify(**kwargs)
2241
2278
  return utils.to_json(value, **kwargs)
2242
2279
 
2243
2280
 
@@ -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(
@@ -835,12 +837,15 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
835
837
  hide_default_values: bool = False,
836
838
  exclude_keys: Optional[Sequence[Union[str, int]]] = None,
837
839
  use_inferred: bool = False,
840
+ omit_symbolic_marker: bool = True,
838
841
  **kwargs,
839
842
  ) -> utils.JSONValueType:
840
843
  """Converts current object to a dict with plain Python objects."""
841
844
  exclude_keys = set(exclude_keys or [])
845
+ json_repr = {}
846
+ if not omit_symbolic_marker:
847
+ json_repr[utils.JSONConvertible.SYMBOLIC_MARKER] = True
842
848
  if self._value_spec and self._value_spec.schema:
843
- json_repr = dict()
844
849
  matched_keys, _ = self._value_spec.schema.resolve(self.keys()) # pytype: disable=attribute-error
845
850
  for key_spec, keys in matched_keys.items():
846
851
  # NOTE(daiyip): The key values of frozen field can safely be excluded
@@ -862,20 +867,23 @@ class Dict(dict, base.Symbolic, pg_typing.CustomTyping):
862
867
  hide_frozen=hide_frozen,
863
868
  hide_default_values=hide_default_values,
864
869
  use_inferred=use_inferred,
865
- **kwargs)
866
- return json_repr
870
+ omit_symbolic_marker=omit_symbolic_marker,
871
+ **kwargs
872
+ )
867
873
  else:
868
- return {
874
+ json_repr.update({
869
875
  k: base.to_json(
870
876
  self.sym_inferred(k, default=v) if (
871
877
  use_inferred and isinstance(v, base.Inferential)) else v,
872
878
  hide_frozen=hide_frozen,
873
879
  hide_default_values=hide_default_values,
874
880
  use_inferred=use_inferred,
881
+ omit_symbolic_marker=omit_symbolic_marker,
875
882
  **kwargs)
876
883
  for k, v in self.sym_items()
877
884
  if k not in exclude_keys
878
- }
885
+ })
886
+ return json_repr
879
887
 
880
888
  def custom_apply(
881
889
  self,
@@ -946,6 +946,14 @@ class DictTest(unittest.TestCase):
946
946
  sd.sym_jsonify(),
947
947
  {'x': 1, 'y': inferred.ValueFromParentChain().to_json()},
948
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
+ )
949
957
 
950
958
  def test_sym_rebind(self):
951
959
  # Refer to RebindTest for more detailed tests.
@@ -1964,6 +1972,24 @@ class SerializationTest(unittest.TestCase):
1964
1972
  self.assertEqual(sd.to_json_str(), '{"x": 1, "y": 2.0}')
1965
1973
  self.assertEqual(base.from_json_str(sd.to_json_str(), value_spec=spec), sd)
1966
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
+
1967
1993
  def test_hide_frozen(self):
1968
1994
 
1969
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,
@@ -799,6 +799,10 @@ class ListTest(unittest.TestCase):
799
799
  self.assertEqual(
800
800
  sl.sym_jsonify(), [0, inferred.ValueFromParentChain().to_json()]
801
801
  )
802
+ self.assertEqual(
803
+ sl.sym_jsonify(omit_symbolic_marker=False),
804
+ ['__symbolic__', 0, inferred.ValueFromParentChain().to_json()]
805
+ )
802
806
 
803
807
  def test_sym_rebind(self):
804
808
  # Refer to RebindTest for more detailed tests.
@@ -1633,6 +1637,25 @@ class SerializationTest(unittest.TestCase):
1633
1637
  'Tuple should have at least one element besides \'__tuple__\'.'):
1634
1638
  base.from_json_str('["__tuple__"]')
1635
1639
 
1640
+ def test_auto_symbolic(self):
1641
+ value = base.from_json([1, 2, 3], auto_symbolic=True)
1642
+ self.assertIsInstance(value, List)
1643
+
1644
+ value = base.from_json([1, 2, 3], auto_symbolic=False)
1645
+ self.assertNotIsInstance(value, List)
1646
+
1647
+ def test_omit_symbolic_marker(self):
1648
+ sl = List([0])
1649
+ self.assertEqual(sl.to_json(omit_symbolic_marker=True), [0])
1650
+ self.assertEqual(
1651
+ sl.to_json(omit_symbolic_marker=False),
1652
+ ['__symbolic__', 0]
1653
+ )
1654
+ sl = base.from_json(['__symbolic__', 0], auto_symbolic=False)
1655
+ self.assertEqual(sl.to_json(omit_symbolic_marker=True), [0])
1656
+ self.assertIsInstance(sl, List)
1657
+ self.assertEqual(sl, [0])
1658
+
1636
1659
  def test_hide_default_values(self):
1637
1660
  sl = List.partial(
1638
1661
  [dict(x=1)],
@@ -994,6 +994,7 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
994
994
  self.__class__.__serialization_key__
995
995
  )
996
996
  }
997
+ kwargs['omit_symbolic_marker'] = True
997
998
  json_dict.update(self._sym_attributes.to_json(**kwargs))
998
999
  return json_dict
999
1000
 
@@ -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.')
@@ -21,8 +21,11 @@ from typing import Any
21
21
  import unittest
22
22
 
23
23
  from pyglove.core import typing as pg_typing
24
+ from pyglove.core.symbolic import list as pg_list # pylint: disable=unused-import
24
25
  from pyglove.core.symbolic import ref
25
26
  from pyglove.core.symbolic.base import contains
27
+ from pyglove.core.symbolic.base import from_json
28
+ from pyglove.core.symbolic.base import to_json
26
29
  from pyglove.core.symbolic.dict import Dict
27
30
  from pyglove.core.symbolic.object import Object
28
31
 
@@ -33,6 +36,10 @@ class A(Object):
33
36
 
34
37
  class RefTest(unittest.TestCase):
35
38
 
39
+ def setUp(self):
40
+ super().setUp()
41
+ self.maxDiff = None
42
+
36
43
  def test_basics(self):
37
44
 
38
45
  a = A(1)
@@ -109,14 +116,94 @@ class RefTest(unittest.TestCase):
109
116
  """))
110
117
 
111
118
  def test_to_json(self):
112
- with self.assertRaisesRegex(
113
- TypeError, '.* cannot be serialized at the moment'):
114
- ref.Ref(A(1)).to_json()
119
+ class B(Object):
120
+ y: Any
115
121
 
116
- self.assertEqual(
117
- ref.Ref(A(1)).to_json(save_ref_value=True),
118
- A(1).to_json()
119
- )
122
+ a = A(1)
123
+ r1 = ref.Ref(a)
124
+ r2 = ref.Ref({'z': a})
125
+ r3 = ref.Ref(Dict(t=r1, p=r2))
126
+ v = Dict(a=r1, b=[B(r2), [r3], r1])
127
+ self.assertIs(v.a, v.b[0].y['z'])
128
+ self.assertIs(v.a, v.b[1][0].t)
129
+ self.assertIs(v.b[0].y, v.b[1][0].p)
130
+ self.assertIs(v.a, v.b[2])
131
+ self.assertIsInstance(v, dict)
132
+ self.assertIsInstance(v.b[0].y, dict)
133
+ self.assertNotIsInstance(v.b[0].y, Dict)
134
+ self.assertIsInstance(v.b[1][0], Dict)
135
+
136
+ json = to_json(v)
137
+ expected = {
138
+ '__context__': {
139
+ 'shared_objects': [
140
+ {
141
+ '_type': A.__type_name__,
142
+ 'x': 1
143
+ },
144
+ {
145
+ 'z': {
146
+ '__ref__': 0
147
+ }
148
+ }
149
+ ]
150
+ },
151
+ '__root__': {
152
+ 'a': {
153
+ '_type': ref.Ref.__type_name__,
154
+ 'value': {
155
+ '__ref__': 0
156
+ }
157
+ },
158
+ 'b': [
159
+ {
160
+ '_type': B.__type_name__,
161
+ 'y': {
162
+ '_type': ref.Ref.__type_name__,
163
+ 'value': {
164
+ '__ref__': 1
165
+ }
166
+ }
167
+ },
168
+ [
169
+ {
170
+ '_type': ref.Ref.__type_name__,
171
+ 'value': {
172
+ '__symbolic__': True,
173
+ 't': {
174
+ '_type': ref.Ref.__type_name__,
175
+ 'value': {
176
+ '__ref__': 0
177
+ }
178
+ },
179
+ 'p': {
180
+ '_type': ref.Ref.__type_name__,
181
+ 'value': {
182
+ '__ref__': 1
183
+ }
184
+ }
185
+ }
186
+ }
187
+ ],
188
+ {
189
+ '_type': ref.Ref.__type_name__,
190
+ 'value': {
191
+ '__ref__': 0
192
+ }
193
+ }
194
+ ]
195
+ }
196
+ }
197
+ self.assertEqual(json, expected)
198
+ v = from_json(json)
199
+ self.assertIs(v.a, v.b[0].y['z'])
200
+ self.assertIs(v.a, v.b[1][0].t)
201
+ self.assertIs(v.b[0].y, v.b[1][0].p)
202
+ self.assertIs(v.a, v.b[2])
203
+ self.assertIsInstance(v, dict)
204
+ self.assertIsInstance(v.b[0].y, dict)
205
+ self.assertNotIsInstance(v.b[0].y, Dict)
206
+ self.assertIsInstance(v.b[1][0], Dict)
120
207
 
121
208
  def test_pickle(self):
122
209
  with self.assertRaisesRegex(
@@ -318,6 +318,9 @@ def _value_spec_from_type_annotation(
318
318
  parent_module: typing.Optional[types.ModuleType] = None
319
319
  ) -> class_schema.ValueSpec:
320
320
  """Creates a value spec from type annotation."""
321
+ if isinstance(annotation, class_schema.ValueSpec):
322
+ return annotation
323
+
321
324
  if isinstance(annotation, str) and not accept_value_as_annotation:
322
325
  annotation = annotation_from_str(annotation, parent_module)
323
326
 
@@ -454,7 +457,8 @@ def _value_spec_from_annotation(
454
457
  """Creates a value spec from annotation."""
455
458
  if isinstance(annotation, class_schema.ValueSpec):
456
459
  return annotation
457
- elif annotation == inspect.Parameter.empty:
460
+
461
+ if annotation == inspect.Parameter.empty:
458
462
  return vs.Any()
459
463
 
460
464
  if annotation is None:
@@ -396,6 +396,9 @@ class ValueSpecFromAnnotationTest(unittest.TestCase):
396
396
  self.assertEqual(
397
397
  ValueSpec.from_annotation(typing.Dict[str, int], True),
398
398
  vs.Dict([(ks.StrKey(), vs.Int())]))
399
+ self.assertEqual(
400
+ ValueSpec.from_annotation(typing.Dict[str, vs.Int()], True),
401
+ vs.Dict([(ks.StrKey(), vs.Int())]))
399
402
  self.assertEqual(
400
403
  ValueSpec.from_annotation(typing.Mapping[str, int], True),
401
404
  vs.Dict([(ks.StrKey(), vs.Int())]))
@@ -74,6 +74,7 @@ modules with the following features:
74
74
  from pyglove.core.utils.json_conversion import Nestable
75
75
  from pyglove.core.utils.json_conversion import JSONValueType
76
76
 
77
+ from pyglove.core.utils.json_conversion import JSONConversionContext
77
78
  from pyglove.core.utils.json_conversion import JSONConvertible
78
79
  from pyglove.core.utils.json_conversion import from_json
79
80
  from pyglove.core.utils.json_conversion import to_json
@@ -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,23 +665,34 @@ 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
688
  auto_dict: bool = False,
463
- **kwargs) -> Any:
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
@@ -476,12 +704,20 @@ def from_json(
476
704
  Returns:
477
705
  Deserialized value.
478
706
  """
707
+ if context is None:
708
+ if (isinstance(json_value, dict)
709
+ and (context_node := json_value.get(JSONConvertible.CONTEXT_KEY))):
710
+ context = JSONConversionContext.from_json(context_node, **kwargs)
711
+ json_value = json_value[JSONConvertible.ROOT_VALUE_KEY]
712
+ else:
713
+ context = JSONConversionContext()
714
+
479
715
  typename_resolved = kwargs.pop('_typename_resolved', False)
480
716
  if not typename_resolved:
481
717
  json_value = resolve_typenames(json_value, auto_import, auto_dict)
482
718
 
483
719
  def child_from(v):
484
- return from_json(v, _typename_resolved=True, **kwargs)
720
+ return from_json(v, context=context, _typename_resolved=True, **kwargs)
485
721
 
486
722
  if isinstance(json_value, list):
487
723
  if json_value and json_value[0] == JSONConvertible.TUPLE_MARKER:
@@ -493,18 +729,21 @@ def from_json(
493
729
  return tuple([child_from(v) for v in json_value[1:]])
494
730
  return [child_from(v) for v in json_value]
495
731
  elif isinstance(json_value, dict):
732
+ if JSONConvertible.REF_KEY in json_value:
733
+ v = context.get_shared(json_value[JSONConvertible.REF_KEY]).value
734
+ return v
496
735
  if JSONConvertible.TYPE_NAME_KEY not in json_value:
497
736
  return {k: child_from(v) for k, v in json_value.items()}
498
737
  factory_fn = json_value.pop(JSONConvertible.TYPE_NAME_KEY)
499
738
  assert factory_fn is not None
500
- return factory_fn(json_value, **kwargs)
739
+ return factory_fn(json_value, context=context, **kwargs)
501
740
  return json_value
502
741
 
503
742
 
504
743
  def resolve_typenames(
505
744
  json_value: JSONValueType,
506
745
  auto_import: bool = True,
507
- auto_dict: bool = False
746
+ auto_dict: bool = False,
508
747
  ) -> JSONValueType:
509
748
  """Inplace resolves the "_type" keys with their factories in a JSON tree."""
510
749
 
@@ -156,7 +156,7 @@ class JSONConvertibleTest(unittest.TestCase):
156
156
  def __init__(self, x=None):
157
157
  self.x = x
158
158
 
159
- def to_json(self):
159
+ def to_json(self, **kwargs):
160
160
  return T.to_json_dict(dict(x=(self.x, None)), exclude_default=True)
161
161
 
162
162
  def __eq__(self, other):
@@ -202,7 +202,7 @@ class JSONConvertibleTest(unittest.TestCase):
202
202
  def __init__(self, x=None):
203
203
  self.x = x
204
204
 
205
- def to_json(self):
205
+ def to_json(self, **kwargs):
206
206
  return self.to_json_dict(
207
207
  dict(x=(self.x, None)), exclude_default=True
208
208
  )
@@ -215,7 +215,6 @@ class JSONConvertibleTest(unittest.TestCase):
215
215
 
216
216
  def test_json_conversion_with_auto_import(self):
217
217
  json_dict = json_conversion.to_json(self.CustomJsonConvertible(1))
218
-
219
218
  with self.assertRaisesRegex(
220
219
  TypeError, 'Type name .* is not registered'):
221
220
  json_conversion.from_json(json_dict, auto_import=False)
@@ -393,6 +392,65 @@ class JSONConvertibleTest(unittest.TestCase):
393
392
  TypeError, '.* is not a `pg.JSONConvertible` subclass'):
394
393
  json_conversion.from_json({'_type': '__main__.A'})
395
394
 
395
+ def test_json_conversion_with_sharing(self):
396
+
397
+ class T(json_conversion.JSONConvertible):
398
+
399
+ def __init__(self, x=None):
400
+ self.x = x
401
+
402
+ def to_json(self, **kwargs):
403
+ return T.to_json_dict(dict(x=(self.x, None)), exclude_default=True)
404
+
405
+ t = T(1)
406
+ x = X(1)
407
+ u = {'x': x}
408
+ v = [u, t]
409
+ y = dict(t=t, x=x, u=u, v=v)
410
+ y_json = json_conversion.to_json(y)
411
+ x_serialized = json_conversion._OpaqueObject(x).to_json()
412
+ self.assertEqual(
413
+ y_json,
414
+ {
415
+ '__context__': {
416
+ 'shared_objects': [
417
+ {
418
+ '_type': json_conversion._type_name(T),
419
+ 'x': 1
420
+ },
421
+ x_serialized,
422
+ {
423
+ 'x': {
424
+ '__ref__': 1
425
+ }
426
+ }
427
+ ]
428
+ },
429
+ '__root__': {
430
+ 't': {
431
+ '__ref__': 0
432
+ },
433
+ 'x': {
434
+ '__ref__': 1
435
+ },
436
+ 'u': {
437
+ '__ref__': 2
438
+ },
439
+ 'v': [
440
+ {
441
+ '__ref__': 2
442
+ },
443
+ {
444
+ '__ref__': 0
445
+ }
446
+ ]
447
+ }
448
+ }
449
+ )
450
+ y_prime = json_conversion.from_json(y_json)
451
+ self.assertIs(y_prime['t'], y_prime['v'][1])
452
+ self.assertIs(y_prime['u'], y_prime['v'][0])
453
+
396
454
 
397
455
  if __name__ == '__main__':
398
456
  unittest.main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyglove
3
- Version: 0.5.0.dev202510220812
3
+ Version: 0.5.0.dev202510230811
4
4
  Summary: PyGlove: A library for manipulating Python objects.
5
5
  Home-page: https://github.com/google/pyglove
6
6
  Author: PyGlove Authors
@@ -19,7 +19,7 @@ pyglove/core/detouring/__init__.py,sha256=ck_n2VSuU31HNVYQkbG4Zvnx90mNYtSVc2StN3
19
19
  pyglove/core/detouring/class_detour.py,sha256=ejuUr7UfRU3l9PrDxD0dpKmt2iqdDU6liHdebA1jEfQ,13312
20
20
  pyglove/core/detouring/class_detour_test.py,sha256=9aAK6qPiT0_HJe5oUpqMVTpoHv0wr_h6c4gWYKMTJoM,5507
21
21
  pyglove/core/geno/__init__.py,sha256=cWA583n7JpsGUkGtoTlMMX83tNS7geRBKFsoPEvFZ4A,4767
22
- pyglove/core/geno/base.py,sha256=4CjZ1j-LGd6q2qootGcLMVuX8nTDxH0LvA1KVSI9qbE,64498
22
+ pyglove/core/geno/base.py,sha256=fZaTmmCJaiIm2ACU8EgEUHQLr9iF0f6yBNdYWIwYKWg,64639
23
23
  pyglove/core/geno/base_test.py,sha256=f0pMpfXsjAqFOBtlOxof3jRX0jLvXpzowk9szcr6bgY,38512
24
24
  pyglove/core/geno/categorical.py,sha256=t1bS129D5iF4CZ0yVHaFDXy2T6ujv5F5HHDc_0X36z8,28015
25
25
  pyglove/core/geno/categorical_test.py,sha256=23UkOPhDSvonxxM_1YoBYp723Xim-BmBzywl612CRyg,18444
@@ -68,8 +68,8 @@ pyglove/core/patching/pattern_based_test.py,sha256=PW1EcVfsFPB6wtgwg3s4dzvigWn3b
68
68
  pyglove/core/patching/rule_based.py,sha256=JAQp8mWeIOxwIdqusA3GmXia-fxQhQsxbUTmE329wF8,17038
69
69
  pyglove/core/patching/rule_based_test.py,sha256=qfy0ILmczV_LMHWEnwo2y079OrJsGYO0nKxSZdmIUcI,18782
70
70
  pyglove/core/symbolic/__init__.py,sha256=VMQj8oW2hGaJwqgchBJbu5qXoJKaNizxv__vsYL4b7U,6057
71
- pyglove/core/symbolic/base.py,sha256=l344Ajup-nYk3Rmn0E9VsuWTtMSVgnI58YiCqNx-N5M,77802
72
- pyglove/core/symbolic/base_test.py,sha256=yASIHIuWiUB1jf4nN-Y4XOjyvr8eQfRpr7s_1LZsu78,7188
71
+ pyglove/core/symbolic/base.py,sha256=aRo-v1w1kGHPhJNddaGA-SZMeLSQs4dNyRPEjHH4Phg,79102
72
+ pyglove/core/symbolic/base_test.py,sha256=OHEexXI7uE2bixT-trfR3j-dfiAJsbY7pFyAA6XEPqA,7338
73
73
  pyglove/core/symbolic/boilerplate.py,sha256=sQ3G25r5bo_UmIdjreL4jkAuQCXIHVlvUfGjjkNod6Y,5955
74
74
  pyglove/core/symbolic/boilerplate_test.py,sha256=1CZ1W6kq3l-3tpaknhGFa04V18bO7vPzis5qzWnxHEs,5252
75
75
  pyglove/core/symbolic/class_wrapper.py,sha256=xQiMh5vFlOQ76tbqsF5UWEghvU4d9UmvbNqIyBccNHg,22869
@@ -78,8 +78,8 @@ pyglove/core/symbolic/compounding.py,sha256=gvOodZ2gWHA0jNdwt8yvnRsPkHQDXDb5s88U
78
78
  pyglove/core/symbolic/compounding_test.py,sha256=hOzrIROvajUTtPm0SUbEsEV4C1bXanhAoHinHrjZoXw,8320
79
79
  pyglove/core/symbolic/contextual_object.py,sha256=ar9Q_0P0HHbDf8kLHpS8GFZEMCRuCCB6DP18iGItiiw,9146
80
80
  pyglove/core/symbolic/contextual_object_test.py,sha256=wA5xfIhEHOC9qE3bbiA59CAPxWs9AVPaNiKEoprkWpQ,10209
81
- pyglove/core/symbolic/dict.py,sha256=mgukJUvw1FxdozoRzUzMnqsIm_WZZae-rTskh9pzLWk,37284
82
- pyglove/core/symbolic/dict_test.py,sha256=sRTpdb1xWAOl87GoUn8GIh-oRhMlrj_EmJ7hkLZCH4Y,73593
81
+ pyglove/core/symbolic/dict.py,sha256=ELM-5XSxChNZNTaDxRDm_rCpMkuJ2nBASMOMxVjB07g,37657
82
+ pyglove/core/symbolic/dict_test.py,sha256=XPOe5DV_opYMkzSJZEl7rTvBBN-H9NvKe8zC7tQf7EE,74429
83
83
  pyglove/core/symbolic/diff.py,sha256=zHfED0Bbq8G_HWNPj3vrOCWzt_062rFhx3BMlpCb9oo,16282
84
84
  pyglove/core/symbolic/diff_test.py,sha256=EDiGHqqKhi-NeMxr-bgjBEqlquee_4l_0IM6hgAb9Mg,29400
85
85
  pyglove/core/symbolic/error_info.py,sha256=rqRwfmnEibMixaS2G-P0VhKjkZl79qjO6EUItnATHlQ,3675
@@ -90,15 +90,15 @@ pyglove/core/symbolic/functor.py,sha256=AwE9GX2cO3QNihac1_ZN0sdG-TrWAJ5lXO2ZQ5Gs
90
90
  pyglove/core/symbolic/functor_test.py,sha256=9c5_7OBKNVNbYC7IaVQB6c5ks2v00qQ36oivyWiBbKA,31798
91
91
  pyglove/core/symbolic/inferred.py,sha256=E4zgphg6NNZad9Fl3jdHQOMZeqEp9XHq5OUYqXEmwZQ,3178
92
92
  pyglove/core/symbolic/inferred_test.py,sha256=G6uPykONcChvs6vZujXHSWaYfjewLTVBscMqzzKNty0,1270
93
- pyglove/core/symbolic/list.py,sha256=z8goU0ntd-Q5ADaCGiKsJwPhdRdQb0Kd_p-ZekXaLy4,30303
94
- pyglove/core/symbolic/list_test.py,sha256=IAyFQ48nyczKUcPNZFKHBkX5oh7Xuxbnv3rRkONhbHw,61146
95
- pyglove/core/symbolic/object.py,sha256=OZ6eK8qyaLsUKxFiV2GWL5Ol6FDzMrrkpWSRKRwIKAA,42726
93
+ pyglove/core/symbolic/list.py,sha256=CRDoBxYJsmNly1MxhY5vO0wp6EUTnxze6-2O9vXHna4,30717
94
+ pyglove/core/symbolic/list_test.py,sha256=Q3kqQ6aaQ40Nw6osSxrGk4N_E1lGZyvwdvxZgLRwMS0,61954
95
+ pyglove/core/symbolic/object.py,sha256=UgDgKdcYDYX3J51EMQamdwaXHkG4SHPunAGkSuHuTa0,42768
96
96
  pyglove/core/symbolic/object_test.py,sha256=rDP7lcQZTFdQCqeaNYs7ZfwvGanzHz7CHM33NCmRIFk,94391
97
97
  pyglove/core/symbolic/origin.py,sha256=OSWMKjvPcISOXrzuX3lCQC8m_qaGl-9INsIB81erUnU,6124
98
98
  pyglove/core/symbolic/origin_test.py,sha256=dU_ZGrGDetM_lYVMn3wQO0d367_t_t8eESe3NrKPBNE,3159
99
99
  pyglove/core/symbolic/pure_symbolic.py,sha256=pvo15gn35_KLiGW_XrTjlx5ddmHbwpLr93VgbQ59uQ8,3231
100
- pyglove/core/symbolic/ref.py,sha256=qRN-0xm9pWlszel5qE-HAhQCkgB0umi_pDlzDhBWPUA,8513
101
- pyglove/core/symbolic/ref_test.py,sha256=0687hClfR5G5_VKuRlwjJGVQ2MC74ADFWklDaZ3aEVI,6294
100
+ pyglove/core/symbolic/ref.py,sha256=gIu02b8BfKspH1XejXhEFh_Iil3jvfGHdpaCRq6qor0,8817
101
+ pyglove/core/symbolic/ref_test.py,sha256=-rCA1AaLZnyuKOh0cJzS5UaQ_9Kp4p7xexZ_e3IwpOg,8974
102
102
  pyglove/core/symbolic/symbolize.py,sha256=ohID9-V8QiFe7OMpPlRomiqUnKBVMpypd8ZuMuHaa4s,6582
103
103
  pyglove/core/symbolic/symbolize_test.py,sha256=o7bRfMhGc6uw2FIH8arE99-bPb3i0YixcHYyiP-QqeQ,6487
104
104
  pyglove/core/tuning/__init__.py,sha256=JtXpjsBto01fLf55hZ1dSx-CEZUyVQeyRP9AMH_hw8c,2229
@@ -113,8 +113,8 @@ pyglove/core/tuning/sample_test.py,sha256=JqwDPy3EPC_VjU9dipk90jj1kovZB3Zb9hAjAl
113
113
  pyglove/core/typing/__init__.py,sha256=u2YSrSi8diTkQn8_1J2hEpk5o7zDhx2tU_oRuS-k1XU,14580
114
114
  pyglove/core/typing/annotated.py,sha256=llaajIDj9GK-4kUGJoO4JsHU6ESPOra2SZ-jG6xmsOQ,3203
115
115
  pyglove/core/typing/annotated_test.py,sha256=p1qid3R-jeiOTTxOVq6hXW8XFvn-h1cUzJWISPst2l8,2484
116
- pyglove/core/typing/annotation_conversion.py,sha256=txUrChAhMNeaukV-PSQEA9BCjtonUQDWFHxnpTE0_K8,15582
117
- pyglove/core/typing/annotation_conversion_test.py,sha256=GW7e3e00jcYJjJGRvCvMtW4QBHyVwFtZVcETdh1yBS8,17540
116
+ pyglove/core/typing/annotation_conversion.py,sha256=hi4LcRXwYzspHAKZ0rtXlq2dQyAmo9GXrm_HCJAGl34,15657
117
+ pyglove/core/typing/annotation_conversion_test.py,sha256=g66iyt3ti_pskIXndk4DiKgO8j8aMYOe2nPKXszlXmw,17675
118
118
  pyglove/core/typing/annotation_future_test.py,sha256=tAVuzWNfW8R4e4l7fx88Q4nJDM2LPUogNKNAIIPAEWQ,3959
119
119
  pyglove/core/typing/callable_ext.py,sha256=PiBQWPeUAH7Lgmf2xKCZqgK7N0OSrTdbnEkV8Ph31OA,9127
120
120
  pyglove/core/typing/callable_ext_test.py,sha256=TnWKU4_ZjvpbHZFtFHgFvCMDiCos8VmLlODcM_7Xg8M,10156
@@ -136,7 +136,7 @@ pyglove/core/typing/typed_missing.py,sha256=-l1omAu0jBZv5BnsFYXBqfvQwVBnmPh_X1wc
136
136
  pyglove/core/typing/typed_missing_test.py,sha256=TCNsb1SRpFaVdxYn2mB_yaLuja8w5Qn5NP7uGiZVBWs,2301
137
137
  pyglove/core/typing/value_specs.py,sha256=8E83QDZMb3lMXhgzfVNt9u6Bg3NPkvpjLXetjkps8UU,103263
138
138
  pyglove/core/typing/value_specs_test.py,sha256=eGXVxdduIM-oEaapJS9Kh7WSQHRUFegLIJ1GEzQkKHA,131017
139
- pyglove/core/utils/__init__.py,sha256=2aw4n1kYG9xlX2tWI-H5i25cBuK1ME9Lmf-F31VlKEk,8657
139
+ pyglove/core/utils/__init__.py,sha256=6P2VcGkjDsOFG640Jqu-jd1K3pfAK5NkcK3NBPLI6RY,8726
140
140
  pyglove/core/utils/common_traits.py,sha256=PWxOgPhG5H60ZwfO8xNAEGRjFUqqDZQBWQYomOfvdy8,3640
141
141
  pyglove/core/utils/common_traits_test.py,sha256=DIuZB_1xfmeTVfWnGOguDQcDAM_iGgBOe8C-5CsIqBc,1122
142
142
  pyglove/core/utils/contextual.py,sha256=_EO_ubCcmI81QyYyyucm3QcH1asQWSeXvT2Xa4KONs8,5355
@@ -149,8 +149,8 @@ pyglove/core/utils/formatting.py,sha256=Wn4d933LQLhuMIfjdRJgpxOThCxBxQrkRBa6Z1-h
149
149
  pyglove/core/utils/formatting_test.py,sha256=hhg-nL6DyE5A2QA92ALHK5QtfAYKfPpTbBARF-IT1j0,14241
150
150
  pyglove/core/utils/hierarchical.py,sha256=jwB-0FhqOspAymAkvJphRhPTQEsoShmKupCZpU3Vip4,19690
151
151
  pyglove/core/utils/hierarchical_test.py,sha256=f382DMJPa_bavJGGQDjuw-hWcafUg5bkQCPX-nbzeiI,21077
152
- pyglove/core/utils/json_conversion.py,sha256=O0Wz94WE3UkjQLFYqiuF3Zw9Q9vfMAaBPF2soREOaYs,26811
153
- pyglove/core/utils/json_conversion_test.py,sha256=zA_cy7ixVL3sTf6i9BCXMlSH56Aa3JnjHnjyqYJ_9XU,11845
152
+ pyglove/core/utils/json_conversion.py,sha256=V7wUviGKhEPWi1gtCGSDXXvBzV6cO7CvmEiGDZ2jWFY,34643
153
+ pyglove/core/utils/json_conversion_test.py,sha256=6afuPJxBXIO-OxFzJBkv1MZqNX-44HIBD6YJjwxXSbs,13412
154
154
  pyglove/core/utils/missing.py,sha256=9gslt1lXd1qSEIuAFxUWu30oD-YdYcnm13eau1S9uqY,1445
155
155
  pyglove/core/utils/missing_test.py,sha256=D6-FuVEwCyJemUiPLcwLmwyptqI5Bx0Pfipc2juhKSE,1335
156
156
  pyglove/core/utils/text_color.py,sha256=xcCTCxY2qFNZs_jismMGus8scEXKBpYGAhpAgnz-MHk,4112
@@ -218,8 +218,8 @@ pyglove/ext/scalars/randoms.py,sha256=LkMIIx7lOq_lvJvVS3BrgWGuWl7Pi91-lA-O8x_gZs
218
218
  pyglove/ext/scalars/randoms_test.py,sha256=nEhiqarg8l_5EOucp59CYrpO2uKxS1pe0hmBdZUzRNM,2000
219
219
  pyglove/ext/scalars/step_wise.py,sha256=IDw3tuTpv0KVh7AN44W43zqm1-E0HWPUlytWOQC9w3Y,3789
220
220
  pyglove/ext/scalars/step_wise_test.py,sha256=TL1vJ19xVx2t5HKuyIzGoogF7N3Rm8YhLE6JF7i0iy8,2540
221
- pyglove-0.5.0.dev202510220812.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
222
- pyglove-0.5.0.dev202510220812.dist-info/METADATA,sha256=hzf1JSlaIMNh-sWfJ1pQpYypOgDnkvYXk9Ek0LfYWgo,7089
223
- pyglove-0.5.0.dev202510220812.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
224
- pyglove-0.5.0.dev202510220812.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
225
- pyglove-0.5.0.dev202510220812.dist-info/RECORD,,
221
+ pyglove-0.5.0.dev202510230811.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
222
+ pyglove-0.5.0.dev202510230811.dist-info/METADATA,sha256=ZkvMwTAwqLF4U3t0Pan_ZSsTEI_NR_F4I10F5YbrXOA,7089
223
+ pyglove-0.5.0.dev202510230811.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
224
+ pyglove-0.5.0.dev202510230811.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
225
+ pyglove-0.5.0.dev202510230811.dist-info/RECORD,,