pyglove 0.5.0.dev202508250811__py3-none-any.whl → 0.5.0.dev202511300809__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.
Files changed (44) hide show
  1. pyglove/core/__init__.py +8 -1
  2. pyglove/core/geno/base.py +7 -3
  3. pyglove/core/io/file_system.py +295 -2
  4. pyglove/core/io/file_system_test.py +291 -0
  5. pyglove/core/logging.py +45 -1
  6. pyglove/core/logging_test.py +12 -21
  7. pyglove/core/monitoring.py +657 -0
  8. pyglove/core/monitoring_test.py +289 -0
  9. pyglove/core/symbolic/__init__.py +7 -0
  10. pyglove/core/symbolic/base.py +89 -35
  11. pyglove/core/symbolic/base_test.py +3 -3
  12. pyglove/core/symbolic/dict.py +31 -12
  13. pyglove/core/symbolic/dict_test.py +49 -0
  14. pyglove/core/symbolic/list.py +17 -3
  15. pyglove/core/symbolic/list_test.py +24 -2
  16. pyglove/core/symbolic/object.py +3 -1
  17. pyglove/core/symbolic/object_test.py +13 -10
  18. pyglove/core/symbolic/ref.py +19 -7
  19. pyglove/core/symbolic/ref_test.py +94 -7
  20. pyglove/core/symbolic/unknown_symbols.py +147 -0
  21. pyglove/core/symbolic/unknown_symbols_test.py +100 -0
  22. pyglove/core/typing/annotation_conversion.py +8 -1
  23. pyglove/core/typing/annotation_conversion_test.py +14 -19
  24. pyglove/core/typing/class_schema.py +24 -1
  25. pyglove/core/typing/json_schema.py +221 -8
  26. pyglove/core/typing/json_schema_test.py +508 -12
  27. pyglove/core/typing/type_conversion.py +17 -3
  28. pyglove/core/typing/type_conversion_test.py +7 -2
  29. pyglove/core/typing/value_specs.py +5 -1
  30. pyglove/core/typing/value_specs_test.py +5 -0
  31. pyglove/core/utils/__init__.py +2 -0
  32. pyglove/core/utils/contextual.py +9 -4
  33. pyglove/core/utils/contextual_test.py +10 -0
  34. pyglove/core/utils/error_utils.py +59 -25
  35. pyglove/core/utils/json_conversion.py +360 -63
  36. pyglove/core/utils/json_conversion_test.py +146 -13
  37. pyglove/core/views/html/controls/tab.py +33 -0
  38. pyglove/core/views/html/controls/tab_test.py +37 -0
  39. pyglove/ext/evolution/base_test.py +1 -1
  40. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/METADATA +8 -1
  41. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/RECORD +44 -40
  42. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/WHEEL +0 -0
  43. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/licenses/LICENSE +0 -0
  44. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/top_level.txt +0 -0
@@ -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.')
@@ -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(
@@ -0,0 +1,147 @@
1
+ # Copyright 2021 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Symbolic types for reprenting unknown types and objects."""
15
+
16
+ from typing import Annotated, Any, ClassVar, Literal
17
+ from pyglove.core import typing as pg_typing
18
+ from pyglove.core import utils
19
+ from pyglove.core.symbolic import list as pg_list # pylint: disable=unused-import
20
+ from pyglove.core.symbolic import object as pg_object
21
+
22
+
23
+ class UnknownSymbol(pg_object.Object, pg_typing.CustomTyping):
24
+ """Interface for symbolic representation of unknown symbols."""
25
+ auto_register = False
26
+
27
+ def custom_apply(self, *args, **kwargs) -> tuple[bool, Any]:
28
+ """Bypass PyGlove type check."""
29
+ return (False, self)
30
+
31
+
32
+ class UnknownType(UnknownSymbol):
33
+ """Symbolic object for representing unknown types."""
34
+
35
+ auto_register = True
36
+ __serialization_key__ = 'unknown_type'
37
+
38
+ # TODO(daiyip): Revisit the design on how `pg.typing.Object()` handles
39
+ # UnknownType. This hacky solution should be removed in the future.
40
+ __no_type_check__ = True
41
+
42
+ name: str
43
+ args: list[Any] = []
44
+
45
+ def sym_jsonify(self, **kwargs) -> utils.JSONValueType:
46
+ json_dict = {'_type': 'type', 'name': self.name}
47
+ if self.args:
48
+ json_dict['args'] = utils.to_json(self.args, **kwargs)
49
+ return json_dict
50
+
51
+ def format(
52
+ self,
53
+ compact: bool = False,
54
+ verbose: bool = True,
55
+ root_indent: int = 0,
56
+ **kwargs
57
+ ) -> str:
58
+ s = f'<unknown-type {self.name}>'
59
+ if self.args:
60
+ s += f'[{", ".join(repr(x) for x in self.args)}]'
61
+ return s
62
+
63
+ def __call__(self, **kwargs):
64
+ return UnknownTypedObject(
65
+ type_name=self.name, **kwargs
66
+ )
67
+
68
+
69
+ class UnknownCallable(UnknownSymbol):
70
+ """Symbolic object for representing unknown callables."""
71
+
72
+ auto_register = False
73
+ name: str
74
+ CALLABLE_TYPE: ClassVar[Literal['function', 'method']]
75
+
76
+ def sym_jsonify(self, **kwargs) -> utils.JSONValueType:
77
+ return {'_type': self.CALLABLE_TYPE, 'name': self.name}
78
+
79
+ def format(
80
+ self,
81
+ compact: bool = False,
82
+ verbose: bool = True,
83
+ root_indent: int = 0,
84
+ **kwargs
85
+ ) -> str:
86
+ return f'<unknown-{self.CALLABLE_TYPE} {self.name}>'
87
+
88
+
89
+ class UnknownFunction(UnknownCallable):
90
+ """Symbolic objject for representing unknown functions."""
91
+
92
+ auto_register = True
93
+ __serialization_key__ = 'unknown_function'
94
+ CALLABLE_TYPE = 'function'
95
+
96
+
97
+ class UnknownMethod(UnknownCallable):
98
+ """Symbolic object for representing unknown methods."""
99
+
100
+ auto_register = True
101
+ __serialization_key__ = 'unknown_method'
102
+ CALLABLE_TYPE = 'method'
103
+
104
+
105
+ class UnknownTypedObject(UnknownSymbol):
106
+ """Symbolic object for representing objects of unknown-type."""
107
+
108
+ auto_register = True
109
+ __serialization_key__ = 'unknown_object'
110
+
111
+ type_name: str
112
+ __kwargs__: Annotated[
113
+ Any,
114
+ (
115
+ 'Fields of the original object will be kept as symbolic attributes '
116
+ 'of this object so they can be accessed through `__getattr__`.'
117
+ )
118
+ ]
119
+
120
+ def sym_jsonify(self, **kwargs) -> utils.JSONValueType:
121
+ """Converts current object to a dict of plain Python objects."""
122
+ json_dict = self._sym_attributes.to_json(
123
+ exclude_keys=set(['type_name']), **kwargs
124
+ )
125
+ assert isinstance(json_dict, dict)
126
+ json_dict[utils.JSONConvertible.TYPE_NAME_KEY] = self.type_name
127
+ return json_dict
128
+
129
+ def format(
130
+ self,
131
+ compact: bool = False,
132
+ verbose: bool = True,
133
+ root_indent: int = 0,
134
+ **kwargs
135
+ ) -> str:
136
+ exclude_keys = kwargs.pop('exclude_keys', set())
137
+ exclude_keys.add('type_name')
138
+ kwargs['exclude_keys'] = exclude_keys
139
+ return self._sym_attributes.format(
140
+ compact,
141
+ verbose,
142
+ root_indent,
143
+ cls_name=f'<unknown-type {self.type_name}>',
144
+ key_as_attribute=True,
145
+ bracket_type=utils.BracketType.ROUND,
146
+ **kwargs,
147
+ )
@@ -0,0 +1,100 @@
1
+ # Copyright 2025 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import unittest
16
+ from pyglove.core import utils
17
+ from pyglove.core.symbolic import unknown_symbols
18
+
19
+
20
+ class UnknownTypeTest(unittest.TestCase):
21
+
22
+ def test_basics(self):
23
+ t = unknown_symbols.UnknownType(name='__main__.ABC', args=[int, str])
24
+ self.assertEqual(t.name, '__main__.ABC')
25
+ self.assertEqual(t.args, [int, str])
26
+ self.assertEqual(
27
+ repr(t),
28
+ '<unknown-type __main__.ABC>[<class \'int\'>, <class \'str\'>]'
29
+ )
30
+ self.assertEqual(
31
+ t.to_json(),
32
+ {
33
+ '_type': 'type',
34
+ 'name': '__main__.ABC',
35
+ 'args': [
36
+ {'_type': 'type', 'name': 'builtins.int'},
37
+ {'_type': 'type', 'name': 'builtins.str'},
38
+ ]
39
+ }
40
+ )
41
+ self.assertEqual(utils.from_json(t.to_json(), convert_unknown=True), t)
42
+ self.assertEqual(
43
+ t(x=1, y=2),
44
+ unknown_symbols.UnknownTypedObject(type_name='__main__.ABC', x=1, y=2)
45
+ )
46
+
47
+
48
+ class UnknownFunctionTest(unittest.TestCase):
49
+
50
+ def test_basics(self):
51
+ t = unknown_symbols.UnknownFunction(name='__main__.foo')
52
+ self.assertEqual(t.name, '__main__.foo')
53
+ self.assertEqual(repr(t), '<unknown-function __main__.foo>')
54
+ self.assertEqual(
55
+ t.to_json(),
56
+ {
57
+ '_type': 'function',
58
+ 'name': '__main__.foo',
59
+ }
60
+ )
61
+ self.assertEqual(utils.from_json(t.to_json(), convert_unknown=True), t)
62
+
63
+
64
+ class UnknownMethodTest(unittest.TestCase):
65
+
66
+ def test_basics(self):
67
+ t = unknown_symbols.UnknownMethod(name='__main__.ABC.bar')
68
+ self.assertEqual(t.name, '__main__.ABC.bar')
69
+ self.assertEqual(repr(t), '<unknown-method __main__.ABC.bar>')
70
+ self.assertEqual(
71
+ t.to_json(),
72
+ {
73
+ '_type': 'method',
74
+ 'name': '__main__.ABC.bar',
75
+ }
76
+ )
77
+ self.assertEqual(utils.from_json(t.to_json(), convert_unknown=True), t)
78
+
79
+
80
+ class UnknownObjectTest(unittest.TestCase):
81
+
82
+ def test_basics(self):
83
+ v = unknown_symbols.UnknownTypedObject(type_name='__main__.ABC', x=1)
84
+ self.assertEqual(v.type_name, '__main__.ABC')
85
+ self.assertEqual(v.x, 1)
86
+ self.assertEqual(repr(v), '<unknown-type __main__.ABC>(x=1)')
87
+ self.assertEqual(
88
+ str(v), '<unknown-type __main__.ABC>(\n x = 1\n)')
89
+ self.assertEqual(
90
+ v.to_json(),
91
+ {
92
+ '_type': '__main__.ABC',
93
+ 'x': 1,
94
+ }
95
+ )
96
+ self.assertEqual(utils.from_json(v.to_json(), convert_unknown=True), v)
97
+
98
+
99
+ if __name__ == '__main__':
100
+ unittest.main()
@@ -16,6 +16,7 @@
16
16
  import builtins
17
17
  import collections
18
18
  import inspect
19
+ import sys
19
20
  import types
20
21
  import typing
21
22
 
@@ -197,6 +198,8 @@ def annotation_from_str(
197
198
  def _resolve(type_id: str):
198
199
 
199
200
  def _as_forward_ref() -> typing.ForwardRef:
201
+ if sys.version_info >= (3, 14):
202
+ return typing.ForwardRef(type_id) # pytype: disable=not-callable
200
203
  return typing.ForwardRef(type_id, False, parent_module) # pytype: disable=not-callable
201
204
 
202
205
  def _resolve_name(name: str, parent_obj: typing.Any):
@@ -318,6 +321,9 @@ def _value_spec_from_type_annotation(
318
321
  parent_module: typing.Optional[types.ModuleType] = None
319
322
  ) -> class_schema.ValueSpec:
320
323
  """Creates a value spec from type annotation."""
324
+ if isinstance(annotation, class_schema.ValueSpec):
325
+ return annotation
326
+
321
327
  if isinstance(annotation, str) and not accept_value_as_annotation:
322
328
  annotation = annotation_from_str(annotation, parent_module)
323
329
 
@@ -454,7 +460,8 @@ def _value_spec_from_annotation(
454
460
  """Creates a value spec from annotation."""
455
461
  if isinstance(annotation, class_schema.ValueSpec):
456
462
  return annotation
457
- elif annotation == inspect.Parameter.empty:
463
+
464
+ if annotation == inspect.Parameter.empty:
458
465
  return vs.Any()
459
466
 
460
467
  if annotation is None: