pyglove 0.5.0.dev202510140809__py3-none-any.whl → 0.5.0.dev202510160810__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.

@@ -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.
@@ -1375,6 +1385,16 @@ class Schema(utils.Formattable, utils.JSONConvertible):
1375
1385
  def __ne__(self, other: Any) -> bool:
1376
1386
  return not self.__eq__(other)
1377
1387
 
1388
+ @classmethod
1389
+ def from_json_schema(
1390
+ cls,
1391
+ json_schema: Dict[str, Any],
1392
+ class_fn: Optional[Callable[[str, 'Schema'], Type[Any]]] = None
1393
+ ) -> 'Schema':
1394
+ """Converts a JSON schema to a schema."""
1395
+ del json_schema, class_fn
1396
+ assert False, 'Overridden in `json_schema.py`.'
1397
+
1378
1398
 
1379
1399
  FieldDef = Union[
1380
1400
  # Key, Value spec/annotation.
@@ -15,7 +15,7 @@
15
15
 
16
16
  import dataclasses
17
17
  import inspect
18
- from typing import Any, Dict, List, Optional, Tuple, Union
18
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
19
19
 
20
20
  from pyglove.core import utils
21
21
  from pyglove.core.typing import callable_signature
@@ -53,8 +53,8 @@ def _json_schema_from_schema(
53
53
  required = []
54
54
 
55
55
  if type_name and include_type_name:
56
- properties['_type'] = {'const': type_name}
57
- required.append('_type')
56
+ properties[utils.JSONConvertible.TYPE_NAME_KEY] = {'const': type_name}
57
+ required.append(utils.JSONConvertible.TYPE_NAME_KEY)
58
58
 
59
59
  for key, field in schema.items():
60
60
  if isinstance(key, ks.ConstStrKey):
@@ -145,7 +145,6 @@ def _json_schema_from_value_spec(
145
145
  }
146
146
  if not isinstance(value_spec.element.value, vs.Any):
147
147
  definition['items'] = _child_json_schema(value_spec.element.value)
148
- return definition
149
148
  elif isinstance(value_spec, vs.Dict):
150
149
  if value_spec.schema is None:
151
150
  definition = {
@@ -168,9 +167,7 @@ def _json_schema_from_value_spec(
168
167
  include_type_name=include_type_name,
169
168
  include_subclasses=include_subclasses,
170
169
  )
171
- definitions = [
172
- _json_schema_from_cls(value_spec.cls)
173
- ]
170
+ definitions = [_json_schema_from_cls(value_spec.cls)]
174
171
 
175
172
  if include_subclasses:
176
173
  for subclass in value_spec.cls.__subclasses__():
@@ -206,12 +203,26 @@ def _json_schema_from_value_spec(
206
203
  f'Value spec {value_spec!r} cannot be converted to JSON schema.'
207
204
  )
208
205
 
206
+ if (value_spec.has_default
207
+ and value_spec.default is not None
208
+ and not isinstance(value_spec, vs.Dict)):
209
+ default = utils.to_json(value_spec.default)
210
+ if not include_type_name:
211
+ def _remove_type_name(_, v: Dict[str, Any]):
212
+ if isinstance(v, dict):
213
+ v.pop(utils.JSONConvertible.TYPE_NAME_KEY, None)
214
+ return v
215
+ default = utils.transform(default, _remove_type_name)
216
+ definition['default'] = default
217
+
209
218
  if not ignore_nonable and value_spec.is_noneable:
210
219
  nullable = {'type': 'null'}
211
220
  if 'anyOf' in definition:
212
221
  definition['anyOf'].append(nullable)
213
222
  else:
214
223
  definition = {'anyOf': [definition, nullable]}
224
+ if value_spec.default is None:
225
+ definition['default'] = None
215
226
  return definition
216
227
 
217
228
 
@@ -294,6 +305,8 @@ def _canonicalize_schema(
294
305
  }
295
306
  canonical_form = {'$defs': referenced_defs} if referenced_defs else {}
296
307
  canonical_form.update(new_root)
308
+ if 'default' in root:
309
+ canonical_form['default'] = root['default']
297
310
  return canonical_form
298
311
 
299
312
  #
@@ -336,5 +349,205 @@ def _schema_to_json_schema(
336
349
  **kwargs
337
350
  )
338
351
 
339
- vs.ValueSpec.to_json_schema = _value_spec_to_json_schema
352
+ class_schema.ValueSpec.to_json_schema = _value_spec_to_json_schema
340
353
  class_schema.Schema.to_json_schema = _schema_to_json_schema
354
+
355
+ #
356
+ # from JSON schema to PyGlove value spec.
357
+ #
358
+
359
+
360
+ def _json_schema_to_value_spec(
361
+ json_schema: Dict[str, Any],
362
+ defs: Dict[str, Type[Any]],
363
+ class_fn: Optional[Callable[[str, class_schema.Schema], Type[Any]]],
364
+ add_json_schema_as_metadata: bool,
365
+ ) -> class_schema.ValueSpec:
366
+ """Converts a JSON schema to a value spec."""
367
+ # Generate code to convert JSON schema to value spec.
368
+ def _value_spec(value_schema: Dict[str, Any]):
369
+ return _json_schema_to_value_spec(
370
+ value_schema, defs, class_fn, add_json_schema_as_metadata
371
+ )
372
+
373
+ if '$ref' in json_schema:
374
+ # TODO(daiyip): Support circular references.
375
+ ref_key = json_schema['$ref'].split('/')[-1]
376
+ type_ref = defs.get(ref_key)
377
+ if type_ref is None:
378
+ raise ValueError(
379
+ f'Reference {ref_key!r} not defined in defs. '
380
+ 'Please make sure classes being referenced are defined '
381
+ 'before the referencing classes. '
382
+ )
383
+ return type_ref
384
+ type_str = json_schema.get('type')
385
+ default = json_schema.get('default', utils.MISSING_VALUE)
386
+ if type_str is None:
387
+ if 'enum' in json_schema:
388
+ for v in json_schema['enum']:
389
+ if not isinstance(v, (str, int, float, bool)):
390
+ raise ValueError(
391
+ f'Enum candidate {v!r} is not supported for JSON schema '
392
+ 'conversion.'
393
+ )
394
+ return vs.Enum(
395
+ default,
396
+ [v for v in json_schema['enum'] if v is not None]
397
+ )
398
+ elif 'anyOf' in json_schema:
399
+ candidates = []
400
+ accepts_none = False
401
+ for v in json_schema['anyOf']:
402
+ candidate = _value_spec(v)
403
+ if candidate.frozen and candidate.default is None:
404
+ accepts_none = True
405
+ continue
406
+ candidates.append(candidate)
407
+
408
+ if len(candidates) == 1:
409
+ spec = candidates[0]
410
+ else:
411
+ spec = vs.Union(candidates)
412
+ if accepts_none:
413
+ spec = spec.noneable()
414
+ return spec
415
+ elif type_str == 'null':
416
+ return vs.Any().freeze(None)
417
+ elif type_str == 'boolean':
418
+ return vs.Bool(default=default)
419
+ elif type_str == 'integer':
420
+ minimum = json_schema.get('minimum')
421
+ maximum = json_schema.get('maximum')
422
+ return vs.Int(min_value=minimum, max_value=maximum, default=default)
423
+ elif type_str == 'number':
424
+ minimum = json_schema.get('minimum')
425
+ maximum = json_schema.get('maximum')
426
+ return vs.Float(min_value=minimum, max_value=maximum, default=default)
427
+ elif type_str == 'string':
428
+ pattern = json_schema.get('pattern')
429
+ return vs.Str(regex=pattern, default=default)
430
+ elif type_str == 'array':
431
+ items = json_schema.get('items')
432
+ return vs.List(_value_spec(items) if items else vs.Any(), default=default)
433
+ elif type_str == 'object':
434
+ schema = _json_schema_to_schema(
435
+ json_schema, defs, class_fn, add_json_schema_as_metadata
436
+ )
437
+ if class_fn is not None and 'title' in json_schema:
438
+ return vs.Object(class_fn(json_schema['title'], schema))
439
+ return vs.Dict(schema=schema if schema.fields else None)
440
+ raise ValueError(f'Unsupported type {type_str!r} in JSON schema.')
441
+
442
+
443
+ def _json_schema_to_schema(
444
+ json_schema: Dict[str, Any],
445
+ defs: Dict[str, Type[Any]],
446
+ class_fn: Optional[Callable[[str, class_schema.Schema], Type[Any]]],
447
+ add_json_schema_as_metadata: bool,
448
+ ) -> class_schema.Schema:
449
+ """Converts a JSON schema to a schema."""
450
+ title = json_schema.get('title')
451
+ properties = json_schema.get('properties', {})
452
+ fields = []
453
+ required = set(json_schema.get('required', []))
454
+ for name, property_schema in properties.items():
455
+ value_spec = _json_schema_to_value_spec(
456
+ property_schema, defs, class_fn, add_json_schema_as_metadata
457
+ )
458
+ if name not in required and not value_spec.has_default:
459
+ value_spec = value_spec.noneable()
460
+ fields.append(
461
+ class_schema.Field(
462
+ name,
463
+ value_spec,
464
+ description=property_schema.get('description'),
465
+ metadata=(
466
+ dict(json_schema=property_schema)
467
+ if add_json_schema_as_metadata else None
468
+ )
469
+ )
470
+ )
471
+ additional_properties = json_schema.get('additionalProperties')
472
+ if additional_properties:
473
+ if isinstance(additional_properties, dict):
474
+ value_spec = _json_schema_to_value_spec(
475
+ additional_properties, defs, class_fn, add_json_schema_as_metadata
476
+ )
477
+ else:
478
+ value_spec = vs.Any()
479
+ fields.append(class_schema.Field(ks.StrKey(), value_spec))
480
+ return class_schema.Schema(
481
+ name=title,
482
+ description=json_schema.get('description'),
483
+ fields=fields,
484
+ allow_nonconst_keys=True,
485
+ )
486
+
487
+
488
+ @classmethod
489
+ def _value_spec_from_json_schema(
490
+ cls,
491
+ json_schema: Dict[str, Any],
492
+ class_fn: Optional[Callable[[str, class_schema.Schema], Type[Any]]] = None,
493
+ add_json_schema_as_metadata: bool = False,
494
+ ) -> class_schema.ValueSpec:
495
+ """Creates a PyGlove value spec from a JSON schema.
496
+
497
+ Args:
498
+ json_schema: The JSON schema for a value spec.
499
+ class_fn: A function that creates a PyGlove class from a class name and a
500
+ schema. If None, all "object" type properties will be converted to
501
+ `pg.typing.Dict`. Otherwise, "object" type properties will be converted to
502
+ a class.
503
+ add_json_schema_as_metadata: Whether to add the JSON schema as field
504
+ metadata.
505
+
506
+ Returns:
507
+ A PyGlove value spec.
508
+ """
509
+ del cls
510
+ defs = {}
511
+ if '$defs' in json_schema:
512
+ for key, def_entry in json_schema['$defs'].items():
513
+ defs[key] = _json_schema_to_value_spec(
514
+ def_entry, defs, class_fn, add_json_schema_as_metadata
515
+ )
516
+ return _json_schema_to_value_spec(
517
+ json_schema, defs, class_fn, add_json_schema_as_metadata
518
+ )
519
+
520
+
521
+ @classmethod
522
+ def _schema_from_json_schema(
523
+ cls,
524
+ json_schema: Dict[str, Any],
525
+ class_fn: Optional[Callable[[str, class_schema.Schema], Type[Any]]] = None,
526
+ add_json_schema_as_metadata: bool = False,
527
+ ) -> class_schema.Schema:
528
+ """Creates a PyGlove schema from a JSON schema.
529
+
530
+ Args:
531
+ json_schema: The JSON schema to convert.
532
+ class_fn: A function that creates a PyGlove class from a class name and a
533
+ schema. If None, all "object" type properties will be converted to
534
+ `pg.typing.Dict`. Otherwise, "object" type properties will be converted to
535
+ a class.
536
+ add_json_schema_as_metadata: Whether to add the JSON schema as field
537
+ metadata.
538
+
539
+ Returns:
540
+ A PyGlove schema.
541
+ """
542
+ del cls
543
+ if json_schema.get('type') != 'object':
544
+ raise ValueError(
545
+ f'JSON schema is not an object type: {json_schema!r}'
546
+ )
547
+ return _json_schema_to_schema(
548
+ json_schema, {}, class_fn, add_json_schema_as_metadata
549
+ )
550
+
551
+
552
+ class_schema.ValueSpec.from_json_schema = _value_spec_from_json_schema
553
+ class_schema.Schema.from_json_schema = _schema_from_json_schema
@@ -31,7 +31,7 @@ class Bar(pg_object.Object):
31
31
  z: Optional[Foo]
32
32
 
33
33
 
34
- class JsonSchemaTest(unittest.TestCase):
34
+ class ToJsonSchemaTest(unittest.TestCase):
35
35
 
36
36
  maxDiff = None
37
37
 
@@ -53,6 +53,10 @@ class JsonSchemaTest(unittest.TestCase):
53
53
  self.assert_json_schema(bool, {
54
54
  'type': 'boolean',
55
55
  })
56
+ self.assert_json_schema(vs.Bool(default=True), {
57
+ 'type': 'boolean',
58
+ 'default': True,
59
+ })
56
60
 
57
61
  def test_int(self):
58
62
  self.assert_json_schema(int, {
@@ -62,9 +66,10 @@ class JsonSchemaTest(unittest.TestCase):
62
66
  'type': 'integer',
63
67
  'minimum': 0,
64
68
  })
65
- self.assert_json_schema(vs.Int(max_value=1), {
69
+ self.assert_json_schema(vs.Int(max_value=1, default=0), {
66
70
  'type': 'integer',
67
71
  'maximum': 1,
72
+ 'default': 0,
68
73
  })
69
74
 
70
75
  def test_float(self):
@@ -75,30 +80,43 @@ class JsonSchemaTest(unittest.TestCase):
75
80
  'type': 'number',
76
81
  'minimum': 0.0,
77
82
  })
78
- self.assert_json_schema(vs.Float(max_value=1.0), {
83
+ self.assert_json_schema(vs.Float(max_value=1.0, default=0.0), {
79
84
  'type': 'number',
80
85
  'maximum': 1.0,
86
+ 'default': 0.0,
81
87
  })
82
88
 
83
89
  def test_str(self):
84
90
  self.assert_json_schema(str, {
85
91
  'type': 'string',
86
92
  })
87
- self.assert_json_schema(vs.Str(regex='a.*'), {
93
+ self.assert_json_schema(vs.Str(regex='a.*', default='a1'), {
88
94
  'type': 'string',
89
95
  'pattern': 'a.*',
96
+ 'default': 'a1',
90
97
  })
91
98
 
92
99
  def test_enum(self):
93
100
  self.assert_json_schema(Literal['a', 1], {
94
101
  'enum': ['a', 1]
95
102
  })
103
+ self.assert_json_schema(vs.Enum(1, ['a', 1]), {
104
+ 'enum': ['a', 1],
105
+ 'default': 1,
106
+ })
96
107
  self.assert_json_schema(Literal['a', 1, None], {
97
108
  'anyOf': [
98
109
  {'enum': ['a', 1]},
99
110
  {'type': 'null'}
100
111
  ]
101
112
  })
113
+ self.assert_json_schema(vs.Enum(None, ['a', 1, None]), {
114
+ 'anyOf': [
115
+ {'enum': ['a', 1]},
116
+ {'type': 'null'}
117
+ ],
118
+ 'default': None
119
+ })
102
120
  with self.assertRaisesRegex(
103
121
  ValueError, 'Enum candidate .* is not supported'
104
122
  ):
@@ -111,6 +129,13 @@ class JsonSchemaTest(unittest.TestCase):
111
129
  'type': 'integer',
112
130
  }
113
131
  })
132
+ self.assert_json_schema(vs.List(int, default=[1, 2]), {
133
+ 'type': 'array',
134
+ 'items': {
135
+ 'type': 'integer',
136
+ },
137
+ 'default': [1, 2],
138
+ })
114
139
 
115
140
  def test_dict(self):
116
141
  self.assert_json_schema(vs.Dict(), {
@@ -143,6 +168,15 @@ class JsonSchemaTest(unittest.TestCase):
143
168
  {'type': 'null'},
144
169
  ]
145
170
  })
171
+ self.assert_json_schema(vs.Union([int, vs.Union([str, int]).noneable()]), {
172
+ 'anyOf': [
173
+ {'type': 'integer'},
174
+ {'type': 'string'},
175
+ # TODO(daiyip): Remove duplicates for nested Union in future.
176
+ {'type': 'integer'},
177
+ {'type': 'null'},
178
+ ]
179
+ })
146
180
 
147
181
  def test_any(self):
148
182
  self.assert_json_schema(vs.Any(), {
@@ -155,6 +189,17 @@ class JsonSchemaTest(unittest.TestCase):
155
189
  {'type': 'null'},
156
190
  ]
157
191
  })
192
+ self.assert_json_schema(vs.Any(default=1), {
193
+ 'anyOf': [
194
+ {'type': 'boolean'},
195
+ {'type': 'number'},
196
+ {'type': 'string'},
197
+ {'type': 'array'},
198
+ {'type': 'object', 'additionalProperties': True},
199
+ {'type': 'null'},
200
+ ],
201
+ 'default': 1,
202
+ })
158
203
 
159
204
  def test_object(self):
160
205
  class A:
@@ -176,9 +221,16 @@ class JsonSchemaTest(unittest.TestCase):
176
221
  'additionalProperties': False,
177
222
  }, include_type_name=False)
178
223
 
179
- self.assert_json_schema(vs.Object(A), {
224
+ class B(pg_object.Object):
225
+ x: int
226
+ y: str
227
+
228
+ self.assert_json_schema(vs.Object(B), {
180
229
  'type': 'object',
181
230
  'properties': {
231
+ '_type': {
232
+ 'const': B.__type_name__,
233
+ },
182
234
  'x': {
183
235
  'type': 'integer',
184
236
  },
@@ -186,11 +238,30 @@ class JsonSchemaTest(unittest.TestCase):
186
238
  'type': 'string',
187
239
  },
188
240
  },
189
- 'required': ['x', 'y'],
190
- 'title': 'A',
241
+ 'required': ['_type', 'x', 'y'],
242
+ 'title': 'B',
191
243
  'additionalProperties': False,
192
244
  }, include_type_name=True)
193
245
 
246
+ self.assert_json_schema(vs.Object(B, default=B(x=1, y='a')), {
247
+ 'type': 'object',
248
+ 'properties': {
249
+ 'x': {
250
+ 'type': 'integer',
251
+ },
252
+ 'y': {
253
+ 'type': 'string',
254
+ },
255
+ },
256
+ 'required': ['x', 'y'],
257
+ 'title': 'B',
258
+ 'additionalProperties': False,
259
+ 'default': {
260
+ 'x': 1,
261
+ 'y': 'a',
262
+ },
263
+ }, include_type_name=False)
264
+
194
265
  def test_pg_object(self):
195
266
 
196
267
  class A(pg_object.Object):
@@ -230,14 +301,14 @@ class JsonSchemaTest(unittest.TestCase):
230
301
  'additionalProperties': False,
231
302
  }, include_type_name=True)
232
303
 
233
- def test_pg_object_nessted(self):
304
+ def test_pg_object_nested(self):
234
305
 
235
306
  class A(pg_object.Object):
236
307
  x: Annotated[int, 'field x']
237
308
  y: str
238
309
 
239
310
  class B(pg_object.Object):
240
- z: A
311
+ z: A = A(x=1, y='a')
241
312
 
242
313
  self.assert_json_schema(vs.Object(B), {
243
314
  '$defs': {
@@ -266,10 +337,15 @@ class JsonSchemaTest(unittest.TestCase):
266
337
  'const': B.__type_name__,
267
338
  },
268
339
  'z': {
269
- '$ref': '#/$defs/A'
340
+ '$ref': '#/$defs/A',
341
+ 'default': {
342
+ '_type': A.__type_name__,
343
+ 'x': 1,
344
+ 'y': 'a',
345
+ },
270
346
  },
271
347
  },
272
- 'required': ['_type', 'z'],
348
+ 'required': ['_type'],
273
349
  'title': 'B',
274
350
  'additionalProperties': False,
275
351
  }, include_type_name=True)
@@ -299,7 +375,7 @@ class JsonSchemaTest(unittest.TestCase):
299
375
  'additionalProperties': False,
300
376
  },
301
377
  },
302
- 'required': ['_type', 'z'],
378
+ 'required': ['_type'],
303
379
  'title': 'B',
304
380
  'additionalProperties': False,
305
381
  }, include_type_name=True, inline_nested_refs=True)
@@ -357,6 +433,7 @@ class JsonSchemaTest(unittest.TestCase):
357
433
  'type': 'null',
358
434
  }
359
435
  ],
436
+ 'default': None,
360
437
  },
361
438
  include_type_name=True,
362
439
  include_subclasses=True,
@@ -473,5 +550,424 @@ class JsonSchemaTest(unittest.TestCase):
473
550
  }
474
551
  )
475
552
 
553
+
554
+ class FromJsonSchemaTest(unittest.TestCase):
555
+
556
+ def assert_value_spec(self, input_json_schema, expected_value_spec):
557
+ value_spec = vs.ValueSpec.from_json_schema(input_json_schema)
558
+ self.assertEqual(value_spec, expected_value_spec)
559
+
560
+ def test_bool(self):
561
+ self.assert_value_spec(
562
+ {
563
+ 'type': 'boolean',
564
+ },
565
+ vs.Bool(),
566
+ )
567
+ self.assert_value_spec(
568
+ {
569
+ 'type': 'boolean',
570
+ 'default': True
571
+ },
572
+ vs.Bool(default=True),
573
+ )
574
+
575
+ def test_int(self):
576
+ self.assert_value_spec(
577
+ {
578
+ 'type': 'integer',
579
+ },
580
+ vs.Int(),
581
+ )
582
+ self.assert_value_spec(
583
+ {
584
+ 'type': 'integer',
585
+ 'minimum': 0,
586
+ },
587
+ vs.Int(min_value=0),
588
+ )
589
+ self.assert_value_spec(
590
+ {
591
+ 'type': 'integer',
592
+ 'maximum': 1,
593
+ 'default': 0,
594
+ },
595
+ vs.Int(max_value=1, default=0),
596
+ )
597
+
598
+ def test_number(self):
599
+ self.assert_value_spec(
600
+ {
601
+ 'type': 'number',
602
+ },
603
+ vs.Float(),
604
+ )
605
+ self.assert_value_spec(
606
+ {
607
+ 'type': 'number',
608
+ 'minimum': 0.0,
609
+ },
610
+ vs.Float(min_value=0.0),
611
+ )
612
+ self.assert_value_spec(
613
+ {
614
+ 'type': 'number',
615
+ 'maximum': 1.0,
616
+ 'default': 0.0,
617
+ },
618
+ vs.Float(max_value=1.0, default=0.0),
619
+ )
620
+
621
+ def test_str(self):
622
+ self.assert_value_spec(
623
+ {
624
+ 'type': 'string',
625
+ },
626
+ vs.Str(),
627
+ )
628
+ self.assert_value_spec(
629
+ {
630
+ 'type': 'string',
631
+ 'pattern': 'a.*',
632
+ 'default': 'a1',
633
+ },
634
+ vs.Str(regex='a.*', default='a1'),
635
+ )
636
+
637
+ def test_enum(self):
638
+ self.assert_value_spec(
639
+ {
640
+ 'enum': ['a', 'b', 'c'],
641
+ 'default': 'b',
642
+ },
643
+ vs.Enum('b', ['a', 'b', 'c']),
644
+ )
645
+ with self.assertRaisesRegex(
646
+ ValueError, 'Enum candidate .* is not supported'
647
+ ):
648
+ vs.ValueSpec.from_json_schema({'enum': [{'x': 1}, {'y': 'abc'}]})
649
+
650
+ def test_null(self):
651
+ self.assert_value_spec(
652
+ {
653
+ 'type': 'null',
654
+ },
655
+ vs.Any().freeze(None),
656
+ )
657
+
658
+ def test_any_of(self):
659
+ self.assert_value_spec(
660
+ {
661
+ 'anyOf': [
662
+ {'type': 'integer'},
663
+ ],
664
+ },
665
+ vs.Int(),
666
+ )
667
+ self.assert_value_spec(
668
+ {
669
+ 'anyOf': [
670
+ {'type': 'integer'},
671
+ {'type': 'string'},
672
+ ],
673
+ },
674
+ vs.Union([vs.Int(), vs.Str()]),
675
+ )
676
+ self.assert_value_spec(
677
+ {
678
+ 'anyOf': [
679
+ {'type': 'integer'},
680
+ {'type': 'string'},
681
+ {'type': 'null'},
682
+ ],
683
+ },
684
+ vs.Union([vs.Int(), vs.Str()]).noneable(),
685
+ )
686
+
687
+ def test_list(self):
688
+ self.assert_value_spec(
689
+ {
690
+ 'type': 'array',
691
+ },
692
+ vs.List(vs.Any()),
693
+ )
694
+ self.assert_value_spec(
695
+ {
696
+ 'type': 'array',
697
+ 'items': {
698
+ 'type': 'integer',
699
+ },
700
+ },
701
+ vs.List(vs.Int()),
702
+ )
703
+ self.assert_value_spec(
704
+ {
705
+ 'type': 'array',
706
+ 'items': {
707
+ 'type': 'integer',
708
+ },
709
+ 'default': [1, 2],
710
+ },
711
+ vs.List(vs.Int(), default=[1, 2]),
712
+ )
713
+
714
+ def test_dict(self):
715
+ self.assert_value_spec(
716
+ {
717
+ 'type': 'object',
718
+ },
719
+ vs.Dict(),
720
+ )
721
+ self.assert_value_spec(
722
+ {
723
+ 'type': 'object',
724
+ 'properties': {
725
+ 'a': {
726
+ 'type': 'integer',
727
+ },
728
+ },
729
+ 'required': ['a'],
730
+ 'additionalProperties': False,
731
+ },
732
+ vs.Dict({'a': vs.Int()}),
733
+ )
734
+ self.assert_value_spec(
735
+ {
736
+ 'type': 'object',
737
+ 'additionalProperties': {'type': 'integer'},
738
+ },
739
+ vs.Dict([(ks.StrKey(), vs.Int())]),
740
+ )
741
+ self.assert_value_spec(
742
+ {
743
+ 'type': 'object',
744
+ 'additionalProperties': True,
745
+ },
746
+ vs.Dict([(ks.StrKey(), vs.Any())]),
747
+ )
748
+
749
+ def _cls_value_spec(self, input_json_schema):
750
+ def schema_to_class(name, schema):
751
+ class _Class(pg_object.Object):
752
+ pass
753
+ cls = _Class
754
+ cls.__name__ = name
755
+ cls.__doc__ = schema.description
756
+ cls.apply_schema(schema)
757
+ return cls
758
+ return vs.ValueSpec.from_json_schema(
759
+ input_json_schema, class_fn=schema_to_class
760
+ )
761
+
762
+ def test_simple_object(self):
763
+ cls_spec = self._cls_value_spec(
764
+ {
765
+ 'type': 'object',
766
+ 'title': 'A',
767
+ 'description': 'Class A',
768
+ 'properties': {
769
+ 'x': {
770
+ 'type': 'integer',
771
+ 'description': 'field x',
772
+ },
773
+ 'y': {
774
+ 'type': 'string',
775
+ },
776
+ },
777
+ 'required': ['x'],
778
+ 'additionalProperties': False,
779
+ },
780
+ )
781
+ self.assertIsNone(cls_spec.cls(x=1).y)
782
+ self.assertEqual(cls_spec.cls.__name__, 'A')
783
+ self.assertEqual(cls_spec.cls.__doc__, 'Class A')
784
+ self.assertEqual(
785
+ cls_spec.cls.__schema__['x'], vs.Field('x', vs.Int(), 'field x')
786
+ )
787
+ self.assertEqual(
788
+ cls_spec.cls.__schema__['y'], vs.Field('y', vs.Str().noneable())
789
+ )
790
+
791
+ def test_nested_object(self):
792
+ cls_spec = self._cls_value_spec(
793
+ {
794
+ 'type': 'object',
795
+ 'title': 'A',
796
+ 'description': 'Class A',
797
+ 'properties': {
798
+ 'x': {
799
+ 'type': 'integer',
800
+ 'description': 'field x',
801
+ },
802
+ 'y': {
803
+ 'type': 'object',
804
+ 'title': 'B',
805
+ 'description': 'Class B',
806
+ 'properties': {
807
+ 'z': {
808
+ 'type': 'string',
809
+ },
810
+ },
811
+ 'required': ['z'],
812
+ 'additionalProperties': False,
813
+ },
814
+ },
815
+ 'required': ['x'],
816
+ 'additionalProperties': False,
817
+ },
818
+ )
819
+ self.assertIsNone(cls_spec.cls(x=1).y)
820
+ self.assertEqual(cls_spec.cls.__name__, 'A')
821
+ self.assertEqual(cls_spec.cls.__doc__, 'Class A')
822
+ self.assertEqual(
823
+ cls_spec.cls.__schema__['x'], vs.Field('x', vs.Int(), 'field x')
824
+ )
825
+ b_cls = cls_spec.cls.__schema__['y'].value.cls
826
+ self.assertEqual(b_cls.__schema__['z'], vs.Field('z', vs.Str()))
827
+
828
+ def test_simple_object_with_def(self):
829
+ cls_spec = self._cls_value_spec(
830
+ {
831
+ '$defs': {
832
+ 'A': {
833
+ 'type': 'object',
834
+ 'title': 'A',
835
+ 'description': 'Class A',
836
+ 'properties': {
837
+ 'x': {
838
+ 'type': 'integer',
839
+ 'description': 'field x',
840
+ 'default': 1,
841
+ },
842
+ 'y': {
843
+ 'type': 'string',
844
+ },
845
+ },
846
+ 'required': ['x'],
847
+ 'additionalProperties': False,
848
+ }
849
+ },
850
+ '$ref': '#/$defs/A',
851
+ }
852
+ )
853
+ self.assertEqual(cls_spec.cls(y='a').x, 1)
854
+ self.assertEqual(cls_spec.cls.__name__, 'A')
855
+ self.assertEqual(cls_spec.cls.__doc__, 'Class A')
856
+
857
+ def test_complex_object_with_def(self):
858
+ cls_spec = self._cls_value_spec(
859
+ {
860
+ '$defs': {
861
+ 'B': {
862
+ 'type': 'object',
863
+ 'title': 'B',
864
+ 'description': 'Class B',
865
+ 'properties': {
866
+ 'z': {
867
+ 'type': 'string',
868
+ },
869
+ },
870
+ 'required': ['z'],
871
+ 'additionalProperties': False,
872
+ },
873
+ 'A': {
874
+ 'type': 'object',
875
+ 'title': 'A',
876
+ 'description': 'Class A',
877
+ 'properties': {
878
+ 'x': {
879
+ 'type': 'integer',
880
+ 'description': 'field x',
881
+ 'default': 1,
882
+ },
883
+ 'y': {
884
+ '$ref': '#/$defs/B',
885
+ }
886
+ },
887
+ 'required': ['x'],
888
+ 'additionalProperties': False,
889
+ },
890
+ },
891
+ '$ref': '#/$defs/A',
892
+ }
893
+ )
894
+ self.assertIsNone(cls_spec.cls(x=1).y)
895
+ self.assertEqual(cls_spec.cls.__name__, 'A')
896
+ self.assertEqual(cls_spec.cls.__doc__, 'Class A')
897
+ self.assertEqual(
898
+ cls_spec.cls.__schema__['x'],
899
+ vs.Field('x', vs.Int(default=1), 'field x')
900
+ )
901
+ b_cls = cls_spec.cls.__schema__['y'].value.cls
902
+ self.assertEqual(b_cls.__name__, 'B')
903
+ self.assertEqual(b_cls.__doc__, 'Class B')
904
+ self.assertEqual(b_cls.__schema__['z'], vs.Field('z', vs.Str()))
905
+
906
+ with self.assertRaisesRegex(
907
+ ValueError, 'Reference .* not defined'
908
+ ):
909
+ self._cls_value_spec(
910
+ {
911
+ '$defs': {
912
+ 'A': {
913
+ 'type': 'object',
914
+ 'title': 'A',
915
+ 'description': 'Class A',
916
+ 'properties': {
917
+ 'x': {
918
+ '$ref': '#/$defs/B',
919
+ }
920
+ },
921
+ 'required': ['x'],
922
+ 'additionalProperties': False,
923
+ },
924
+ # B should go before A.
925
+ 'B': {
926
+ 'type': 'object',
927
+ 'title': 'B',
928
+ 'description': 'Class B',
929
+ 'properties': {
930
+ 'z': {
931
+ 'type': 'string',
932
+ },
933
+ },
934
+ 'required': ['z'],
935
+ 'additionalProperties': False,
936
+ },
937
+ },
938
+ '$ref': '#/$defs/A',
939
+ }
940
+ )
941
+
942
+ def test_unsupported_json_schema(self):
943
+ with self.assertRaisesRegex(
944
+ ValueError, 'Unsupported type .* in JSON schema'
945
+ ):
946
+ vs.ValueSpec.from_json_schema({'type': 'oneOf'})
947
+
948
+ def test_schema_from_json_schema(self):
949
+ schema = vs.Schema.from_json_schema(
950
+ {
951
+ 'type': 'object',
952
+ 'title': 'A',
953
+ 'description': 'Class A',
954
+ 'properties': {
955
+ 'x': {
956
+ 'type': 'integer',
957
+ },
958
+ },
959
+ 'required': ['x'],
960
+ 'additionalProperties': False,
961
+ },
962
+ )
963
+ self.assertEqual(schema.description, 'Class A')
964
+ self.assertEqual(list(schema.fields.keys()), ['x'])
965
+ self.assertEqual(schema.fields['x'].value, vs.Int())
966
+
967
+ with self.assertRaisesRegex(
968
+ ValueError, 'JSON schema is not an object type'
969
+ ):
970
+ vs.Schema.from_json_schema({'type': 'integer'})
971
+
476
972
  if __name__ == '__main__':
477
973
  unittest.main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyglove
3
- Version: 0.5.0.dev202510140809
3
+ Version: 0.5.0.dev202510160810
4
4
  Summary: PyGlove: A library for manipulating Python objects.
5
5
  Home-page: https://github.com/google/pyglove
6
6
  Author: PyGlove Authors
@@ -120,13 +120,13 @@ pyglove/core/typing/callable_ext.py,sha256=PiBQWPeUAH7Lgmf2xKCZqgK7N0OSrTdbnEkV8
120
120
  pyglove/core/typing/callable_ext_test.py,sha256=TnWKU4_ZjvpbHZFtFHgFvCMDiCos8VmLlODcM_7Xg8M,10156
121
121
  pyglove/core/typing/callable_signature.py,sha256=DRpt7aShfkn8pb3SCiZzS_27eHbkQ_d2UB8BUhJjs0Q,27176
122
122
  pyglove/core/typing/callable_signature_test.py,sha256=iQmHsKPhJPQlMikDhEyxKyq7yWyXI9juKCLYgKhrH3U,25145
123
- pyglove/core/typing/class_schema.py,sha256=3U92-Suwdxlc8mXjK6Tqa9iiqogtRuv1YZwVYpmkMTc,55288
123
+ pyglove/core/typing/class_schema.py,sha256=jICxKo6Ubu35FJO1ei6iOqHB9I204Wtea5UGng2_hTE,55897
124
124
  pyglove/core/typing/class_schema_test.py,sha256=sJkE7ndDSIKb0EUcjZiVFOeJYDI7Hdu2GdPJCMgZxrI,29556
125
125
  pyglove/core/typing/custom_typing.py,sha256=qdnIKHWNt5kZAAFdpQXra8bBu6RljMbbJ_YDG2mhAUA,2205
126
126
  pyglove/core/typing/inspect.py,sha256=VLSz1KAunNm2hx0eEMjiwxKLl9FHlKr9nHelLT25iEA,7726
127
127
  pyglove/core/typing/inspect_test.py,sha256=xclevobF0X8c_B5b1q1dkBJZN1TsVA1RUhk5l25DUCM,10248
128
- pyglove/core/typing/json_schema.py,sha256=shHElSIpw6AZ5L1iKrKzX3yZXgGKYFJOvMC-syEf-i8,10453
129
- pyglove/core/typing/json_schema_test.py,sha256=DhtyoWLoYzCS-DU67QuwaZKZ-9WgK_ZFVsiFp94Ltx8,13011
128
+ pyglove/core/typing/json_schema.py,sha256=F7uWXN8Tk4cAydh8FE9jncaP_n7KEvgprvNHeagTvqE,17871
129
+ pyglove/core/typing/json_schema_test.py,sha256=ZxMO2xgKiELNDzoQ84cmXsyCtFA0Ltn1I4gZjF-3efY,26482
130
130
  pyglove/core/typing/key_specs.py,sha256=-7xjCuUGoQgD0sMafsRFNlw3S4f1r-7t5OO4ev5bbeI,9225
131
131
  pyglove/core/typing/key_specs_test.py,sha256=5zornpyHMAYoRaG8KDXHiQ3obu9UfRp3399lBeUNTPk,6499
132
132
  pyglove/core/typing/pytype_support.py,sha256=lyX11WVbCwoOi5tTQ90pEOS-yvo_6iEi7Lxbp-nXu2A,2069
@@ -218,8 +218,8 @@ pyglove/ext/scalars/randoms.py,sha256=LkMIIx7lOq_lvJvVS3BrgWGuWl7Pi91-lA-O8x_gZs
218
218
  pyglove/ext/scalars/randoms_test.py,sha256=nEhiqarg8l_5EOucp59CYrpO2uKxS1pe0hmBdZUzRNM,2000
219
219
  pyglove/ext/scalars/step_wise.py,sha256=IDw3tuTpv0KVh7AN44W43zqm1-E0HWPUlytWOQC9w3Y,3789
220
220
  pyglove/ext/scalars/step_wise_test.py,sha256=TL1vJ19xVx2t5HKuyIzGoogF7N3Rm8YhLE6JF7i0iy8,2540
221
- pyglove-0.5.0.dev202510140809.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
222
- pyglove-0.5.0.dev202510140809.dist-info/METADATA,sha256=HEmZUREDV5KgclOelE6hLhjYLYXbUh53zH8825zAWII,7089
223
- pyglove-0.5.0.dev202510140809.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
224
- pyglove-0.5.0.dev202510140809.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
225
- pyglove-0.5.0.dev202510140809.dist-info/RECORD,,
221
+ pyglove-0.5.0.dev202510160810.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
222
+ pyglove-0.5.0.dev202510160810.dist-info/METADATA,sha256=65NWYsHR82HetqC5evpjVKFUh_P8ARXiZkKLwu5MOss,7089
223
+ pyglove-0.5.0.dev202510160810.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
224
+ pyglove-0.5.0.dev202510160810.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
225
+ pyglove-0.5.0.dev202510160810.dist-info/RECORD,,