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
@@ -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:
@@ -34,6 +34,12 @@ class Foo:
34
34
  _MODULE = sys.modules[__name__]
35
35
 
36
36
 
37
+ def typing_forward_ref(name: str) -> typing.ForwardRef:
38
+ if sys.version_info >= (3, 14):
39
+ return typing.ForwardRef(name)
40
+ return typing.ForwardRef(name, False, _MODULE)
41
+
42
+
37
43
  class AnnotationFromStrTest(unittest.TestCase):
38
44
  """Tests for annotation_from_str."""
39
45
 
@@ -69,7 +75,7 @@ class AnnotationFromStrTest(unittest.TestCase):
69
75
  )
70
76
  self.assertEqual(
71
77
  annotation_conversion.annotation_from_str('list[Foo.Baz]', _MODULE),
72
- list[typing.ForwardRef('Foo.Baz', False, _MODULE)]
78
+ list[typing_forward_ref('Foo.Baz')]
73
79
  )
74
80
 
75
81
  def test_generic_types(self):
@@ -138,18 +144,12 @@ class AnnotationFromStrTest(unittest.TestCase):
138
144
  self.assertEqual(
139
145
  annotation_conversion.annotation_from_str(
140
146
  'AAA', _MODULE),
141
- typing.ForwardRef(
142
- 'AAA', False, _MODULE
143
- )
147
+ typing_forward_ref('AAA')
144
148
  )
145
149
  self.assertEqual(
146
150
  annotation_conversion.annotation_from_str(
147
151
  'typing.List[AAA]', _MODULE),
148
- typing.List[
149
- typing.ForwardRef(
150
- 'AAA', False, _MODULE
151
- )
152
- ]
152
+ typing.List[typing_forward_ref('AAA')]
153
153
  )
154
154
 
155
155
  def test_reloading(self):
@@ -157,20 +157,12 @@ class AnnotationFromStrTest(unittest.TestCase):
157
157
  self.assertEqual(
158
158
  annotation_conversion.annotation_from_str(
159
159
  'typing.List[Foo]', _MODULE),
160
- typing.List[
161
- typing.ForwardRef(
162
- 'Foo', False, _MODULE
163
- )
164
- ]
160
+ typing.List[typing_forward_ref('Foo')]
165
161
  )
166
162
  self.assertEqual(
167
163
  annotation_conversion.annotation_from_str(
168
164
  'typing.List[Foo.Bar]', _MODULE),
169
- typing.List[
170
- typing.ForwardRef(
171
- 'Foo.Bar', False, _MODULE
172
- )
173
- ]
165
+ typing.List[typing_forward_ref('Foo.Bar')]
174
166
  )
175
167
  delattr(_MODULE, '__reloading__')
176
168
 
@@ -396,6 +388,9 @@ class ValueSpecFromAnnotationTest(unittest.TestCase):
396
388
  self.assertEqual(
397
389
  ValueSpec.from_annotation(typing.Dict[str, int], True),
398
390
  vs.Dict([(ks.StrKey(), vs.Int())]))
391
+ self.assertEqual(
392
+ ValueSpec.from_annotation(typing.Dict[str, vs.Int()], True),
393
+ vs.Dict([(ks.StrKey(), vs.Int())]))
399
394
  self.assertEqual(
400
395
  ValueSpec.from_annotation(typing.Mapping[str, int], True),
401
396
  vs.Dict([(ks.StrKey(), vs.Int())]))
@@ -582,6 +582,16 @@ class ValueSpec(utils.Formattable, utils.JSONConvertible):
582
582
  del include_type_name, include_subclasses, inline_nested_refs, kwargs
583
583
  assert False, 'Overridden in `json_schema.py`.'
584
584
 
585
+ @classmethod
586
+ def from_json_schema(
587
+ cls,
588
+ json_schema: Dict[str, Any],
589
+ class_fn: Optional[Callable[[str, 'Schema'], Type[Any]]] = None
590
+ ) -> 'ValueSpec':
591
+ """Converts a JSON schema to a value spec."""
592
+ del json_schema, class_fn
593
+ assert False, 'Overridden in `json_schema.py`.'
594
+
585
595
 
586
596
  class Field(utils.Formattable, utils.JSONConvertible):
587
597
  """Class that represents the definition of one or a group of attributes.
@@ -1219,6 +1229,9 @@ class Schema(utils.Formattable, utils.JSONConvertible):
1219
1229
  f'(parent=\'{root_path}\')'
1220
1230
  )
1221
1231
 
1232
+ # Symbolic.Dict uses `sym_getattr` to support getting symbolic attributes.
1233
+ get_value = getattr(dict_obj, 'sym_getattr', dict_obj.get)
1234
+
1222
1235
  for key_spec, keys in matched_keys.items():
1223
1236
  field = self._fields[key_spec]
1224
1237
  # For missing const keys, we add to keys collection to add missing value.
@@ -1226,7 +1239,7 @@ class Schema(utils.Formattable, utils.JSONConvertible):
1226
1239
  keys.append(str(key_spec))
1227
1240
  for key in keys:
1228
1241
  if dict_obj:
1229
- value = dict_obj.get(key, utils.MISSING_VALUE)
1242
+ value = get_value(key, utils.MISSING_VALUE)
1230
1243
  else:
1231
1244
  value = utils.MISSING_VALUE
1232
1245
  # NOTE(daiyip): field.default_value may be MISSING_VALUE too
@@ -1375,6 +1388,16 @@ class Schema(utils.Formattable, utils.JSONConvertible):
1375
1388
  def __ne__(self, other: Any) -> bool:
1376
1389
  return not self.__eq__(other)
1377
1390
 
1391
+ @classmethod
1392
+ def from_json_schema(
1393
+ cls,
1394
+ json_schema: Dict[str, Any],
1395
+ class_fn: Optional[Callable[[str, 'Schema'], Type[Any]]] = None
1396
+ ) -> 'Schema':
1397
+ """Converts a JSON schema to a schema."""
1398
+ del json_schema, class_fn
1399
+ assert False, 'Overridden in `json_schema.py`.'
1400
+
1378
1401
 
1379
1402
  FieldDef = Union[
1380
1403
  # Key, Value spec/annotation.