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.
- pyglove/core/__init__.py +54 -20
- pyglove/core/coding/__init__.py +42 -0
- pyglove/core/coding/errors.py +111 -0
- pyglove/core/coding/errors_test.py +98 -0
- pyglove/core/coding/execution.py +309 -0
- pyglove/core/coding/execution_test.py +333 -0
- pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
- pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
- pyglove/core/coding/parsing.py +153 -0
- pyglove/core/coding/parsing_test.py +150 -0
- pyglove/core/coding/permissions.py +100 -0
- pyglove/core/coding/permissions_test.py +93 -0
- pyglove/core/geno/base.py +54 -41
- pyglove/core/geno/base_test.py +2 -4
- pyglove/core/geno/categorical.py +37 -28
- pyglove/core/geno/custom.py +19 -16
- pyglove/core/geno/numerical.py +20 -17
- pyglove/core/geno/space.py +4 -5
- pyglove/core/hyper/base.py +6 -6
- pyglove/core/hyper/categorical.py +94 -55
- pyglove/core/hyper/custom.py +7 -7
- pyglove/core/hyper/custom_test.py +9 -10
- pyglove/core/hyper/derived.py +30 -22
- pyglove/core/hyper/derived_test.py +2 -4
- pyglove/core/hyper/dynamic_evaluation.py +5 -6
- pyglove/core/hyper/evolvable.py +57 -46
- pyglove/core/hyper/numerical.py +48 -24
- pyglove/core/hyper/numerical_test.py +9 -9
- pyglove/core/hyper/object_template.py +58 -46
- pyglove/core/io/__init__.py +1 -0
- pyglove/core/io/file_system.py +17 -7
- pyglove/core/io/file_system_test.py +2 -0
- pyglove/core/io/sequence.py +299 -0
- pyglove/core/io/sequence_test.py +124 -0
- pyglove/core/logging_test.py +0 -2
- pyglove/core/patching/object_factory.py +4 -4
- pyglove/core/patching/pattern_based.py +4 -4
- pyglove/core/patching/rule_based.py +17 -5
- pyglove/core/patching/rule_based_test.py +27 -4
- pyglove/core/symbolic/__init__.py +2 -7
- pyglove/core/symbolic/base.py +320 -183
- pyglove/core/symbolic/base_test.py +123 -19
- pyglove/core/symbolic/boilerplate.py +7 -13
- pyglove/core/symbolic/boilerplate_test.py +25 -23
- pyglove/core/symbolic/class_wrapper.py +48 -45
- pyglove/core/symbolic/class_wrapper_test.py +2 -2
- pyglove/core/symbolic/compounding.py +9 -15
- pyglove/core/symbolic/compounding_test.py +2 -4
- pyglove/core/symbolic/dict.py +154 -110
- pyglove/core/symbolic/dict_test.py +238 -130
- pyglove/core/symbolic/diff.py +199 -10
- pyglove/core/symbolic/diff_test.py +226 -0
- pyglove/core/symbolic/flags.py +1 -1
- pyglove/core/symbolic/functor.py +29 -26
- pyglove/core/symbolic/functor_test.py +102 -50
- pyglove/core/symbolic/inferred.py +2 -2
- pyglove/core/symbolic/list.py +81 -50
- pyglove/core/symbolic/list_test.py +119 -97
- pyglove/core/symbolic/object.py +225 -113
- pyglove/core/symbolic/object_test.py +320 -108
- pyglove/core/symbolic/origin.py +17 -14
- pyglove/core/symbolic/origin_test.py +4 -2
- pyglove/core/symbolic/pure_symbolic.py +4 -3
- pyglove/core/symbolic/ref.py +108 -21
- pyglove/core/symbolic/ref_test.py +93 -0
- pyglove/core/symbolic/symbolize_test.py +10 -2
- pyglove/core/tuning/local_backend.py +2 -2
- pyglove/core/tuning/protocols.py +3 -3
- pyglove/core/tuning/sample_test.py +3 -3
- pyglove/core/typing/__init__.py +14 -5
- pyglove/core/typing/annotation_conversion.py +43 -27
- pyglove/core/typing/annotation_conversion_test.py +23 -0
- pyglove/core/typing/callable_ext.py +241 -3
- pyglove/core/typing/callable_ext_test.py +255 -0
- pyglove/core/typing/callable_signature.py +510 -66
- pyglove/core/typing/callable_signature_test.py +619 -99
- pyglove/core/typing/class_schema.py +229 -154
- pyglove/core/typing/class_schema_test.py +149 -95
- pyglove/core/typing/custom_typing.py +5 -4
- pyglove/core/typing/inspect.py +63 -0
- pyglove/core/typing/inspect_test.py +39 -0
- pyglove/core/typing/key_specs.py +10 -11
- pyglove/core/typing/key_specs_test.py +7 -4
- pyglove/core/typing/type_conversion.py +4 -5
- pyglove/core/typing/type_conversion_test.py +12 -12
- pyglove/core/typing/typed_missing.py +6 -7
- pyglove/core/typing/typed_missing_test.py +7 -8
- pyglove/core/typing/value_specs.py +604 -362
- pyglove/core/typing/value_specs_test.py +328 -90
- pyglove/core/utils/__init__.py +164 -0
- pyglove/core/{object_utils → utils}/common_traits.py +3 -67
- pyglove/core/utils/common_traits_test.py +36 -0
- pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
- pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
- pyglove/core/{object_utils → utils}/error_utils.py +78 -9
- pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
- pyglove/core/utils/formatting.py +464 -0
- pyglove/core/utils/formatting_test.py +453 -0
- pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
- pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
- pyglove/core/{object_utils → utils}/json_conversion.py +177 -52
- pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
- pyglove/core/{object_utils → utils}/missing.py +3 -3
- pyglove/core/{object_utils → utils}/missing_test.py +2 -4
- pyglove/core/utils/text_color.py +128 -0
- pyglove/core/utils/text_color_test.py +94 -0
- pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
- pyglove/core/utils/timing.py +236 -0
- pyglove/core/utils/timing_test.py +154 -0
- pyglove/core/{object_utils → utils}/value_location.py +275 -6
- pyglove/core/utils/value_location_test.py +707 -0
- pyglove/core/views/__init__.py +32 -0
- pyglove/core/views/base.py +804 -0
- pyglove/core/views/base_test.py +580 -0
- pyglove/core/views/html/__init__.py +27 -0
- pyglove/core/views/html/base.py +547 -0
- pyglove/core/views/html/base_test.py +830 -0
- pyglove/core/views/html/controls/__init__.py +35 -0
- pyglove/core/views/html/controls/base.py +275 -0
- pyglove/core/views/html/controls/label.py +207 -0
- pyglove/core/views/html/controls/label_test.py +157 -0
- pyglove/core/views/html/controls/progress_bar.py +183 -0
- pyglove/core/views/html/controls/progress_bar_test.py +97 -0
- pyglove/core/views/html/controls/tab.py +320 -0
- pyglove/core/views/html/controls/tab_test.py +87 -0
- pyglove/core/views/html/controls/tooltip.py +99 -0
- pyglove/core/views/html/controls/tooltip_test.py +99 -0
- pyglove/core/views/html/tree_view.py +1517 -0
- pyglove/core/views/html/tree_view_test.py +1461 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/METADATA +18 -4
- pyglove-0.4.5.dev202501132210.dist-info/RECORD +214 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/WHEEL +1 -1
- pyglove/core/object_utils/__init__.py +0 -154
- pyglove/core/object_utils/common_traits_test.py +0 -82
- pyglove/core/object_utils/formatting.py +0 -234
- pyglove/core/object_utils/formatting_test.py +0 -223
- pyglove/core/object_utils/value_location_test.py +0 -385
- pyglove/core/symbolic/schema_utils.py +0 -327
- pyglove/core/symbolic/schema_utils_test.py +0 -57
- pyglove/core/typing/class_schema_utils.py +0 -202
- pyglove/core/typing/class_schema_utils_test.py +0 -194
- pyglove-0.4.5.dev20240319.dist-info/RECORD +0 -185
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/top_level.txt +0 -0
pyglove/core/symbolic/object.py
CHANGED
@@ -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,
|
20
|
+
from typing import Any, Dict, Iterator, List, Optional, Sequence, Union
|
21
21
|
|
22
|
-
from pyglove.core import
|
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
|
-
|
81
|
-
|
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
|
-
|
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(
|
153
|
-
|
154
|
-
|
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
|
-
|
157
|
-
attr_value
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
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 =
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
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
|
-
#
|
337
|
-
|
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
|
-
@
|
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
|
-
|
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[
|
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
|
-
@
|
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[
|
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 =
|
526
|
-
keys_str =
|
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 =
|
547
|
-
was_phrase =
|
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 =
|
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 =
|
575
|
-
keys_str =
|
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[
|
727
|
-
|
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
|
-
|
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) ->
|
969
|
+
def sym_jsonify(self, **kwargs) -> utils.JSONValueType:
|
853
970
|
"""Converts current object to a dict of plain Python objects."""
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
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=
|
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:
|
884
|
-
pg_typing.Field,
|
885
|
-
|
886
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|