pyglove 0.4.5.dev20240319__py3-none-any.whl → 0.4.5.dev202501132210__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 (145) hide show
  1. pyglove/core/__init__.py +54 -20
  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 +309 -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 +54 -41
  14. pyglove/core/geno/base_test.py +2 -4
  15. pyglove/core/geno/categorical.py +37 -28
  16. pyglove/core/geno/custom.py +19 -16
  17. pyglove/core/geno/numerical.py +20 -17
  18. pyglove/core/geno/space.py +4 -5
  19. pyglove/core/hyper/base.py +6 -6
  20. pyglove/core/hyper/categorical.py +94 -55
  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 +2 -4
  25. pyglove/core/hyper/dynamic_evaluation.py +5 -6
  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/io/__init__.py +1 -0
  31. pyglove/core/io/file_system.py +17 -7
  32. pyglove/core/io/file_system_test.py +2 -0
  33. pyglove/core/io/sequence.py +299 -0
  34. pyglove/core/io/sequence_test.py +124 -0
  35. pyglove/core/logging_test.py +0 -2
  36. pyglove/core/patching/object_factory.py +4 -4
  37. pyglove/core/patching/pattern_based.py +4 -4
  38. pyglove/core/patching/rule_based.py +17 -5
  39. pyglove/core/patching/rule_based_test.py +27 -4
  40. pyglove/core/symbolic/__init__.py +2 -7
  41. pyglove/core/symbolic/base.py +320 -183
  42. pyglove/core/symbolic/base_test.py +123 -19
  43. pyglove/core/symbolic/boilerplate.py +7 -13
  44. pyglove/core/symbolic/boilerplate_test.py +25 -23
  45. pyglove/core/symbolic/class_wrapper.py +48 -45
  46. pyglove/core/symbolic/class_wrapper_test.py +2 -2
  47. pyglove/core/symbolic/compounding.py +9 -15
  48. pyglove/core/symbolic/compounding_test.py +2 -4
  49. pyglove/core/symbolic/dict.py +154 -110
  50. pyglove/core/symbolic/dict_test.py +238 -130
  51. pyglove/core/symbolic/diff.py +199 -10
  52. pyglove/core/symbolic/diff_test.py +226 -0
  53. pyglove/core/symbolic/flags.py +1 -1
  54. pyglove/core/symbolic/functor.py +29 -26
  55. pyglove/core/symbolic/functor_test.py +102 -50
  56. pyglove/core/symbolic/inferred.py +2 -2
  57. pyglove/core/symbolic/list.py +81 -50
  58. pyglove/core/symbolic/list_test.py +119 -97
  59. pyglove/core/symbolic/object.py +225 -113
  60. pyglove/core/symbolic/object_test.py +320 -108
  61. pyglove/core/symbolic/origin.py +17 -14
  62. pyglove/core/symbolic/origin_test.py +4 -2
  63. pyglove/core/symbolic/pure_symbolic.py +4 -3
  64. pyglove/core/symbolic/ref.py +108 -21
  65. pyglove/core/symbolic/ref_test.py +93 -0
  66. pyglove/core/symbolic/symbolize_test.py +10 -2
  67. pyglove/core/tuning/local_backend.py +2 -2
  68. pyglove/core/tuning/protocols.py +3 -3
  69. pyglove/core/tuning/sample_test.py +3 -3
  70. pyglove/core/typing/__init__.py +14 -5
  71. pyglove/core/typing/annotation_conversion.py +43 -27
  72. pyglove/core/typing/annotation_conversion_test.py +23 -0
  73. pyglove/core/typing/callable_ext.py +241 -3
  74. pyglove/core/typing/callable_ext_test.py +255 -0
  75. pyglove/core/typing/callable_signature.py +510 -66
  76. pyglove/core/typing/callable_signature_test.py +619 -99
  77. pyglove/core/typing/class_schema.py +229 -154
  78. pyglove/core/typing/class_schema_test.py +149 -95
  79. pyglove/core/typing/custom_typing.py +5 -4
  80. pyglove/core/typing/inspect.py +63 -0
  81. pyglove/core/typing/inspect_test.py +39 -0
  82. pyglove/core/typing/key_specs.py +10 -11
  83. pyglove/core/typing/key_specs_test.py +7 -4
  84. pyglove/core/typing/type_conversion.py +4 -5
  85. pyglove/core/typing/type_conversion_test.py +12 -12
  86. pyglove/core/typing/typed_missing.py +6 -7
  87. pyglove/core/typing/typed_missing_test.py +7 -8
  88. pyglove/core/typing/value_specs.py +604 -362
  89. pyglove/core/typing/value_specs_test.py +328 -90
  90. pyglove/core/utils/__init__.py +164 -0
  91. pyglove/core/{object_utils → utils}/common_traits.py +3 -67
  92. pyglove/core/utils/common_traits_test.py +36 -0
  93. pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
  94. pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
  95. pyglove/core/{object_utils → utils}/error_utils.py +78 -9
  96. pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
  97. pyglove/core/utils/formatting.py +464 -0
  98. pyglove/core/utils/formatting_test.py +453 -0
  99. pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
  100. pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
  101. pyglove/core/{object_utils → utils}/json_conversion.py +177 -52
  102. pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
  103. pyglove/core/{object_utils → utils}/missing.py +3 -3
  104. pyglove/core/{object_utils → utils}/missing_test.py +2 -4
  105. pyglove/core/utils/text_color.py +128 -0
  106. pyglove/core/utils/text_color_test.py +94 -0
  107. pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
  108. pyglove/core/utils/timing.py +236 -0
  109. pyglove/core/utils/timing_test.py +154 -0
  110. pyglove/core/{object_utils → utils}/value_location.py +275 -6
  111. pyglove/core/utils/value_location_test.py +707 -0
  112. pyglove/core/views/__init__.py +32 -0
  113. pyglove/core/views/base.py +804 -0
  114. pyglove/core/views/base_test.py +580 -0
  115. pyglove/core/views/html/__init__.py +27 -0
  116. pyglove/core/views/html/base.py +547 -0
  117. pyglove/core/views/html/base_test.py +830 -0
  118. pyglove/core/views/html/controls/__init__.py +35 -0
  119. pyglove/core/views/html/controls/base.py +275 -0
  120. pyglove/core/views/html/controls/label.py +207 -0
  121. pyglove/core/views/html/controls/label_test.py +157 -0
  122. pyglove/core/views/html/controls/progress_bar.py +183 -0
  123. pyglove/core/views/html/controls/progress_bar_test.py +97 -0
  124. pyglove/core/views/html/controls/tab.py +320 -0
  125. pyglove/core/views/html/controls/tab_test.py +87 -0
  126. pyglove/core/views/html/controls/tooltip.py +99 -0
  127. pyglove/core/views/html/controls/tooltip_test.py +99 -0
  128. pyglove/core/views/html/tree_view.py +1517 -0
  129. pyglove/core/views/html/tree_view_test.py +1461 -0
  130. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/METADATA +18 -4
  131. pyglove-0.4.5.dev202501132210.dist-info/RECORD +214 -0
  132. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/WHEEL +1 -1
  133. pyglove/core/object_utils/__init__.py +0 -154
  134. pyglove/core/object_utils/common_traits_test.py +0 -82
  135. pyglove/core/object_utils/formatting.py +0 -234
  136. pyglove/core/object_utils/formatting_test.py +0 -223
  137. pyglove/core/object_utils/value_location_test.py +0 -385
  138. pyglove/core/symbolic/schema_utils.py +0 -327
  139. pyglove/core/symbolic/schema_utils_test.py +0 -57
  140. pyglove/core/typing/class_schema_utils.py +0 -202
  141. pyglove/core/typing/class_schema_utils_test.py +0 -194
  142. pyglove-0.4.5.dev20240319.dist-info/RECORD +0 -185
  143. /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
  144. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
  145. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/top_level.txt +0 -0
@@ -17,15 +17,14 @@ import abc
17
17
  import functools
18
18
  import inspect
19
19
  import typing
20
- from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union
20
+ from typing import Any, Dict, Iterator, List, Optional, Sequence, Union
21
21
 
22
- from pyglove.core import logging
23
- from pyglove.core import object_utils
22
+ from pyglove.core import coding
24
23
  from pyglove.core import typing as pg_typing
24
+ from pyglove.core import utils
25
25
  from pyglove.core.symbolic import base
26
26
  from pyglove.core.symbolic import dict as pg_dict
27
27
  from pyglove.core.symbolic import flags
28
- from pyglove.core.symbolic import schema_utils
29
28
 
30
29
 
31
30
  class ObjectMeta(abc.ABCMeta):
@@ -50,44 +49,56 @@ class ObjectMeta(abc.ABCMeta):
50
49
  """
51
50
  return f'{cls.__module__}.{cls.__qualname__}'
52
51
 
53
- def __getattr__(cls, name):
54
- # NOTE(daiyip): For backward compatibility, we allows these names to
55
- # be used as aliases to the canonical names if users do not override them.
56
- if name == 'schema':
57
- logging.warning(
58
- '`pg.Object.schema` is deprecated and will be removed in future. '
59
- 'Please use `__schema__` instead.')
60
- return cls.__schema__
61
- elif name == 'type_name':
62
- logging.warning(
63
- '`pg.Object.type_name` is deprecated and will be removed in future. '
64
- 'Please use `__type_name__` instead.')
65
- return cls.__type_name__
66
- raise AttributeError(name)
67
-
68
52
  @property
69
53
  def init_arg_list(cls) -> List[str]:
70
54
  """Gets __init__ positional argument list."""
71
55
  return typing.cast(List[str], cls.__schema__.metadata['init_arg_list'])
72
56
 
73
- def apply_schema(cls, schema: pg_typing.Schema) -> None:
57
+ def apply_schema(cls, schema: Optional[pg_typing.Schema] = None) -> None:
74
58
  """Applies a schema to a symbolic class.
75
59
 
76
60
  Args:
77
61
  schema: The schema that will be applied to class. If `cls` was attached
78
- with an existing schema. The old schema will be dropped.
62
+ with an existing schema. The old schema will be dropped. If None, the
63
+ cls will update its signature and getters according to the (maybe
64
+ updated) old schema.
79
65
  """
80
- setattr(cls, '__schema__', schema)
81
- setattr(cls, '__sym_fields', pg_typing.Dict(schema))
66
+ # Formalize schema first.
67
+ if schema is not None:
68
+ schema = cls._normalize_schema(schema) # pytype: disable=attribute-error
69
+ setattr(cls, '__schema__', schema)
70
+ setattr(cls, '__sym_fields', pg_typing.Dict(schema))
82
71
 
83
- # Update `init_arg_list`` based on the updated schema.
84
- init_arg_list = schema.metadata.get('init_arg_list', None)
85
- if init_arg_list is None:
86
- init_arg_list = schema_utils.auto_init_arg_list(cls)
87
- cls.__schema__.metadata['init_arg_list'] = init_arg_list
88
- schema_utils.validate_init_arg_list(init_arg_list, cls.__schema__)
89
72
  cls._on_schema_update() # pytype: disable=attribute-error
90
73
 
74
+ def update_schema(
75
+ cls,
76
+ fields: List[
77
+ Union[
78
+ pg_typing.Field,
79
+ List[pg_typing.FieldDef],
80
+ Dict[pg_typing.FieldKeyDef, pg_typing.FieldValueDef],
81
+ ]
82
+ ],
83
+ extend: bool = True,
84
+ *,
85
+ init_arg_list: Optional[Sequence[str]] = None,
86
+ metadata: Optional[Dict[str, Any]] = None,
87
+ ) -> None:
88
+ """Updates the schema of the class."""
89
+ metadata = metadata or {}
90
+ if init_arg_list is None:
91
+ init_arg_list = metadata.pop('init_arg_list', None)
92
+
93
+ metadata['init_arg_list'] = init_arg_list
94
+ schema = pg_typing.create_schema(
95
+ fields=fields,
96
+ base_schema_list=[cls.__schema__] if extend else [],
97
+ allow_nonconst_keys=True,
98
+ metadata=metadata,
99
+ )
100
+ cls.apply_schema(schema)
101
+
91
102
  def register_for_deserialization(
92
103
  cls,
93
104
  serialization_key: Optional[str] = None,
@@ -105,7 +116,7 @@ class ObjectMeta(abc.ABCMeta):
105
116
 
106
117
  # Register class with 'type' property.
107
118
  for key in serialization_keys:
108
- object_utils.JSONConvertible.register(
119
+ utils.JSONConvertible.register(
109
120
  key, cls, flags.is_repeated_class_registration_allowed()
110
121
  )
111
122
 
@@ -138,33 +149,49 @@ class ObjectMeta(abc.ABCMeta):
138
149
  if key is None:
139
150
  continue
140
151
 
152
+ # Skip class-level attributes that are not symbolic fields.
153
+ if typing.get_origin(attr_annotation) is typing.ClassVar:
154
+ continue
155
+
141
156
  field = pg_typing.Field.from_annotation(key, attr_annotation)
142
157
  if isinstance(key, pg_typing.ConstStrKey):
143
158
  attr_value = cls.__dict__.get(attr_name, pg_typing.MISSING_VALUE)
144
159
  if attr_value != pg_typing.MISSING_VALUE:
145
160
  field.value.set_default(attr_value)
161
+
162
+ if (field.value.frozen and
163
+ field.value.default is
164
+ pg_typing.value_specs._FROZEN_VALUE_PLACEHOLDER): # pylint: disable=protected-access
165
+ raise TypeError(
166
+ f'Field {field.key!r} is marked as final but has no default value.'
167
+ )
146
168
  fields.append(field)
147
169
 
148
170
  # Trigger event so subclass could modify the fields.
149
171
  fields = cls._end_annotation_inference(fields) # pytype: disable=attribute-error
150
172
  return fields
151
173
 
152
- def _update_default_values_from_class_attributes(cls):
153
- """Updates the symbolic attribute defaults from class attributes."""
154
- for field in cls.__schema__.fields.values():
174
+ def _update_default_values_from_class_attributes(
175
+ cls, schema: pg_typing.Schema):
176
+ """Freezes callable fields if their defaults are provided as methods."""
177
+ for field in schema.fields.values():
155
178
  if isinstance(field.key, pg_typing.ConstStrKey):
156
- attr_name = field.key.text
157
- attr_value = cls.__dict__.get(attr_name, pg_typing.MISSING_VALUE)
158
- if (
159
- attr_value != pg_typing.MISSING_VALUE
160
- and not isinstance(attr_value, property)
161
- and (
162
- # This allows class methods to be used as callable
163
- # symbolic attributes.
164
- not inspect.isfunction(attr_value)
165
- or isinstance(field.value, pg_typing.Callable)
166
- )
167
- ):
179
+ attr_value = cls.__dict__.get(field.key.text, pg_typing.MISSING_VALUE)
180
+ if (attr_value == pg_typing.MISSING_VALUE
181
+ or isinstance(attr_value, property)):
182
+ continue
183
+ if inspect.isfunction(attr_value):
184
+ # When users add a method that has the same name as as field, two
185
+ # scenarios emerge. If the field is a callable type, the method will
186
+ # serve as the default value for the field. As a result, we freeze the
187
+ # field so it can't be provided from the constructor. If the field is
188
+ # not a callable type, the symbolic field and the method will coexist,
189
+ # meaning that the method has higher priority when being accessed,
190
+ # while users still can use `sym_getattr` to access the value for the
191
+ # symboic field.
192
+ if isinstance(field.value, pg_typing.Callable):
193
+ field.value.freeze(attr_value, apply_before_use=False)
194
+ else:
168
195
  field.value.set_default(attr_value)
169
196
 
170
197
 
@@ -282,7 +309,7 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
282
309
  Args:
283
310
  user_cls: The source class that calls this class method.
284
311
  """
285
- object_utils.ensure_explicit_method_override(
312
+ utils.ensure_explicit_method_override(
286
313
  cls.__init__,
287
314
  (
288
315
  '`pg.Object.__init__` is a PyGlove managed method. For setting up '
@@ -290,7 +317,8 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
290
317
  '`_on_init()`. If you do have a need to override `__init__` and '
291
318
  'know the implications, please decorate your overridden method '
292
319
  'with `@pg.explicit_method_override`.'
293
- ))
320
+ ),
321
+ )
294
322
 
295
323
  # Set `__serialization_key__` before JSONConvertible.__init_subclass__
296
324
  # is called.
@@ -311,15 +339,16 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
311
339
  base_schema_list.append(base_schema)
312
340
 
313
341
  new_fields = user_cls._infer_fields_from_annotations()
314
- cls_schema = schema_utils.formalize_schema(
315
- pg_typing.create_schema(
316
- new_fields,
317
- name=user_cls.__type_name__,
318
- base_schema_list=base_schema_list,
319
- allow_nonconst_keys=True,
320
- metadata={},
321
- )
342
+ cls_schema = pg_typing.create_schema(
343
+ new_fields,
344
+ base_schema_list=base_schema_list,
345
+ allow_nonconst_keys=True,
346
+ metadata={},
322
347
  )
348
+
349
+ # Freeze callable symbolic attributes if they are provided as methods.
350
+ user_cls._update_default_values_from_class_attributes(cls_schema)
351
+
323
352
  # NOTE(daiyip): When new fields are added through class attributes.
324
353
  # We invalidate `init_arg_list` so PyGlove could recompute it based
325
354
  # on its schema during `apply_schema`. Otherwise, we inherit the
@@ -330,12 +359,99 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
330
359
  cls_schema.metadata['init_arg_list'] = None
331
360
  user_cls.apply_schema(cls_schema)
332
361
 
362
+ @classmethod
363
+ def _normalize_schema(cls, schema: pg_typing.Schema) -> pg_typing.Schema:
364
+ """Normalizes the schema before applying it."""
365
+
366
+ schema.set_name(cls.__type_name__)
367
+ docstr = utils.docstr(cls)
368
+ if docstr:
369
+ schema.set_description(docstr.description)
370
+
371
+ def _formalize_field(path: utils.KeyPath, node: Any) -> bool:
372
+ """Formalize field."""
373
+ if isinstance(node, pg_typing.Field):
374
+ field = node
375
+ if (not flags.is_empty_field_description_allowed()
376
+ and not field.description):
377
+ raise ValueError(
378
+ f'Field description must not be empty (path={path}).')
379
+
380
+ field.value.set_default(
381
+ field.apply(
382
+ field.default_value,
383
+ allow_partial=True,
384
+ transform_fn=base.symbolic_transform_fn(allow_partial=True)),
385
+ use_default_apply=False)
386
+ if isinstance(field.value, pg_typing.Dict):
387
+ if field.value.schema is not None:
388
+ field.value.schema.set_name(f'{schema.name}.{path.path}')
389
+ utils.traverse(
390
+ field.value.schema.fields, _formalize_field, None, path
391
+ )
392
+ elif isinstance(field.value, pg_typing.List):
393
+ _formalize_field(utils.KeyPath(0, path), field.value.element)
394
+ elif isinstance(field.value, pg_typing.Tuple):
395
+ for i, elem in enumerate(field.value.elements):
396
+ _formalize_field(utils.KeyPath(i, path), elem)
397
+ elif isinstance(field.value, pg_typing.Union):
398
+ for i, c in enumerate(field.value.candidates):
399
+ _formalize_field(
400
+ utils.KeyPath(i, path),
401
+ pg_typing.Field(field.key, c, 'Union sub-type.'),
402
+ )
403
+ return True
404
+
405
+ utils.traverse(schema.fields, _formalize_field)
406
+ return schema
407
+
408
+ @classmethod
409
+ def _finalize_init_arg_list(cls) -> List[str]:
410
+ """Finalizes init_arg_list based on schema."""
411
+ # Update `init_arg_list`` based on the updated schema.
412
+ init_arg_list = cls.__schema__.metadata.get('init_arg_list', None)
413
+ if init_arg_list is None:
414
+ # Inherit from the first non-empty base if they have the same signature.
415
+ # This allows to bypass interface-only bases.
416
+ for base_cls in cls.__bases__:
417
+ schema = getattr(base_cls, '__schema__', None) # pylint: disable=redefined-outer-name
418
+ if isinstance(schema, pg_typing.Schema):
419
+ if ([(k, f.frozen) for k, f in schema.fields.items()]
420
+ == [(k, f.frozen) for k, f in cls.__schema__.fields.items()]):
421
+ init_arg_list = base_cls.init_arg_list
422
+ else:
423
+ break
424
+ if init_arg_list is None:
425
+ # Automatically generate from the field definitions in their
426
+ # declaration order from base classes to subclasses.
427
+ init_arg_list = [
428
+ str(key)
429
+ for key, field in cls.__schema__.fields.items()
430
+ if isinstance(key, pg_typing.ConstStrKey) and not field.frozen
431
+ ]
432
+ cls.__schema__.metadata['init_arg_list'] = init_arg_list
433
+ else:
434
+ for i, arg in enumerate(init_arg_list):
435
+ is_vararg = False
436
+ if i == len(init_arg_list) - 1 and arg.startswith('*'):
437
+ arg = arg[1:]
438
+ is_vararg = True
439
+ field = cls.__schema__.get_field(arg)
440
+ if field is None:
441
+ raise TypeError(
442
+ f'Argument {arg!r} from `init_arg_list` is not defined as a '
443
+ f'symbolic field. init_arg_list={init_arg_list!r}.')
444
+ if is_vararg and not isinstance(field.value, pg_typing.List):
445
+ raise TypeError(
446
+ f'Variable positional argument {arg!r} should be declared with '
447
+ f'`pg.typing.List(...)`. Encountered {field.value!r}.')
448
+ return init_arg_list
449
+
333
450
  @classmethod
334
451
  def _on_schema_update(cls):
335
452
  """Customizable trait: handling schema change."""
336
- # Update the default value for each field after schema is updated. This is
337
- # because that users may change a field's default value via class attribute.
338
- cls._update_default_values_from_class_attributes() # pylint: disable=no-value-for-parameter
453
+ # Finalize init_arg_list baesd on schema.
454
+ cls._finalize_init_arg_list()
339
455
 
340
456
  # Update all schema-based signatures.
341
457
  cls._update_signatures_based_on_schema()
@@ -363,7 +479,7 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
363
479
  # Create a new `__init__` that passes through all the arguments to
364
480
  # in `pg.Object.__init__`. This is needed for each class to use different
365
481
  # signature.
366
- @object_utils.explicit_method_override
482
+ @utils.explicit_method_override
367
483
  @functools.wraps(pseudo_init)
368
484
  def _init(self, *args, **kwargs):
369
485
  # We pass through the arguments to `Object.__init__` instead of
@@ -392,7 +508,7 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
392
508
  def _create_sym_attribute(cls, attr_name, field):
393
509
  """Customizable trait: template of single symbolic attribute."""
394
510
  return property(
395
- object_utils.make_function(
511
+ coding.make_function(
396
512
  attr_name,
397
513
  ['self'],
398
514
  [f"return self.sym_inferred('{attr_name}')"],
@@ -426,7 +542,9 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
426
542
  json_value: Any,
427
543
  *,
428
544
  allow_partial: bool = False,
429
- root_path: Optional[object_utils.KeyPath] = None) -> 'Object':
545
+ root_path: Optional[utils.KeyPath] = None,
546
+ **kwargs,
547
+ ) -> 'Object':
430
548
  """Class method that load an symbolic Object from a JSON value.
431
549
 
432
550
  Example::
@@ -463,24 +581,26 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
463
581
  json_value: Input JSON value, only JSON dict is acceptable.
464
582
  allow_partial: Whether to allow elements of the list to be partial.
465
583
  root_path: KeyPath of loaded object in its object tree.
584
+ **kwargs: Additional keyword arguments to pass through.
466
585
 
467
586
  Returns:
468
587
  A symbolic Object instance.
469
588
  """
470
589
  return cls(allow_partial=allow_partial, root_path=root_path, **{
471
- k: base.from_json(v, allow_partial=allow_partial)
590
+ k: base.from_json(v, allow_partial=allow_partial, **kwargs)
472
591
  for k, v in json_value.items()
473
592
  })
474
593
 
475
- @object_utils.explicit_method_override
594
+ @utils.explicit_method_override
476
595
  def __init__(
477
596
  self,
478
597
  *args,
479
598
  allow_partial: bool = False,
480
599
  sealed: Optional[bool] = None,
481
- root_path: Optional[object_utils.KeyPath] = None,
600
+ root_path: Optional[utils.KeyPath] = None,
482
601
  explicit_init: bool = False,
483
- **kwargs):
602
+ **kwargs,
603
+ ):
484
604
  """Create an Object instance.
485
605
 
486
606
  Args:
@@ -522,8 +642,8 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
522
642
  # Fill field_args and init_args from **kwargs.
523
643
  _, unmatched_keys = self.__class__.__schema__.resolve(list(kwargs.keys()))
524
644
  if unmatched_keys:
525
- arg_phrase = object_utils.auto_plural(len(unmatched_keys), 'argument')
526
- keys_str = object_utils.comma_delimited_str(unmatched_keys)
645
+ arg_phrase = utils.auto_plural(len(unmatched_keys), 'argument')
646
+ keys_str = utils.comma_delimited_str(unmatched_keys)
527
647
  raise TypeError(
528
648
  f'{self.__class__.__name__}.__init__() got unexpected '
529
649
  f'keyword {arg_phrase}: {keys_str}')
@@ -543,8 +663,8 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
543
663
  field_args[vararg_name] = list(args[num_named_args:])
544
664
  args = args[:num_named_args]
545
665
  elif len(args) > len(init_arg_names):
546
- arg_phrase = object_utils.auto_plural(len(init_arg_names), 'argument')
547
- was_phrase = object_utils.auto_plural(len(args), 'was', 'were')
666
+ arg_phrase = utils.auto_plural(len(init_arg_names), 'argument')
667
+ was_phrase = utils.auto_plural(len(args), 'was', 'were')
548
668
  raise TypeError(
549
669
  f'{self.__class__.__name__}.__init__() takes '
550
670
  f'{len(init_arg_names)} positional {arg_phrase} but {len(args)} '
@@ -556,7 +676,7 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
556
676
 
557
677
  for k, v in kwargs.items():
558
678
  if k in field_args:
559
- values_str = object_utils.comma_delimited_str([field_args[k], v])
679
+ values_str = utils.comma_delimited_str([field_args[k], v])
560
680
  raise TypeError(
561
681
  f'{self.__class__.__name__}.__init__() got multiple values for '
562
682
  f'argument \'{k}\': {values_str}.')
@@ -571,8 +691,8 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
571
691
  and field.key not in field_args):
572
692
  missing_args.append(str(field.key))
573
693
  if missing_args:
574
- arg_phrase = object_utils.auto_plural(len(missing_args), 'argument')
575
- keys_str = object_utils.comma_delimited_str(missing_args)
694
+ arg_phrase = utils.auto_plural(len(missing_args), 'argument')
695
+ keys_str = utils.comma_delimited_str(missing_args)
576
696
  raise TypeError(
577
697
  f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
578
698
  f'required {arg_phrase}: {keys_str}.')
@@ -622,8 +742,7 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
622
742
  and during __init__.
623
743
  """
624
744
 
625
- def _on_change(self,
626
- field_updates: Dict[object_utils.KeyPath, base.FieldUpdate]):
745
+ def _on_change(self, field_updates: Dict[utils.KeyPath, base.FieldUpdate]):
627
746
  """Event that is triggered when field values in the subtree are updated.
628
747
 
629
748
  This event will be called
@@ -643,8 +762,7 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
643
762
  del field_updates
644
763
  return self._on_bound()
645
764
 
646
- def _on_path_change(
647
- self, old_path: object_utils.KeyPath, new_path: object_utils.KeyPath):
765
+ def _on_path_change(self, old_path: utils.KeyPath, new_path: utils.KeyPath):
648
766
  """Event that is triggered after the symbolic path changes."""
649
767
  del old_path, new_path
650
768
 
@@ -723,8 +841,8 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
723
841
  return self._sym_attributes.sym_getattr(key)
724
842
 
725
843
  def _sym_rebind(
726
- self, path_value_pairs: Dict[object_utils.KeyPath, Any]
727
- ) -> List[base.FieldUpdate]:
844
+ self, path_value_pairs: Dict[utils.KeyPath, Any]
845
+ ) -> List[base.FieldUpdate]:
728
846
  """Rebind current object using object-form members."""
729
847
  if base.treats_as_sealed(self):
730
848
  raise base.WritePermissionError(
@@ -763,9 +881,8 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
763
881
  return self
764
882
 
765
883
  def _update_children_paths(
766
- self,
767
- old_path: object_utils.KeyPath,
768
- new_path: object_utils.KeyPath) -> None:
884
+ self, old_path: utils.KeyPath, new_path: utils.KeyPath
885
+ ) -> None:
769
886
  """Update children paths according to root_path of current node."""
770
887
  self._sym_attributes.sym_setpath(new_path)
771
888
  self._on_path_change(old_path, new_path)
@@ -849,16 +966,15 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
849
966
  return self.sym_hash()
850
967
  return super().__hash__()
851
968
 
852
- def sym_jsonify(self, **kwargs) -> object_utils.JSONValueType:
969
+ def sym_jsonify(self, **kwargs) -> utils.JSONValueType:
853
970
  """Converts current object to a dict of plain Python objects."""
854
- return object_utils.merge([
855
- {
856
- object_utils.JSONConvertible.TYPE_NAME_KEY: (
857
- self.__class__.__serialization_key__
858
- )
859
- },
860
- self._sym_attributes.to_json(**kwargs),
861
- ])
971
+ json_dict = {
972
+ utils.JSONConvertible.TYPE_NAME_KEY: (
973
+ self.__class__.__serialization_key__
974
+ )
975
+ }
976
+ json_dict.update(self._sym_attributes.to_json(**kwargs))
977
+ return json_dict
862
978
 
863
979
  def format(self,
864
980
  compact: bool = False,
@@ -872,21 +988,24 @@ class Object(base.Symbolic, metaclass=ObjectMeta):
872
988
  root_indent,
873
989
  cls_name=self.__class__.__name__,
874
990
  key_as_attribute=True,
875
- bracket_type=object_utils.BracketType.ROUND,
876
- **kwargs)
991
+ bracket_type=utils.BracketType.ROUND,
992
+ **kwargs,
993
+ )
877
994
 
878
995
 
879
996
  base.Symbolic.ObjectType = Object
880
997
 
881
998
 
882
999
  def members(
883
- fields: List[Union[
884
- pg_typing.Field,
885
- Tuple[Union[str, pg_typing.KeySpec], pg_typing.ValueSpec, str],
886
- Tuple[Union[str, pg_typing.KeySpec], pg_typing.ValueSpec, str, Any]]],
1000
+ fields: Union[
1001
+ List[Union[pg_typing.Field, pg_typing.FieldDef]],
1002
+ Dict[pg_typing.FieldKeyDef, pg_typing.FieldValueDef],
1003
+ ],
887
1004
  metadata: Optional[Dict[str, Any]] = None,
888
1005
  init_arg_list: Optional[Sequence[str]] = None,
889
- **kwargs
1006
+ serialization_key: Optional[str] = None,
1007
+ additional_keys: Optional[List[str]] = None,
1008
+ add_to_registry: bool = True,
890
1009
  ) -> pg_typing.Decorator:
891
1010
  """Function/Decorator for declaring symbolic fields for ``pg.Object``.
892
1011
 
@@ -946,16 +1065,17 @@ def members(
946
1065
  provided, the `init_arg_list` will be automatically generated from
947
1066
  symbolic attributes defined from ``pg.members`` in their declaration
948
1067
  order, from the base classes to the subclass.
949
- **kwargs: Keyword arguments for infrequently used options. Acceptable
950
- keywords are: * `serialization_key`: An optional string to be used as the
1068
+ serialization_key: An optional string to be used as the
951
1069
  serialization key for the class during `sym_jsonify`. If None,
952
1070
  `cls.__type_name__` will be used. This is introduced for scenarios when we
953
1071
  want to relocate a class, before the downstream can recognize the new
954
- location, we need the class to serialize it using previous key. *
955
- `additional_keys`: An optional list of strings as additional keys to
1072
+ location, we need the class to serialize it using previous key.
1073
+ additional_keys: An optional list of strings as additional keys to
956
1074
  deserialize an object of the registered class. This can be useful when we
957
1075
  need to relocate or rename the registered class while being able to load
958
1076
  existing serialized JSON values.
1077
+ add_to_registry: If True, register serialization keys and additional keys
1078
+ with the class.
959
1079
 
960
1080
  Returns:
961
1081
  a decorator function that register the class or function with schema
@@ -967,22 +1087,16 @@ def members(
967
1087
  KeyError: If type has already been registered in the registry.
968
1088
  ValueError: schema cannot be created from fields.
969
1089
  """
970
- serialization_key = kwargs.pop('serialization_key', None)
971
- additional_keys = kwargs.pop('additional_keys', None)
972
- if kwargs:
973
- raise TypeError(f'Unsupported keyword arguments: {list(kwargs.keys())!r}.')
974
-
975
1090
  def _decorator(cls):
976
1091
  """Decorator function that registers schema with an Object class."""
977
- schema_utils.update_schema(
978
- cls,
1092
+ cls.update_schema(
979
1093
  fields,
980
1094
  extend=True,
981
1095
  init_arg_list=init_arg_list,
982
1096
  metadata=metadata,
983
- serialization_key=serialization_key,
984
- additional_keys=additional_keys,
985
1097
  )
1098
+ if add_to_registry:
1099
+ cls.register_for_deserialization(serialization_key, additional_keys)
986
1100
  return cls
987
1101
  return typing.cast(pg_typing.Decorator, _decorator)
988
1102
 
@@ -1014,8 +1128,6 @@ def use_init_args(init_arg_list: Sequence[str]) -> pg_typing.Decorator:
1014
1128
  a decorator function that updates the `__init__` signature.
1015
1129
  """
1016
1130
  def _decorator(cls):
1017
- schema_utils.update_schema(
1018
- cls, [], extend=True, init_arg_list=init_arg_list
1019
- )
1131
+ cls.update_schema([], extend=True, init_arg_list=init_arg_list)
1020
1132
  return cls
1021
1133
  return typing.cast(pg_typing.Decorator, _decorator)