pyglove 0.4.5.dev202505010810__py3-none-any.whl → 0.4.5.dev202505030808__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.
@@ -394,6 +394,9 @@ from pyglove.core.typing.callable_ext import enable_preset_args
394
394
  from pyglove.core.typing.callable_ext import preset_args
395
395
  from pyglove.core.typing.callable_ext import CallableWithOptionalKeywordArgs
396
396
 
397
+ # JSON schema conversion.
398
+ from pyglove.core.typing.json_schema import to_json_schema
399
+
397
400
  # PyType support.
398
401
  from pyglove.core.typing.pytype_support import *
399
402
 
@@ -571,6 +571,17 @@ class ValueSpec(utils.Formattable, utils.JSONConvertible):
571
571
  del annotation, auto_typing, accept_value_as_annotation, parent_module
572
572
  assert False, 'Overridden in `annotation_conversion.py`.'
573
573
 
574
+ def to_json_schema(
575
+ self,
576
+ include_type_name: bool = True,
577
+ include_subclasses: bool = False,
578
+ inline_nested_refs: bool = False,
579
+ **kwargs
580
+ ) -> Dict[str, Any]:
581
+ """Converts this field to JSON schema."""
582
+ del include_type_name, include_subclasses, inline_nested_refs, kwargs
583
+ assert False, 'Overridden in `json_schema.py`.'
584
+
574
585
 
575
586
  class Field(utils.Formattable, utils.JSONConvertible):
576
587
  """Class that represents the definition of one or a group of attributes.
@@ -1344,6 +1355,18 @@ class Schema(utils.Formattable, utils.JSONConvertible):
1344
1355
  **kwargs,
1345
1356
  )
1346
1357
 
1358
+ def to_json_schema(
1359
+ self,
1360
+ *,
1361
+ include_type_name: bool = True,
1362
+ include_subclasses: bool = False,
1363
+ inline_nested_refs: bool = False,
1364
+ **kwargs
1365
+ ) -> Dict[str, Any]:
1366
+ """Converts this field to JSON schema."""
1367
+ del include_type_name, include_subclasses, inline_nested_refs, kwargs
1368
+ assert False, 'Overridden in `json_schema.py`.'
1369
+
1347
1370
  def __eq__(self, other: Any) -> bool:
1348
1371
  if self is other:
1349
1372
  return True
@@ -0,0 +1,340 @@
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
+ """PyGlove typing to JSON schema conversion."""
15
+
16
+ import dataclasses
17
+ import inspect
18
+ from typing import Any, Dict, List, Optional, Tuple, Union
19
+
20
+ from pyglove.core import utils
21
+ from pyglove.core.typing import callable_signature
22
+ from pyglove.core.typing import class_schema
23
+ from pyglove.core.typing import key_specs as ks
24
+ from pyglove.core.typing import value_specs as vs
25
+
26
+
27
+ @dataclasses.dataclass
28
+ class _DefEntry:
29
+ definition: Optional[Dict[str, Any]]
30
+ num_references: int = 0
31
+
32
+
33
+ def _json_schema_from_schema(
34
+ schema: class_schema.Schema,
35
+ *,
36
+ defs: Dict[str, _DefEntry],
37
+ type_name: Optional[str] = None,
38
+ include_type_name: bool = True,
39
+ include_subclasses: bool = False,
40
+ ) -> Dict[str, Any]:
41
+ """Converts a PyGlove schema to JSON schema."""
42
+ title = schema.name.split('.')[-1] if schema.name else None
43
+ entry = defs.get(title)
44
+ if entry is None:
45
+ # NOTE(daiyip): Add a forward reference to the entry so recursive
46
+ # references can be resolved.
47
+ entry = _DefEntry(definition=None, num_references=0)
48
+ if title:
49
+ defs[title] = entry
50
+
51
+ additional_properties = False
52
+ properties = {}
53
+ required = []
54
+
55
+ if type_name and include_type_name:
56
+ properties['_type'] = {'const': type_name}
57
+ required.append('_type')
58
+
59
+ for key, field in schema.items():
60
+ if isinstance(key, ks.ConstStrKey):
61
+ prop = _json_schema_from_value_spec(
62
+ field.value, defs=defs
63
+ )
64
+ if field.description:
65
+ prop['description'] = field.description
66
+ properties[key.text] = prop
67
+ if not field.value.has_default:
68
+ required.append(key.text)
69
+ else:
70
+ if isinstance(field.value, vs.Any):
71
+ additional_properties = True
72
+ else:
73
+ additional_properties = _json_schema_from_value_spec(
74
+ field.value, defs=defs,
75
+ include_type_name=include_type_name,
76
+ include_subclasses=include_subclasses,
77
+ )
78
+ entry.definition = _dict_with_optional_fields([
79
+ ('type', 'object', None),
80
+ ('title', title, None),
81
+ ('description', schema.description, None),
82
+ ('properties', properties, {}),
83
+ ('required', required, []),
84
+ ('additionalProperties', additional_properties, None),
85
+ ])
86
+
87
+ entry.num_references += 1
88
+ if title:
89
+ return {'$ref': f'#/$defs/{title}'}
90
+ else:
91
+ assert entry.definition is not None
92
+ return entry.definition # pytype: disable=bad-return-type
93
+
94
+
95
+ def _json_schema_from_value_spec(
96
+ value_spec: vs.ValueSpec,
97
+ *,
98
+ defs: Dict[str, _DefEntry],
99
+ include_type_name: bool = True,
100
+ include_subclasses: bool = False,
101
+ ignore_nonable: bool = False,
102
+ ) -> Dict[str, Any]:
103
+ """Converts a value spec to JSON schema."""
104
+ def _child_json_schema(v: vs.ValueSpec, ignore_nonable: bool = False):
105
+ return _json_schema_from_value_spec(
106
+ v, defs=defs,
107
+ include_type_name=include_type_name,
108
+ include_subclasses=include_subclasses,
109
+ ignore_nonable=ignore_nonable
110
+ )
111
+
112
+ if isinstance(value_spec, vs.Bool):
113
+ definition = {
114
+ 'type': 'boolean'
115
+ }
116
+ elif isinstance(value_spec, vs.Int):
117
+ definition = _dict_with_optional_fields([
118
+ ('type', 'integer', None),
119
+ ('minimum', value_spec.min_value, None),
120
+ ('maximum', value_spec.max_value, None),
121
+ ])
122
+ elif isinstance(value_spec, vs.Float):
123
+ definition = _dict_with_optional_fields([
124
+ ('type', 'number', None),
125
+ ('minimum', value_spec.min_value, None),
126
+ ('maximum', value_spec.max_value, None),
127
+ ])
128
+ elif isinstance(value_spec, vs.Str):
129
+ definition = _dict_with_optional_fields([
130
+ ('type', 'string', None),
131
+ ('pattern', getattr(value_spec.regex, 'pattern', None), None)
132
+ ])
133
+ elif isinstance(value_spec, vs.Enum):
134
+ for v in value_spec.values:
135
+ if not isinstance(v, (str, bool, int, float, type(None))):
136
+ raise ValueError(
137
+ f'Enum candidate {v!r} is not supported for JSON schema generation.'
138
+ )
139
+ definition = {
140
+ 'enum': [v for v in value_spec.values if v is not None],
141
+ }
142
+ elif isinstance(value_spec, vs.List):
143
+ definition = {
144
+ 'type': 'array',
145
+ }
146
+ if not isinstance(value_spec.element.value, vs.Any):
147
+ definition['items'] = _child_json_schema(value_spec.element.value)
148
+ return definition
149
+ elif isinstance(value_spec, vs.Dict):
150
+ if value_spec.schema is None:
151
+ definition = {
152
+ 'type': 'object',
153
+ 'additionalProperties': True
154
+ }
155
+ else:
156
+ definition = _json_schema_from_schema(
157
+ value_spec.schema, defs=defs, include_type_name=include_type_name
158
+ )
159
+ elif isinstance(value_spec, vs.Object):
160
+ def _json_schema_from_cls(cls: type[Any]):
161
+ schema = getattr(cls, '__schema__', None)
162
+ if schema is None:
163
+ schema = callable_signature.signature(cls).to_schema()
164
+ return _json_schema_from_schema(
165
+ schema,
166
+ defs=defs,
167
+ type_name=getattr(cls, '__type_name__', None),
168
+ include_type_name=include_type_name,
169
+ include_subclasses=include_subclasses,
170
+ )
171
+ definitions = [
172
+ _json_schema_from_cls(value_spec.cls)
173
+ ]
174
+
175
+ if include_subclasses:
176
+ for subclass in value_spec.cls.__subclasses__():
177
+ if not inspect.isabstract(subclass):
178
+ definitions.append(_json_schema_from_cls(subclass))
179
+
180
+ if len(definitions) == 1:
181
+ definition = definitions[0]
182
+ else:
183
+ definition = {'anyOf': _normalize_anyofs(definitions)}
184
+ elif isinstance(value_spec, vs.Union):
185
+ definition = {
186
+ 'anyOf': _normalize_anyofs([
187
+ _child_json_schema(v, ignore_nonable=True)
188
+ for v in value_spec.candidates
189
+ ])
190
+ }
191
+ elif isinstance(value_spec, vs.Any):
192
+ # Consider using return {}
193
+ definition = {
194
+ 'anyOf': [
195
+ _child_json_schema(v) for v in [
196
+ vs.Bool(),
197
+ vs.Float(),
198
+ vs.Str(),
199
+ vs.List(vs.Any()),
200
+ vs.Dict(),
201
+ ]
202
+ ]
203
+ }
204
+ else:
205
+ raise TypeError(
206
+ f'Value spec {value_spec!r} cannot be converted to JSON schema.'
207
+ )
208
+
209
+ if not ignore_nonable and value_spec.is_noneable:
210
+ nullable = {'type': 'null'}
211
+ if 'anyOf' in definition:
212
+ definition['anyOf'].append(nullable)
213
+ else:
214
+ definition = {'anyOf': [definition, nullable]}
215
+ return definition
216
+
217
+
218
+ def _normalize_anyofs(candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
219
+ flattened = []
220
+ for candidate in candidates:
221
+ if 'anyOf' in candidate:
222
+ flattened.extend(_normalize_anyofs(candidate['anyOf']))
223
+ else:
224
+ flattened.append(candidate)
225
+ return flattened
226
+
227
+
228
+ def _dict_with_optional_fields(
229
+ pairs: List[Tuple[str, Any, Any]]
230
+ ) -> Dict[str, Any]:
231
+ return {
232
+ k: v for k, v, default in pairs if v != default
233
+ }
234
+
235
+
236
+ def to_json_schema(
237
+ value: Union[vs.ValueSpec, class_schema.Schema, Any],
238
+ *,
239
+ include_type_name: bool = True,
240
+ include_subclasses: bool = False,
241
+ inline_nested_refs: bool = False,
242
+ ) -> Dict[str, Any]:
243
+ """Converts a value spec to JSON schema."""
244
+ defs = dict()
245
+ if isinstance(value, class_schema.Schema):
246
+ root = _json_schema_from_schema(
247
+ value, defs=defs,
248
+ type_name=value.name,
249
+ include_type_name=include_type_name,
250
+ include_subclasses=include_subclasses,
251
+ )
252
+ else:
253
+ value = vs.ValueSpec.from_annotation(value, auto_typing=True)
254
+ root = _json_schema_from_value_spec(
255
+ value,
256
+ defs=defs,
257
+ include_type_name=include_type_name,
258
+ include_subclasses=include_subclasses,
259
+ )
260
+ return _canonicalize_schema(root, defs, inline_nested_refs=inline_nested_refs)
261
+
262
+
263
+ def _canonicalize_schema(
264
+ root: Dict[str, Any],
265
+ defs: Dict[str, _DefEntry],
266
+ *,
267
+ inline_nested_refs: bool = True,
268
+ include_defs: bool = True,
269
+ ) -> Dict[str, Any]:
270
+ """Canonicalizes a JSON schema."""
271
+ if not defs:
272
+ return root
273
+
274
+ def _maybe_inline_ref(k: utils.KeyPath, v: Any):
275
+ del k
276
+ if isinstance(v, dict) and '$ref' in v:
277
+ ref_key = v['$ref'].split('/')[-1]
278
+ if defs[ref_key].num_references == 1:
279
+ defs[ref_key].num_references -= 1
280
+ if not inline_nested_refs:
281
+ return defs[ref_key].definition
282
+ else:
283
+ return _canonicalize_schema(
284
+ defs[ref_key].definition, defs=defs, include_defs=False,
285
+ inline_nested_refs=True
286
+ )
287
+ return v
288
+
289
+ new_root = utils.transform(root, _maybe_inline_ref)
290
+ if not include_defs:
291
+ return new_root
292
+ referenced_defs = {
293
+ k: v.definition for k, v in defs.items() if v.num_references > 0
294
+ }
295
+ canonical_form = {'$defs': referenced_defs} if referenced_defs else {}
296
+ canonical_form.update(new_root)
297
+ return canonical_form
298
+
299
+ #
300
+ # Override to_json_schema() method for ValueSpec and Schema.
301
+ #
302
+
303
+
304
+ def _value_spec_to_json_schema(
305
+ self: Union[vs.ValueSpec, class_schema.Schema, Any],
306
+ *,
307
+ include_type_name: bool = True,
308
+ include_subclasses: bool = False,
309
+ inline_nested_refs: bool = False,
310
+ **kwargs
311
+ ) -> Dict[str, Any]:
312
+ """Converts a value spec to JSON schema."""
313
+ return to_json_schema(
314
+ self,
315
+ include_type_name=include_type_name,
316
+ include_subclasses=include_subclasses,
317
+ inline_nested_refs=inline_nested_refs,
318
+ **kwargs
319
+ )
320
+
321
+
322
+ def _schema_to_json_schema(
323
+ self: class_schema.Schema,
324
+ *,
325
+ include_type_name: bool = True,
326
+ include_subclasses: bool = False,
327
+ inline_nested_refs: bool = False,
328
+ **kwargs
329
+ ) -> Dict[str, Any]:
330
+ """Converts a schema to JSON schema."""
331
+ return to_json_schema(
332
+ self,
333
+ include_type_name=include_type_name,
334
+ include_subclasses=include_subclasses,
335
+ inline_nested_refs=inline_nested_refs,
336
+ **kwargs
337
+ )
338
+
339
+ vs.ValueSpec.to_json_schema = _value_spec_to_json_schema
340
+ class_schema.Schema.to_json_schema = _schema_to_json_schema
@@ -0,0 +1,477 @@
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
+ """Tests for JSON schema conversion."""
15
+
16
+ from typing import Annotated, Literal, Optional
17
+ import unittest
18
+ from pyglove.core.symbolic import object as pg_object
19
+ from pyglove.core.typing import annotation_conversion # pylint: disable=unused-import
20
+ from pyglove.core.typing import json_schema
21
+ from pyglove.core.typing import key_specs as ks
22
+ from pyglove.core.typing import value_specs as vs
23
+
24
+
25
+ class Foo(pg_object.Object):
26
+ x: int
27
+ y: 'Bar'
28
+
29
+
30
+ class Bar(pg_object.Object):
31
+ z: Optional[Foo]
32
+
33
+
34
+ class JsonSchemaTest(unittest.TestCase):
35
+
36
+ maxDiff = None
37
+
38
+ def assert_json_schema(
39
+ self, t, expected_json_schema,
40
+ *,
41
+ include_type_name: bool = True,
42
+ include_subclasses: bool = False,
43
+ inline_nested_refs: bool = False):
44
+ actual_schema = json_schema.to_json_schema(
45
+ t,
46
+ include_type_name=include_type_name,
47
+ include_subclasses=include_subclasses,
48
+ inline_nested_refs=inline_nested_refs,
49
+ )
50
+ self.assertEqual(actual_schema, expected_json_schema)
51
+
52
+ def test_bool(self):
53
+ self.assert_json_schema(bool, {
54
+ 'type': 'boolean',
55
+ })
56
+
57
+ def test_int(self):
58
+ self.assert_json_schema(int, {
59
+ 'type': 'integer',
60
+ })
61
+ self.assert_json_schema(vs.Int(min_value=0), {
62
+ 'type': 'integer',
63
+ 'minimum': 0,
64
+ })
65
+ self.assert_json_schema(vs.Int(max_value=1), {
66
+ 'type': 'integer',
67
+ 'maximum': 1,
68
+ })
69
+
70
+ def test_float(self):
71
+ self.assert_json_schema(float, {
72
+ 'type': 'number',
73
+ })
74
+ self.assert_json_schema(vs.Float(min_value=0.0), {
75
+ 'type': 'number',
76
+ 'minimum': 0.0,
77
+ })
78
+ self.assert_json_schema(vs.Float(max_value=1.0), {
79
+ 'type': 'number',
80
+ 'maximum': 1.0,
81
+ })
82
+
83
+ def test_str(self):
84
+ self.assert_json_schema(str, {
85
+ 'type': 'string',
86
+ })
87
+ self.assert_json_schema(vs.Str(regex='a.*'), {
88
+ 'type': 'string',
89
+ 'pattern': 'a.*',
90
+ })
91
+
92
+ def test_enum(self):
93
+ self.assert_json_schema(Literal['a', 1], {
94
+ 'enum': ['a', 1]
95
+ })
96
+ self.assert_json_schema(Literal['a', 1, None], {
97
+ 'anyOf': [
98
+ {'enum': ['a', 1]},
99
+ {'type': 'null'}
100
+ ]
101
+ })
102
+ with self.assertRaisesRegex(
103
+ ValueError, 'Enum candidate .* is not supported'
104
+ ):
105
+ json_schema.to_json_schema(Literal['a', 1, None, ValueError()])
106
+
107
+ def test_list(self):
108
+ self.assert_json_schema(vs.List[int], {
109
+ 'type': 'array',
110
+ 'items': {
111
+ 'type': 'integer',
112
+ }
113
+ })
114
+
115
+ def test_dict(self):
116
+ self.assert_json_schema(vs.Dict(), {
117
+ 'type': 'object', 'additionalProperties': True
118
+ })
119
+ self.assert_json_schema(vs.Dict({'a': int}), {
120
+ 'type': 'object',
121
+ 'properties': {'a': {
122
+ 'type': 'integer',
123
+ }},
124
+ 'required': ['a'],
125
+ 'additionalProperties': False,
126
+ })
127
+ self.assert_json_schema(vs.Dict([(ks.StrKey(), int)]), {
128
+ 'type': 'object',
129
+ 'additionalProperties': {'type': 'integer'},
130
+ })
131
+ self.assert_json_schema(vs.Dict([(ks.StrKey(), vs.Any())]), {
132
+ 'type': 'object',
133
+ 'additionalProperties': True,
134
+ })
135
+
136
+ def test_union(self):
137
+ self.assert_json_schema(vs.Union([int, vs.Union([str, int]).noneable()]), {
138
+ 'anyOf': [
139
+ {'type': 'integer'},
140
+ {'type': 'string'},
141
+ # TODO(daiyip): Remove duplicates for nested Union in future.
142
+ {'type': 'integer'},
143
+ {'type': 'null'},
144
+ ]
145
+ })
146
+
147
+ def test_any(self):
148
+ self.assert_json_schema(vs.Any(), {
149
+ 'anyOf': [
150
+ {'type': 'boolean'},
151
+ {'type': 'number'},
152
+ {'type': 'string'},
153
+ {'type': 'array'},
154
+ {'type': 'object', 'additionalProperties': True},
155
+ {'type': 'null'},
156
+ ]
157
+ })
158
+
159
+ def test_object(self):
160
+ class A:
161
+ def __init__(self, x: int, y: str):
162
+ pass
163
+
164
+ self.assert_json_schema(vs.Object(A), {
165
+ 'type': 'object',
166
+ 'properties': {
167
+ 'x': {
168
+ 'type': 'integer',
169
+ },
170
+ 'y': {
171
+ 'type': 'string',
172
+ },
173
+ },
174
+ 'required': ['x', 'y'],
175
+ 'title': 'A',
176
+ 'additionalProperties': False,
177
+ }, include_type_name=False)
178
+
179
+ self.assert_json_schema(vs.Object(A), {
180
+ 'type': 'object',
181
+ 'properties': {
182
+ 'x': {
183
+ 'type': 'integer',
184
+ },
185
+ 'y': {
186
+ 'type': 'string',
187
+ },
188
+ },
189
+ 'required': ['x', 'y'],
190
+ 'title': 'A',
191
+ 'additionalProperties': False,
192
+ }, include_type_name=True)
193
+
194
+ def test_pg_object(self):
195
+
196
+ class A(pg_object.Object):
197
+ x: int
198
+ y: str
199
+
200
+ self.assert_json_schema(vs.Object(A), {
201
+ 'type': 'object',
202
+ 'properties': {
203
+ 'x': {
204
+ 'type': 'integer',
205
+ },
206
+ 'y': {
207
+ 'type': 'string',
208
+ },
209
+ },
210
+ 'required': ['x', 'y'],
211
+ 'title': 'A',
212
+ 'additionalProperties': False,
213
+ }, include_type_name=False)
214
+
215
+ self.assert_json_schema(vs.Object(A), {
216
+ 'type': 'object',
217
+ 'properties': {
218
+ '_type': {
219
+ 'const': A.__type_name__,
220
+ },
221
+ 'x': {
222
+ 'type': 'integer',
223
+ },
224
+ 'y': {
225
+ 'type': 'string',
226
+ },
227
+ },
228
+ 'required': ['_type', 'x', 'y'],
229
+ 'title': 'A',
230
+ 'additionalProperties': False,
231
+ }, include_type_name=True)
232
+
233
+ def test_pg_object_nessted(self):
234
+
235
+ class A(pg_object.Object):
236
+ x: Annotated[int, 'field x']
237
+ y: str
238
+
239
+ class B(pg_object.Object):
240
+ z: A
241
+
242
+ self.assert_json_schema(vs.Object(B), {
243
+ '$defs': {
244
+ 'A': {
245
+ 'type': 'object',
246
+ 'properties': {
247
+ '_type': {
248
+ 'const': A.__type_name__,
249
+ },
250
+ 'x': {
251
+ 'type': 'integer',
252
+ 'description': 'field x',
253
+ },
254
+ 'y': {
255
+ 'type': 'string',
256
+ },
257
+ },
258
+ 'required': ['_type', 'x', 'y'],
259
+ 'title': 'A',
260
+ 'additionalProperties': False,
261
+ }
262
+ },
263
+ 'type': 'object',
264
+ 'properties': {
265
+ '_type': {
266
+ 'const': B.__type_name__,
267
+ },
268
+ 'z': {
269
+ '$ref': '#/$defs/A'
270
+ },
271
+ },
272
+ 'required': ['_type', 'z'],
273
+ 'title': 'B',
274
+ 'additionalProperties': False,
275
+ }, include_type_name=True)
276
+
277
+ self.assert_json_schema(vs.Object(B), {
278
+ 'type': 'object',
279
+ 'properties': {
280
+ '_type': {
281
+ 'const': B.__type_name__,
282
+ },
283
+ 'z': {
284
+ 'type': 'object',
285
+ 'properties': {
286
+ '_type': {
287
+ 'const': A.__type_name__,
288
+ },
289
+ 'x': {
290
+ 'type': 'integer',
291
+ 'description': 'field x',
292
+ },
293
+ 'y': {
294
+ 'type': 'string',
295
+ },
296
+ },
297
+ 'required': ['_type', 'x', 'y'],
298
+ 'title': 'A',
299
+ 'additionalProperties': False,
300
+ },
301
+ },
302
+ 'required': ['_type', 'z'],
303
+ 'title': 'B',
304
+ 'additionalProperties': False,
305
+ }, include_type_name=True, inline_nested_refs=True)
306
+
307
+ def test_pg_object_with_subclasses(self):
308
+ class A(pg_object.Object):
309
+ x: int
310
+ y: str
311
+
312
+ class B(A):
313
+ z: int
314
+
315
+ self.assert_json_schema(
316
+ vs.Object(A).noneable(),
317
+ {
318
+ 'anyOf': [
319
+ {
320
+ 'additionalProperties': False,
321
+ 'properties': {
322
+ '_type': {
323
+ 'const': A.__type_name__,
324
+ },
325
+ 'x': {
326
+ 'type': 'integer',
327
+ },
328
+ 'y': {
329
+ 'type': 'string',
330
+ },
331
+ },
332
+ 'required': ['_type', 'x', 'y'],
333
+ 'title': 'A',
334
+ 'type': 'object',
335
+ },
336
+ {
337
+ 'additionalProperties': False,
338
+ 'properties': {
339
+ '_type': {
340
+ 'const': B.__type_name__,
341
+ },
342
+ 'x': {
343
+ 'type': 'integer',
344
+ },
345
+ 'y': {
346
+ 'type': 'string',
347
+ },
348
+ 'z': {
349
+ 'type': 'integer',
350
+ },
351
+ },
352
+ 'required': ['_type', 'x', 'y', 'z'],
353
+ 'title': 'B',
354
+ 'type': 'object',
355
+ },
356
+ {
357
+ 'type': 'null',
358
+ }
359
+ ],
360
+ },
361
+ include_type_name=True,
362
+ include_subclasses=True,
363
+ )
364
+
365
+ def test_pg_object_with_recursive_refs(self):
366
+ self.assert_json_schema(
367
+ vs.Object(Foo),
368
+ {
369
+ '$defs': {
370
+ 'Foo': {
371
+ 'additionalProperties': False,
372
+ 'properties': {
373
+ '_type': {
374
+ 'const': Foo.__type_name__,
375
+ },
376
+ 'x': {
377
+ 'type': 'integer',
378
+ },
379
+ 'y': {
380
+ '$ref': '#/$defs/Bar',
381
+ },
382
+ },
383
+ 'required': ['_type', 'x', 'y'],
384
+ 'title': 'Foo',
385
+ 'type': 'object',
386
+ },
387
+ 'Bar': {
388
+ 'additionalProperties': False,
389
+ 'properties': {
390
+ '_type': {
391
+ 'const': Bar.__type_name__,
392
+ },
393
+ 'z': {
394
+ 'anyOf': [
395
+ {'$ref': '#/$defs/Foo'},
396
+ {'type': 'null'},
397
+ ]
398
+ },
399
+ },
400
+ 'required': ['_type', 'z'],
401
+ 'title': 'Bar',
402
+ 'type': 'object',
403
+ }
404
+ },
405
+ '$ref': '#/$defs/Foo',
406
+ }
407
+ )
408
+
409
+ def test_unsupported_value_spec(self):
410
+ with self.assertRaisesRegex(
411
+ TypeError, 'Value spec .* cannot be converted to JSON schema'
412
+ ):
413
+ json_schema.to_json_schema(vs.Callable())
414
+
415
+ def test_schema(self):
416
+ class A(pg_object.Object):
417
+ x: int
418
+ y: str
419
+
420
+ self.assert_json_schema(
421
+ A.__schema__,
422
+ {
423
+ 'type': 'object',
424
+ 'properties': {
425
+ '_type': {
426
+ 'const': A.__type_name__,
427
+ },
428
+ 'x': {
429
+ 'type': 'integer',
430
+ },
431
+ 'y': {
432
+ 'type': 'string',
433
+ },
434
+ },
435
+ 'required': ['_type', 'x', 'y'],
436
+ 'title': 'A',
437
+ 'additionalProperties': False,
438
+ },
439
+ include_type_name=True,
440
+ )
441
+
442
+ def test_value_spec_to_json_schema(self):
443
+ self.assertEqual(
444
+ vs.Int().to_json_schema(),
445
+ {
446
+ 'type': 'integer',
447
+ }
448
+ )
449
+
450
+ def test_schema_to_json_schema(self):
451
+ class A(pg_object.Object):
452
+ x: int
453
+ y: str
454
+
455
+ self.assertEqual(
456
+ A.__schema__.to_json_schema(),
457
+ {
458
+ 'type': 'object',
459
+ 'properties': {
460
+ '_type': {
461
+ 'const': A.__type_name__,
462
+ },
463
+ 'x': {
464
+ 'type': 'integer',
465
+ },
466
+ 'y': {
467
+ 'type': 'string',
468
+ },
469
+ },
470
+ 'required': ['_type', 'x', 'y'],
471
+ 'title': 'A',
472
+ 'additionalProperties': False,
473
+ }
474
+ )
475
+
476
+ if __name__ == '__main__':
477
+ unittest.main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyglove
3
- Version: 0.4.5.dev202505010810
3
+ Version: 0.4.5.dev202505030808
4
4
  Summary: PyGlove: A library for manipulating Python objects.
5
5
  Home-page: https://github.com/google/pyglove
6
6
  Author: PyGlove Authors
@@ -106,7 +106,7 @@ pyglove/core/tuning/protocols.py,sha256=10Iukt1rqh05caURTZffSsb3CcHo7epBQnNtnyMy
106
106
  pyglove/core/tuning/protocols_test.py,sha256=Cbzvz3EacaW2sbm1rTSQXEt_VucMoQbeQ6AeN-GV5Vc,1883
107
107
  pyglove/core/tuning/sample.py,sha256=UzsCY8kiqnzH_mR94zLXhOloyvvEwfmBWluBjmefUFA,12975
108
108
  pyglove/core/tuning/sample_test.py,sha256=JqwDPy3EPC_VjU9dipk90jj1kovZB3Zb9hAjAlZ-U1U,17551
109
- pyglove/core/typing/__init__.py,sha256=MwraQ-6K8NYI_xk4V3oaVSkrPqiU9InDtAgXPSRBvik,14494
109
+ pyglove/core/typing/__init__.py,sha256=u2YSrSi8diTkQn8_1J2hEpk5o7zDhx2tU_oRuS-k1XU,14580
110
110
  pyglove/core/typing/annotated.py,sha256=llaajIDj9GK-4kUGJoO4JsHU6ESPOra2SZ-jG6xmsOQ,3203
111
111
  pyglove/core/typing/annotated_test.py,sha256=p1qid3R-jeiOTTxOVq6hXW8XFvn-h1cUzJWISPst2l8,2484
112
112
  pyglove/core/typing/annotation_conversion.py,sha256=txUrChAhMNeaukV-PSQEA9BCjtonUQDWFHxnpTE0_K8,15582
@@ -116,11 +116,13 @@ pyglove/core/typing/callable_ext.py,sha256=PiBQWPeUAH7Lgmf2xKCZqgK7N0OSrTdbnEkV8
116
116
  pyglove/core/typing/callable_ext_test.py,sha256=TnWKU4_ZjvpbHZFtFHgFvCMDiCos8VmLlODcM_7Xg8M,10156
117
117
  pyglove/core/typing/callable_signature.py,sha256=DRpt7aShfkn8pb3SCiZzS_27eHbkQ_d2UB8BUhJjs0Q,27176
118
118
  pyglove/core/typing/callable_signature_test.py,sha256=iQmHsKPhJPQlMikDhEyxKyq7yWyXI9juKCLYgKhrH3U,25145
119
- pyglove/core/typing/class_schema.py,sha256=u_pxvykZlwyd5ycdfHpWYI8ccttmBbzexrajNVbEVG4,54553
119
+ pyglove/core/typing/class_schema.py,sha256=3U92-Suwdxlc8mXjK6Tqa9iiqogtRuv1YZwVYpmkMTc,55288
120
120
  pyglove/core/typing/class_schema_test.py,sha256=sJkE7ndDSIKb0EUcjZiVFOeJYDI7Hdu2GdPJCMgZxrI,29556
121
121
  pyglove/core/typing/custom_typing.py,sha256=qdnIKHWNt5kZAAFdpQXra8bBu6RljMbbJ_YDG2mhAUA,2205
122
122
  pyglove/core/typing/inspect.py,sha256=VLSz1KAunNm2hx0eEMjiwxKLl9FHlKr9nHelLT25iEA,7726
123
123
  pyglove/core/typing/inspect_test.py,sha256=xclevobF0X8c_B5b1q1dkBJZN1TsVA1RUhk5l25DUCM,10248
124
+ pyglove/core/typing/json_schema.py,sha256=shHElSIpw6AZ5L1iKrKzX3yZXgGKYFJOvMC-syEf-i8,10453
125
+ pyglove/core/typing/json_schema_test.py,sha256=DhtyoWLoYzCS-DU67QuwaZKZ-9WgK_ZFVsiFp94Ltx8,13011
124
126
  pyglove/core/typing/key_specs.py,sha256=-7xjCuUGoQgD0sMafsRFNlw3S4f1r-7t5OO4ev5bbeI,9225
125
127
  pyglove/core/typing/key_specs_test.py,sha256=5zornpyHMAYoRaG8KDXHiQ3obu9UfRp3399lBeUNTPk,6499
126
128
  pyglove/core/typing/pytype_support.py,sha256=lyX11WVbCwoOi5tTQ90pEOS-yvo_6iEi7Lxbp-nXu2A,2069
@@ -212,8 +214,8 @@ pyglove/ext/scalars/randoms.py,sha256=LkMIIx7lOq_lvJvVS3BrgWGuWl7Pi91-lA-O8x_gZs
212
214
  pyglove/ext/scalars/randoms_test.py,sha256=nEhiqarg8l_5EOucp59CYrpO2uKxS1pe0hmBdZUzRNM,2000
213
215
  pyglove/ext/scalars/step_wise.py,sha256=IDw3tuTpv0KVh7AN44W43zqm1-E0HWPUlytWOQC9w3Y,3789
214
216
  pyglove/ext/scalars/step_wise_test.py,sha256=TL1vJ19xVx2t5HKuyIzGoogF7N3Rm8YhLE6JF7i0iy8,2540
215
- pyglove-0.4.5.dev202505010810.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
216
- pyglove-0.4.5.dev202505010810.dist-info/METADATA,sha256=x9jX1JAzZ7jsVnFkXSCjct596hP4KJPUd88EPcHwPj0,7089
217
- pyglove-0.4.5.dev202505010810.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
218
- pyglove-0.4.5.dev202505010810.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
219
- pyglove-0.4.5.dev202505010810.dist-info/RECORD,,
217
+ pyglove-0.4.5.dev202505030808.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
218
+ pyglove-0.4.5.dev202505030808.dist-info/METADATA,sha256=glCGeZAatT2jJo4vHgSm2evKvJxY9jC-K1N82J32JJ4,7089
219
+ pyglove-0.4.5.dev202505030808.dist-info/WHEEL,sha256=7ciDxtlje1X8OhobNuGgi1t-ACdFSelPnSmDPrtlobY,91
220
+ pyglove-0.4.5.dev202505030808.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
221
+ pyglove-0.4.5.dev202505030808.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.1.0)
2
+ Generator: setuptools (80.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5