pyglove 0.5.0.dev202508250811__py3-none-any.whl → 0.5.0.dev202511300809__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. pyglove/core/__init__.py +8 -1
  2. pyglove/core/geno/base.py +7 -3
  3. pyglove/core/io/file_system.py +295 -2
  4. pyglove/core/io/file_system_test.py +291 -0
  5. pyglove/core/logging.py +45 -1
  6. pyglove/core/logging_test.py +12 -21
  7. pyglove/core/monitoring.py +657 -0
  8. pyglove/core/monitoring_test.py +289 -0
  9. pyglove/core/symbolic/__init__.py +7 -0
  10. pyglove/core/symbolic/base.py +89 -35
  11. pyglove/core/symbolic/base_test.py +3 -3
  12. pyglove/core/symbolic/dict.py +31 -12
  13. pyglove/core/symbolic/dict_test.py +49 -0
  14. pyglove/core/symbolic/list.py +17 -3
  15. pyglove/core/symbolic/list_test.py +24 -2
  16. pyglove/core/symbolic/object.py +3 -1
  17. pyglove/core/symbolic/object_test.py +13 -10
  18. pyglove/core/symbolic/ref.py +19 -7
  19. pyglove/core/symbolic/ref_test.py +94 -7
  20. pyglove/core/symbolic/unknown_symbols.py +147 -0
  21. pyglove/core/symbolic/unknown_symbols_test.py +100 -0
  22. pyglove/core/typing/annotation_conversion.py +8 -1
  23. pyglove/core/typing/annotation_conversion_test.py +14 -19
  24. pyglove/core/typing/class_schema.py +24 -1
  25. pyglove/core/typing/json_schema.py +221 -8
  26. pyglove/core/typing/json_schema_test.py +508 -12
  27. pyglove/core/typing/type_conversion.py +17 -3
  28. pyglove/core/typing/type_conversion_test.py +7 -2
  29. pyglove/core/typing/value_specs.py +5 -1
  30. pyglove/core/typing/value_specs_test.py +5 -0
  31. pyglove/core/utils/__init__.py +2 -0
  32. pyglove/core/utils/contextual.py +9 -4
  33. pyglove/core/utils/contextual_test.py +10 -0
  34. pyglove/core/utils/error_utils.py +59 -25
  35. pyglove/core/utils/json_conversion.py +360 -63
  36. pyglove/core/utils/json_conversion_test.py +146 -13
  37. pyglove/core/views/html/controls/tab.py +33 -0
  38. pyglove/core/views/html/controls/tab_test.py +37 -0
  39. pyglove/ext/evolution/base_test.py +1 -1
  40. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/METADATA +8 -1
  41. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/RECORD +44 -40
  42. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/WHEEL +0 -0
  43. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/licenses/LICENSE +0 -0
  44. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/top_level.txt +0 -0
@@ -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.
@@ -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