pyglove 0.4.5.dev202411132359__py3-none-any.whl → 0.4.5.dev202501250807__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 (118) hide show
  1. pyglove/core/__init__.py +40 -21
  2. pyglove/core/coding/__init__.py +42 -0
  3. pyglove/core/coding/errors.py +111 -0
  4. pyglove/core/coding/errors_test.py +98 -0
  5. pyglove/core/coding/execution.py +312 -0
  6. pyglove/core/coding/execution_test.py +333 -0
  7. pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
  8. pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
  9. pyglove/core/coding/parsing.py +153 -0
  10. pyglove/core/coding/parsing_test.py +150 -0
  11. pyglove/core/coding/permissions.py +100 -0
  12. pyglove/core/coding/permissions_test.py +93 -0
  13. pyglove/core/geno/base.py +53 -38
  14. pyglove/core/geno/base_test.py +2 -4
  15. pyglove/core/geno/categorical.py +36 -27
  16. pyglove/core/geno/custom.py +18 -15
  17. pyglove/core/geno/numerical.py +19 -16
  18. pyglove/core/geno/space.py +3 -4
  19. pyglove/core/hyper/base.py +6 -6
  20. pyglove/core/hyper/categorical.py +91 -52
  21. pyglove/core/hyper/custom.py +7 -7
  22. pyglove/core/hyper/custom_test.py +9 -10
  23. pyglove/core/hyper/derived.py +30 -22
  24. pyglove/core/hyper/derived_test.py +3 -5
  25. pyglove/core/hyper/dynamic_evaluation.py +3 -4
  26. pyglove/core/hyper/evolvable.py +57 -46
  27. pyglove/core/hyper/numerical.py +48 -24
  28. pyglove/core/hyper/numerical_test.py +9 -9
  29. pyglove/core/hyper/object_template.py +58 -46
  30. pyglove/core/logging_test.py +0 -2
  31. pyglove/core/patching/object_factory.py +4 -4
  32. pyglove/core/patching/pattern_based.py +4 -4
  33. pyglove/core/patching/rule_based.py +4 -3
  34. pyglove/core/symbolic/__init__.py +4 -0
  35. pyglove/core/symbolic/base.py +200 -136
  36. pyglove/core/symbolic/base_test.py +17 -19
  37. pyglove/core/symbolic/boilerplate.py +4 -5
  38. pyglove/core/symbolic/class_wrapper.py +10 -14
  39. pyglove/core/symbolic/class_wrapper_test.py +2 -2
  40. pyglove/core/symbolic/compounding.py +2 -2
  41. pyglove/core/symbolic/compounding_test.py +2 -4
  42. pyglove/core/symbolic/contextual_object.py +288 -0
  43. pyglove/core/symbolic/contextual_object_test.py +327 -0
  44. pyglove/core/symbolic/dict.py +115 -87
  45. pyglove/core/symbolic/dict_test.py +188 -131
  46. pyglove/core/symbolic/diff.py +12 -12
  47. pyglove/core/symbolic/flags.py +1 -1
  48. pyglove/core/symbolic/functor.py +16 -15
  49. pyglove/core/symbolic/functor_test.py +2 -4
  50. pyglove/core/symbolic/inferred.py +2 -2
  51. pyglove/core/symbolic/list.py +70 -47
  52. pyglove/core/symbolic/list_test.py +117 -98
  53. pyglove/core/symbolic/object.py +59 -58
  54. pyglove/core/symbolic/object_test.py +143 -90
  55. pyglove/core/symbolic/origin.py +5 -7
  56. pyglove/core/symbolic/pure_symbolic.py +4 -3
  57. pyglove/core/symbolic/ref.py +33 -16
  58. pyglove/core/symbolic/ref_test.py +17 -0
  59. pyglove/core/tuning/local_backend.py +2 -2
  60. pyglove/core/tuning/protocols.py +3 -3
  61. pyglove/core/typing/annotation_conversion.py +8 -3
  62. pyglove/core/typing/annotation_conversion_test.py +8 -0
  63. pyglove/core/typing/callable_ext.py +11 -13
  64. pyglove/core/typing/callable_signature.py +22 -19
  65. pyglove/core/typing/callable_signature_test.py +3 -5
  66. pyglove/core/typing/class_schema.py +93 -54
  67. pyglove/core/typing/class_schema_test.py +4 -5
  68. pyglove/core/typing/custom_typing.py +5 -4
  69. pyglove/core/typing/key_specs.py +5 -7
  70. pyglove/core/typing/key_specs_test.py +4 -4
  71. pyglove/core/typing/type_conversion.py +4 -5
  72. pyglove/core/typing/type_conversion_test.py +12 -12
  73. pyglove/core/typing/typed_missing.py +6 -7
  74. pyglove/core/typing/typed_missing_test.py +7 -8
  75. pyglove/core/typing/value_specs.py +287 -144
  76. pyglove/core/typing/value_specs_test.py +148 -25
  77. pyglove/core/utils/__init__.py +172 -0
  78. pyglove/core/{object_utils → utils}/common_traits.py +2 -2
  79. pyglove/core/{object_utils → utils}/common_traits_test.py +1 -3
  80. pyglove/core/utils/contextual.py +147 -0
  81. pyglove/core/utils/contextual_test.py +88 -0
  82. pyglove/core/{object_utils → utils}/docstr_utils_test.py +1 -3
  83. pyglove/core/{object_utils → utils}/error_utils.py +3 -3
  84. pyglove/core/{object_utils → utils}/error_utils_test.py +1 -1
  85. pyglove/core/{object_utils → utils}/formatting.py +1 -1
  86. pyglove/core/{object_utils → utils}/formatting_test.py +1 -2
  87. pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
  88. pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
  89. pyglove/core/{object_utils → utils}/json_conversion.py +1 -1
  90. pyglove/core/{object_utils → utils}/json_conversion_test.py +1 -3
  91. pyglove/core/{object_utils → utils}/missing.py +2 -2
  92. pyglove/core/{object_utils → utils}/missing_test.py +2 -4
  93. pyglove/core/utils/text_color.py +128 -0
  94. pyglove/core/utils/text_color_test.py +94 -0
  95. pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
  96. pyglove/core/{object_utils → utils}/timing.py +21 -10
  97. pyglove/core/{object_utils → utils}/timing_test.py +14 -12
  98. pyglove/core/{object_utils → utils}/value_location.py +2 -2
  99. pyglove/core/{object_utils → utils}/value_location_test.py +2 -4
  100. pyglove/core/views/base.py +25 -29
  101. pyglove/core/views/html/base.py +15 -16
  102. pyglove/core/views/html/controls/base.py +46 -9
  103. pyglove/core/views/html/controls/label.py +13 -2
  104. pyglove/core/views/html/controls/label_test.py +27 -8
  105. pyglove/core/views/html/controls/progress_bar.py +3 -5
  106. pyglove/core/views/html/controls/progress_bar_test.py +2 -2
  107. pyglove/core/views/html/controls/tab.py +217 -66
  108. pyglove/core/views/html/controls/tab_test.py +46 -15
  109. pyglove/core/views/html/tree_view.py +39 -37
  110. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/METADATA +17 -3
  111. pyglove-0.4.5.dev202501250807.dist-info/RECORD +218 -0
  112. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/WHEEL +1 -1
  113. pyglove/core/object_utils/__init__.py +0 -164
  114. pyglove-0.4.5.dev202411132359.dist-info/RECORD +0 -203
  115. /pyglove/core/{object_utils → utils}/docstr_utils.py +0 -0
  116. /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
  117. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/LICENSE +0 -0
  118. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/top_level.txt +0 -0
@@ -18,7 +18,7 @@ import inspect
18
18
  import types
19
19
  import typing
20
20
 
21
- from pyglove.core import object_utils
21
+ from pyglove.core import utils
22
22
  from pyglove.core.typing import annotated
23
23
  from pyglove.core.typing import class_schema
24
24
  from pyglove.core.typing import inspect as pg_inspect
@@ -73,7 +73,7 @@ def _value_spec_from_default_value(
73
73
  elif isinstance(value, tuple):
74
74
  value_spec = vs.Tuple(
75
75
  [_value_spec_from_default_value(elem, False) for elem in value])
76
- elif inspect.isfunction(value) or isinstance(value, object_utils.Functor):
76
+ elif inspect.isfunction(value) or isinstance(value, utils.Functor):
77
77
  value_spec = vs.Callable()
78
78
  elif not isinstance(value, type):
79
79
  value_spec = vs.Object(type(value))
@@ -132,7 +132,7 @@ def _value_spec_from_type_annotation(
132
132
  return vs.Union([vs.List(elem), vs.Tuple(elem)])
133
133
  # Handling literals.
134
134
  elif origin is typing.Literal:
135
- return vs.Enum(object_utils.MISSING_VALUE, args)
135
+ return vs.Enum(utils.MISSING_VALUE, args)
136
136
  # Handling dict.
137
137
  elif origin in (dict, typing.Dict, collections.abc.Mapping):
138
138
  if not args:
@@ -177,6 +177,11 @@ def _value_spec_from_type_annotation(
177
177
  if optional:
178
178
  spec = spec.noneable()
179
179
  return spec
180
+ elif origin is typing.Final:
181
+ return _value_spec_from_type_annotation(
182
+ args[0],
183
+ accept_value_as_annotation=False
184
+ ).freeze(vs._FROZEN_VALUE_PLACEHOLDER) # pylint: disable=protected-access
180
185
  elif isinstance(annotation, typing.ForwardRef):
181
186
  annotation = annotation.__forward_arg__
182
187
  if parent_module is not None:
@@ -306,6 +306,14 @@ class ValueSpecFromAnnotationTest(unittest.TestCase):
306
306
  ValueSpec.from_annotation(int | str, True),
307
307
  vs.Union([vs.Int(), vs.Str()]))
308
308
 
309
+ def test_final(self):
310
+ self.assertEqual(
311
+ ValueSpec.from_annotation(
312
+ typing.Final[int], True
313
+ ).set_default(1),
314
+ vs.Int().freeze(1)
315
+ )
316
+
309
317
 
310
318
  if __name__ == '__main__':
311
319
  unittest.main()
@@ -19,14 +19,14 @@ import inspect
19
19
  import types
20
20
  from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Tuple, Union
21
21
 
22
- from pyglove.core import object_utils
22
+ from pyglove.core import utils
23
23
  from pyglove.core.typing import callable_signature
24
24
 
25
25
 
26
26
  _TLS_KEY_PRESET_KWARGS = '__preset_kwargs__'
27
27
 
28
28
 
29
- class PresetArgValue(object_utils.Formattable):
29
+ class PresetArgValue(utils.Formattable):
30
30
  """Value placeholder for arguments whose value will be provided by presets.
31
31
 
32
32
  Example:
@@ -39,12 +39,12 @@ class PresetArgValue(object_utils.Formattable):
39
39
  print(foo(x=1)) # 2: y=1
40
40
  """
41
41
 
42
- def __init__(self, default: Any = object_utils.MISSING_VALUE):
42
+ def __init__(self, default: Any = utils.MISSING_VALUE):
43
43
  self.default = default
44
44
 
45
45
  @property
46
46
  def has_default(self) -> bool:
47
- return self.default != object_utils.MISSING_VALUE
47
+ return self.default != utils.MISSING_VALUE
48
48
 
49
49
  def __eq__(self, other: Any) -> bool:
50
50
  return isinstance(other, PresetArgValue) and (
@@ -55,13 +55,13 @@ class PresetArgValue(object_utils.Formattable):
55
55
  return not self.__eq__(other)
56
56
 
57
57
  def format(self, *args, **kwargs):
58
- return object_utils.kvlist_str(
58
+ return utils.kvlist_str(
59
59
  [
60
- ('default', self.default, object_utils.MISSING_VALUE),
60
+ ('default', self.default, utils.MISSING_VALUE),
61
61
  ],
62
62
  label='PresetArgValue',
63
63
  *args,
64
- **kwargs
64
+ **kwargs,
65
65
  )
66
66
 
67
67
  @classmethod
@@ -182,15 +182,15 @@ def preset_args(
182
182
  Current preset kwargs.
183
183
  """
184
184
 
185
- parent_presets = object_utils.thread_local_peek(
185
+ parent_presets = utils.thread_local_peek(
186
186
  _TLS_KEY_PRESET_KWARGS, _ArgPresets()
187
187
  )
188
188
  current_preset = parent_presets.derive(kwargs, preset_name, inherit_preset)
189
- object_utils.thread_local_push(_TLS_KEY_PRESET_KWARGS, current_preset)
189
+ utils.thread_local_push(_TLS_KEY_PRESET_KWARGS, current_preset)
190
190
  try:
191
191
  yield current_preset
192
192
  finally:
193
- object_utils.thread_local_pop(_TLS_KEY_PRESET_KWARGS, None)
193
+ utils.thread_local_pop(_TLS_KEY_PRESET_KWARGS, None)
194
194
 
195
195
 
196
196
  def enable_preset_args(
@@ -243,9 +243,7 @@ def enable_preset_args(
243
243
  @functools.wraps(func)
244
244
  def _func(*args, **kwargs):
245
245
  # Map positional arguments to keyword arguments.
246
- presets = object_utils.thread_local_peek(
247
- _TLS_KEY_PRESET_KWARGS, None
248
- )
246
+ presets = utils.thread_local_peek(_TLS_KEY_PRESET_KWARGS, None)
249
247
  preset_kwargs = presets.get_preset(preset_name) if presets else {}
250
248
  args, kwargs = PresetArgValue.resolve_args(
251
249
  args, kwargs, positional_arg_names, arg_defaults, preset_kwargs,
@@ -21,7 +21,8 @@ import types
21
21
  import typing
22
22
  from typing import Any, Callable, Dict, List, Optional, Union
23
23
 
24
- from pyglove.core import object_utils
24
+ from pyglove.core import coding
25
+ from pyglove.core import utils
25
26
  from pyglove.core.typing import class_schema
26
27
  from pyglove.core.typing import key_specs as ks
27
28
 
@@ -140,7 +141,7 @@ class CallableType(enum.Enum):
140
141
  METHOD = 2
141
142
 
142
143
 
143
- class Signature(object_utils.Formattable):
144
+ class Signature(utils.Formattable):
144
145
  """PY3 function signature."""
145
146
 
146
147
  def __init__(self,
@@ -256,7 +257,7 @@ class Signature(object_utils.Formattable):
256
257
  **kwargs,
257
258
  ) -> str:
258
259
  """Format current object."""
259
- return object_utils.kvlist_str(
260
+ return utils.kvlist_str(
260
261
  [
261
262
  ('', self.id, ''),
262
263
  ('args', self.args, []),
@@ -270,7 +271,7 @@ class Signature(object_utils.Formattable):
270
271
  compact=compact,
271
272
  verbose=verbose,
272
273
  root_indent=root_indent,
273
- **kwargs
274
+ **kwargs,
274
275
  )
275
276
 
276
277
  def annotate(
@@ -287,7 +288,7 @@ class Signature(object_utils.Formattable):
287
288
  return_value = class_schema.ValueSpec.from_annotation(
288
289
  return_value, auto_typing=True
289
290
  )
290
- if object_utils.MISSING_VALUE != return_value.default:
291
+ if utils.MISSING_VALUE != return_value.default:
291
292
  raise ValueError('return value spec should not have default value.')
292
293
  self.return_value = return_value
293
294
 
@@ -336,12 +337,12 @@ class Signature(object_utils.Formattable):
336
337
  or field.value.default is None
337
338
  ):
338
339
  field.value.set_default(
339
- arg.value_spec.default, root_path=object_utils.KeyPath(arg.name)
340
+ arg.value_spec.default, root_path=utils.KeyPath(arg.name)
340
341
  )
341
342
  if arg.value_spec.default != field.value.default:
342
343
  if field.value.is_noneable and not arg.value_spec.has_default:
343
- # Special handling noneable which always comes with a default.
344
- field.value.set_default(object_utils.MISSING_VALUE)
344
+ # Special handling noneable which always comes with a default.
345
+ field.value.set_default(utils.MISSING_VALUE)
345
346
  elif not (
346
347
  # Special handling Dict type which always has default.
347
348
  isinstance(field.value, class_schema.ValueSpec.DictType)
@@ -548,7 +549,7 @@ class Signature(object_utils.Formattable):
548
549
  if not callable(callable_object):
549
550
  raise TypeError(f'{callable_object!r} is not callable.')
550
551
 
551
- if isinstance(callable_object, object_utils.Functor):
552
+ if isinstance(callable_object, utils.Functor):
552
553
  assert callable_object.__signature__ is not None
553
554
  return callable_object.__signature__
554
555
 
@@ -565,15 +566,15 @@ class Signature(object_utils.Formattable):
565
566
  description = None
566
567
  args_doc = {}
567
568
  if func.__doc__:
568
- cls_doc = object_utils.DocStr.parse(func.__doc__)
569
+ cls_doc = utils.DocStr.parse(func.__doc__)
569
570
  description = cls_doc.short_description
570
571
  args_doc.update(cls_doc.args)
571
572
 
572
573
  if func.__init__.__doc__:
573
- init_doc = object_utils.DocStr.parse(func.__init__.__doc__)
574
+ init_doc = utils.DocStr.parse(func.__init__.__doc__)
574
575
  args_doc.update(init_doc.args)
575
- docstr = object_utils.DocStr(
576
- object_utils.DocStrStyle.GOOGLE,
576
+ docstr = utils.DocStr(
577
+ utils.DocStrStyle.GOOGLE,
577
578
  short_description=description,
578
579
  long_description=None,
579
580
  examples=[],
@@ -592,7 +593,7 @@ class Signature(object_utils.Formattable):
592
593
  else CallableType.FUNCTION
593
594
  )
594
595
  if auto_doc:
595
- docstr = object_utils.docstr(func)
596
+ docstr = utils.docstr(func)
596
597
  sig = inspect.signature(func)
597
598
 
598
599
  module_name = getattr(func, '__module__', None)
@@ -616,7 +617,7 @@ class Signature(object_utils.Formattable):
616
617
  module_name: Optional[str] = None,
617
618
  qualname: Optional[str] = None,
618
619
  auto_typing: bool = False,
619
- docstr: Union[str, object_utils.DocStr, None] = None,
620
+ docstr: Union[str, utils.DocStr, None] = None,
620
621
  parent_module: Optional[types.ModuleType] = None,
621
622
  ) -> 'Signature':
622
623
  """Returns PyGlove signature from Python signature.
@@ -643,7 +644,7 @@ class Signature(object_utils.Formattable):
643
644
  varkw = None
644
645
 
645
646
  if isinstance(docstr, str):
646
- docstr = object_utils.DocStr.parse(docstr)
647
+ docstr = utils.DocStr.parse(docstr)
647
648
 
648
649
  def make_arg_spec(param: inspect.Parameter) -> Argument:
649
650
  """Makes argument spec from inspect.Parameter."""
@@ -707,7 +708,7 @@ class Signature(object_utils.Formattable):
707
708
  force_missing_as_default: bool = False,
708
709
  arg_prefix: str = ''):
709
710
  s = [f'{arg_prefix}{arg_name}']
710
- if arg_spec.annotation != object_utils.MISSING_VALUE:
711
+ if arg_spec.annotation != utils.MISSING_VALUE:
711
712
  s.append(f': _annotation_{arg_name}')
712
713
  exec_locals[f'_annotation_{arg_name}'] = arg_spec.annotation
713
714
  if not arg_prefix and (force_missing_as_default or arg_spec.has_default):
@@ -753,14 +754,16 @@ class Signature(object_utils.Formattable):
753
754
  )
754
755
 
755
756
  # Generate function.
756
- fn = object_utils.make_function(
757
+ fn = coding.make_function(
757
758
  self.name,
758
759
  args=args,
759
760
  body=body,
760
761
  exec_globals=exec_globals,
761
762
  exec_locals=exec_locals,
762
763
  return_type=getattr(
763
- self.return_value, 'annotation', object_utils.MISSING_VALUE))
764
+ self.return_value, 'annotation', coding.NO_TYPE_ANNOTATION
765
+ ),
766
+ )
764
767
  fn.__module__ = self.module_name
765
768
  fn.__name__ = self.name
766
769
  fn.__qualname__ = self.qualname
@@ -11,15 +11,13 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- """Tests for pyglove.core.typing.callable_signature."""
15
-
16
14
  import copy
17
15
  import dataclasses
18
16
  import inspect
19
17
  from typing import List
20
18
  import unittest
21
19
 
22
- from pyglove.core import object_utils
20
+ from pyglove.core import utils
23
21
  from pyglove.core.typing import annotation_conversion # pylint: disable=unused-import
24
22
  from pyglove.core.typing import callable_signature
25
23
  from pyglove.core.typing import class_schema
@@ -628,8 +626,8 @@ class FromCallableTest(unittest.TestCase):
628
626
  self.assertIsNotNone(signature.varkw)
629
627
 
630
628
  def test_signature_with_forward_declarations(self):
631
- signature = callable_signature.signature(object_utils.KeyPath)
632
- self.assertIs(signature.get_value_spec('parent').cls, object_utils.KeyPath)
629
+ signature = callable_signature.signature(utils.KeyPath)
630
+ self.assertIs(signature.get_value_spec('parent').cls, utils.KeyPath)
633
631
 
634
632
 
635
633
  class FromSchemaTest(unittest.TestCase):
@@ -20,10 +20,10 @@ import sys
20
20
  import types
21
21
  from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Type, Union
22
22
 
23
- from pyglove.core import object_utils
23
+ from pyglove.core import utils
24
24
 
25
25
 
26
- class KeySpec(object_utils.Formattable, object_utils.JSONConvertible):
26
+ class KeySpec(utils.Formattable, utils.JSONConvertible):
27
27
  """Interface for key specifications.
28
28
 
29
29
  A key specification determines what keys are acceptable for a symbolic
@@ -94,7 +94,7 @@ class KeySpec(object_utils.Formattable, object_utils.JSONConvertible):
94
94
  assert False, 'Overridden in `key_specs.py`.'
95
95
 
96
96
 
97
- class ForwardRef(object_utils.Formattable):
97
+ class ForwardRef(utils.Formattable):
98
98
  """Forward type reference."""
99
99
 
100
100
  def __init__(self, module: types.ModuleType, name: str):
@@ -147,7 +147,7 @@ class ForwardRef(object_utils.Formattable):
147
147
  **kwargs
148
148
  ) -> str:
149
149
  """Format this object."""
150
- return object_utils.kvlist_str(
150
+ return utils.kvlist_str(
151
151
  [
152
152
  ('module', self.module.__name__, None),
153
153
  ('name', self.name, None),
@@ -180,7 +180,7 @@ class ForwardRef(object_utils.Formattable):
180
180
  return ForwardRef(self.module, self.name)
181
181
 
182
182
 
183
- class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
183
+ class ValueSpec(utils.Formattable, utils.JSONConvertible):
184
184
  """Interface for value specifications.
185
185
 
186
186
  A value specification defines what values are acceptable for a symbolic
@@ -344,6 +344,10 @@ class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
344
344
  Tuple[Type[Any], ...]]: # pyformat: disable
345
345
  """Returns acceptable (resolved) value type(s)."""
346
346
 
347
+ @abc.abstractmethod
348
+ def __call__(self, *args, **kwargs) -> Any:
349
+ """Instantiates a value based on the spec.."""
350
+
347
351
  @property
348
352
  @abc.abstractmethod
349
353
  def forward_refs(self) -> Set[ForwardRef]:
@@ -363,7 +367,7 @@ class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
363
367
  self,
364
368
  default: Any,
365
369
  use_default_apply: bool = True,
366
- root_path: Optional[object_utils.KeyPath] = None
370
+ root_path: Optional[utils.KeyPath] = None,
367
371
  ) -> 'ValueSpec':
368
372
  """Sets the default value and returns `self`.
369
373
 
@@ -394,13 +398,14 @@ class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
394
398
  @property
395
399
  def has_default(self) -> bool:
396
400
  """Returns True if the default value is provided."""
397
- return self.default != object_utils.MISSING_VALUE
401
+ return self.default != utils.MISSING_VALUE
398
402
 
399
403
  @abc.abstractmethod
400
404
  def freeze(
401
405
  self,
402
- permanent_value: Any = object_utils.MISSING_VALUE,
403
- apply_before_use: bool = True) -> 'ValueSpec':
406
+ permanent_value: Any = utils.MISSING_VALUE,
407
+ apply_before_use: bool = True,
408
+ ) -> 'ValueSpec':
404
409
  """Sets the default value using a permanent value and freezes current spec.
405
410
 
406
411
  A frozen value spec will not accept any value that is not the default
@@ -467,10 +472,11 @@ class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
467
472
  self,
468
473
  value: Any,
469
474
  allow_partial: bool = False,
470
- child_transform: Optional[Callable[
471
- [object_utils.KeyPath, 'Field', Any], Any]] = None,
472
- root_path: Optional[object_utils.KeyPath] = None,
473
- ) -> Any:
475
+ child_transform: Optional[
476
+ Callable[[utils.KeyPath, 'Field', Any], Any]
477
+ ] = None,
478
+ root_path: Optional[utils.KeyPath] = None,
479
+ ) -> Any:
474
480
  """Validates, completes and transforms the input value.
475
481
 
476
482
  Here is the procedure of ``apply``::
@@ -547,7 +553,7 @@ class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
547
553
  assert False, 'Overridden in `annotation_conversion.py`.'
548
554
 
549
555
 
550
- class Field(object_utils.Formattable, object_utils.JSONConvertible):
556
+ class Field(utils.Formattable, utils.JSONConvertible):
551
557
  """Class that represents the definition of one or a group of attributes.
552
558
 
553
559
  ``Field`` is held by a :class:`pyglove.Schema` object for defining the
@@ -587,7 +593,9 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
587
593
  key_spec: Union[KeySpec, str],
588
594
  value_spec: ValueSpec,
589
595
  description: Optional[str] = None,
590
- metadata: Optional[Dict[str, Any]] = None):
596
+ metadata: Optional[Dict[str, Any]] = None,
597
+ origin: Optional[Type[Any]] = None,
598
+ ) -> None:
591
599
  """Constructor.
592
600
 
593
601
  Args:
@@ -596,6 +604,7 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
596
604
  value_spec: Value specification of the field.
597
605
  description: Description of the field.
598
606
  metadata: A dict of objects as metadata for the field.
607
+ origin: The class that this field originates from.
599
608
 
600
609
  Raises:
601
610
  ValueError: metadata is not a dict.
@@ -606,6 +615,7 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
606
615
  self._key = key_spec
607
616
  self._value = value_spec
608
617
  self._description = description
618
+ self._origin = origin
609
619
 
610
620
  if metadata and not isinstance(metadata, dict):
611
621
  raise ValueError('metadata must be a dict.')
@@ -661,6 +671,15 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
661
671
  """
662
672
  return self._metadata
663
673
 
674
+ @property
675
+ def origin(self) -> Optional[Type[Any]]:
676
+ """The class that this field originates from."""
677
+ return self._origin
678
+
679
+ def set_origin(self, origin: Type[Any]) -> None:
680
+ """Sets the origin (source class) of this field."""
681
+ self._origin = origin
682
+
664
683
  def extend(self, base_field: 'Field') -> 'Field':
665
684
  """Extend current field based on a base field."""
666
685
  self.key.extend(base_field.key)
@@ -677,9 +696,11 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
677
696
  self,
678
697
  value: Any,
679
698
  allow_partial: bool = False,
680
- transform_fn: Optional[Callable[
681
- [object_utils.KeyPath, 'Field', Any], Any]] = None,
682
- root_path: Optional[object_utils.KeyPath] = None) -> Any:
699
+ transform_fn: Optional[
700
+ Callable[[utils.KeyPath, 'Field', Any], Any]
701
+ ] = None,
702
+ root_path: Optional[utils.KeyPath] = None,
703
+ ) -> Any:
683
704
  """Apply current field to a value, which validate and complete the value.
684
705
 
685
706
  Args:
@@ -731,18 +752,19 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
731
752
  **kwargs,
732
753
  ) -> str:
733
754
  """Format this field into a string."""
734
- return object_utils.kvlist_str(
755
+ return utils.kvlist_str(
735
756
  [
736
757
  ('key', self._key, None),
737
758
  ('value', self._value, None),
738
759
  ('description', self._description, None),
739
760
  ('metadata', self._metadata, {}),
761
+ ('origin', self._origin, None),
740
762
  ],
741
763
  label=self.__class__.__name__,
742
764
  compact=compact,
743
765
  verbose=verbose,
744
766
  root_indent=root_indent,
745
- **kwargs
767
+ **kwargs,
746
768
  )
747
769
 
748
770
  def to_json(self, **kwargs: Any) -> Dict[str, Any]:
@@ -771,7 +793,7 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
771
793
  return not self.__eq__(other)
772
794
 
773
795
 
774
- class Schema(object_utils.Formattable, object_utils.JSONConvertible):
796
+ class Schema(utils.Formattable, utils.JSONConvertible):
775
797
  """Class that represents a schema.
776
798
 
777
799
  PyGlove's runtime type system is based on the concept of ``Schema`` (
@@ -861,7 +883,9 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
861
883
  description: Optional[str] = None,
862
884
  *,
863
885
  allow_nonconst_keys: bool = False,
864
- metadata: Optional[Dict[str, Any]] = None):
886
+ metadata: Optional[Dict[str, Any]] = None,
887
+ for_cls: Optional[Type[Any]] = None,
888
+ ):
865
889
  """Constructor.
866
890
 
867
891
  Args:
@@ -874,6 +898,7 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
874
898
  description: Optional str as the description for the schema.
875
899
  allow_nonconst_keys: Whether immediate fields can use non-const keys.
876
900
  metadata: Optional dict of user objects as schema-level metadata.
901
+ for_cls: Optional class that this schema applies to.
877
902
 
878
903
  Raises:
879
904
  TypeError: Argument `fields` is not a list.
@@ -895,6 +920,11 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
895
920
  self._description = description
896
921
  self._metadata = metadata or {}
897
922
 
923
+ if for_cls is not None:
924
+ for f in fields:
925
+ if f.origin is None:
926
+ f.set_origin(for_cls)
927
+
898
928
  self._dynamic_field = None
899
929
  for f in fields:
900
930
  if not f.key.is_const:
@@ -930,21 +960,27 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
930
960
  Returns:
931
961
  The merged schema.
932
962
  """
933
- field_names = set()
934
- fields = []
963
+ fields = {}
935
964
  kw_field = None
936
965
  for schema in schema_list:
937
966
  for key, field in schema.fields.items():
938
- if key.is_const and key not in field_names:
939
- fields.append(field)
940
- field_names.add(key)
941
- elif not key.is_const and kw_field is None:
967
+ if key.is_const:
968
+ if key not in fields or (
969
+ field.origin is not None
970
+ and fields[key].origin is not None
971
+ and issubclass(field.origin, fields[key].origin)
972
+ ):
973
+ fields[key] = field
974
+ elif kw_field is None:
942
975
  kw_field = field
943
976
 
944
977
  if kw_field is not None:
945
- fields.append(kw_field)
978
+ fields[kw_field.key] = kw_field
946
979
  return Schema(
947
- fields, name=name, description=description, allow_nonconst_keys=True
980
+ list(fields.values()),
981
+ name=name,
982
+ description=description,
983
+ allow_nonconst_keys=True
948
984
  )
949
985
 
950
986
  def extend(self, base: 'Schema') -> 'Schema':
@@ -955,13 +991,12 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
955
991
  parent_field: Field,
956
992
  child_field: Field) -> Field:
957
993
  """Merge function on field with the same key."""
958
- if parent_field != object_utils.MISSING_VALUE:
959
- if object_utils.MISSING_VALUE == child_field:
994
+ if parent_field != utils.MISSING_VALUE:
995
+ if utils.MISSING_VALUE == child_field:
960
996
  if (not self._allow_nonconst_keys and not parent_field.key.is_const):
961
- hints = object_utils.kvlist_str([
962
- ('base', base.name, None),
963
- ('path', path, None)
964
- ])
997
+ hints = utils.kvlist_str(
998
+ [('base', base.name, None), ('path', path, None)]
999
+ )
965
1000
  raise ValueError(
966
1001
  f'Non-const key {parent_field.key} is not allowed to be '
967
1002
  f'added to the schema. ({hints})')
@@ -970,16 +1005,15 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
970
1005
  try:
971
1006
  child_field.extend(parent_field)
972
1007
  except Exception as e: # pylint: disable=broad-except
973
- hints = object_utils.kvlist_str([
974
- ('base', base.name, None),
975
- ('path', path, None)
976
- ])
1008
+ hints = utils.kvlist_str(
1009
+ [('base', base.name, None), ('path', path, None)]
1010
+ )
977
1011
  raise e.__class__(f'{e} ({hints})').with_traceback(
978
1012
  sys.exc_info()[2])
979
1013
  return child_field
980
1014
 
981
- self._fields = object_utils.merge([base.fields, self.fields], _merge_field)
982
- self._metadata = object_utils.merge([base.metadata, self.metadata])
1015
+ self._fields = utils.merge([base.fields, self.fields], _merge_field)
1016
+ self._metadata = utils.merge([base.metadata, self.metadata])
983
1017
 
984
1018
  # Inherit dynamic field from base if it's not present in the child.
985
1019
  if self._dynamic_field is None:
@@ -1102,8 +1136,8 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
1102
1136
  dict_obj: Dict[str, Any],
1103
1137
  allow_partial: bool = False,
1104
1138
  child_transform: Optional[Callable[
1105
- [object_utils.KeyPath, Field, Any], Any]] = None,
1106
- root_path: Optional[object_utils.KeyPath] = None,
1139
+ [utils.KeyPath, Field, Any], Any]] = None,
1140
+ root_path: Optional[utils.KeyPath] = None,
1107
1141
  ) -> Dict[str, Any]: # pyformat: disable
1108
1142
  # pyformat: disable
1109
1143
  """Apply this schema to a dict object, validate and transform it.
@@ -1160,18 +1194,18 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
1160
1194
  keys.append(str(key_spec))
1161
1195
  for key in keys:
1162
1196
  if dict_obj:
1163
- value = dict_obj.get(key, object_utils.MISSING_VALUE)
1197
+ value = dict_obj.get(key, utils.MISSING_VALUE)
1164
1198
  else:
1165
- value = object_utils.MISSING_VALUE
1199
+ value = utils.MISSING_VALUE
1166
1200
  # NOTE(daiyip): field.default_value may be MISSING_VALUE too
1167
1201
  # or partial.
1168
- if object_utils.MISSING_VALUE == value:
1202
+ if utils.MISSING_VALUE == value:
1169
1203
  value = copy.deepcopy(field.default_value)
1170
1204
  new_value = field.apply(
1171
1205
  value,
1172
1206
  allow_partial=allow_partial,
1173
1207
  transform_fn=child_transform,
1174
- root_path=object_utils.KeyPath(key, root_path)
1208
+ root_path=utils.KeyPath(key, root_path),
1175
1209
  )
1176
1210
 
1177
1211
  # NOTE(daiyip): `pg.Dict.__getitem__`` has special logics in handling
@@ -1185,10 +1219,12 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
1185
1219
  dict_obj[key] = new_value
1186
1220
  return dict_obj
1187
1221
 
1188
- def validate(self,
1189
- dict_obj: Dict[str, Any],
1190
- allow_partial: bool = False,
1191
- root_path: Optional[object_utils.KeyPath] = None) -> None:
1222
+ def validate(
1223
+ self,
1224
+ dict_obj: Dict[str, Any],
1225
+ allow_partial: bool = False,
1226
+ root_path: Optional[utils.KeyPath] = None,
1227
+ ) -> None:
1192
1228
  """Validates whether dict object is conformed with the schema."""
1193
1229
  self.apply(
1194
1230
  copy.deepcopy(dict_obj),
@@ -1253,12 +1289,12 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
1253
1289
  root_indent: int = 0,
1254
1290
  *,
1255
1291
  cls_name: Optional[str] = None,
1256
- bracket_type: object_utils.BracketType = object_utils.BracketType.ROUND,
1292
+ bracket_type: utils.BracketType = utils.BracketType.ROUND,
1257
1293
  fields_only: bool = False,
1258
1294
  **kwargs,
1259
1295
  ) -> str:
1260
1296
  """Format current Schema into nicely printed string."""
1261
- return object_utils.kvlist_str(
1297
+ return utils.kvlist_str(
1262
1298
  [
1263
1299
  ('name', self.name, None),
1264
1300
  ('description', self.description, None),
@@ -1407,6 +1443,7 @@ def create_schema(
1407
1443
  allow_nonconst_keys: bool = False,
1408
1444
  metadata: Optional[Dict[str, Any]] = None,
1409
1445
  description: Optional[str] = None,
1446
+ for_cls: Optional[Type[Any]] = None,
1410
1447
  parent_module: Optional[types.ModuleType] = None
1411
1448
  ) -> Schema:
1412
1449
  """Creates ``Schema`` from a list of ``Field``s or equivalences.
@@ -1448,6 +1485,7 @@ def create_schema(
1448
1485
  allow_nonconst_keys: Whether to allow non const keys in schema.
1449
1486
  metadata: Optional dict of user objects as schema-level metadata.
1450
1487
  description: Optional description of the schema.
1488
+ for_cls: (Optional) the class that this schema applies to.
1451
1489
  parent_module: (Optional) parent module for defining this schema, which will
1452
1490
  be used for forward reference lookup.
1453
1491
 
@@ -1471,6 +1509,7 @@ def create_schema(
1471
1509
  allow_nonconst_keys=allow_nonconst_keys,
1472
1510
  metadata=metadata,
1473
1511
  description=description,
1512
+ for_cls=for_cls,
1474
1513
  )
1475
1514
 
1476
1515