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.
- pyglove/core/typing/__init__.py +3 -0
- pyglove/core/typing/class_schema.py +23 -0
- pyglove/core/typing/json_schema.py +340 -0
- pyglove/core/typing/json_schema_test.py +477 -0
- {pyglove-0.4.5.dev202505010810.dist-info → pyglove-0.4.5.dev202505030808.dist-info}/METADATA +1 -1
- {pyglove-0.4.5.dev202505010810.dist-info → pyglove-0.4.5.dev202505030808.dist-info}/RECORD +9 -7
- {pyglove-0.4.5.dev202505010810.dist-info → pyglove-0.4.5.dev202505030808.dist-info}/WHEEL +1 -1
- {pyglove-0.4.5.dev202505010810.dist-info → pyglove-0.4.5.dev202505030808.dist-info}/licenses/LICENSE +0 -0
- {pyglove-0.4.5.dev202505010810.dist-info → pyglove-0.4.5.dev202505030808.dist-info}/top_level.txt +0 -0
pyglove/core/typing/__init__.py
CHANGED
@@ -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()
|
@@ -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=
|
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=
|
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.
|
216
|
-
pyglove-0.4.5.
|
217
|
-
pyglove-0.4.5.
|
218
|
-
pyglove-0.4.5.
|
219
|
-
pyglove-0.4.5.
|
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,,
|
{pyglove-0.4.5.dev202505010810.dist-info → pyglove-0.4.5.dev202505030808.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
{pyglove-0.4.5.dev202505010810.dist-info → pyglove-0.4.5.dev202505030808.dist-info}/top_level.txt
RENAMED
File without changes
|