pyglove 0.4.5.dev20240318__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.dev20240318.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.dev20240318.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.dev20240318.dist-info/RECORD +0 -185
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/top_level.txt +0 -0
@@ -18,12 +18,12 @@ import copy
|
|
18
18
|
import inspect
|
19
19
|
import sys
|
20
20
|
import types
|
21
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, Union
|
21
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Type, Union
|
22
22
|
|
23
|
-
from pyglove.core import
|
23
|
+
from pyglove.core import utils
|
24
24
|
|
25
25
|
|
26
|
-
class KeySpec(
|
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(
|
97
|
+
class ForwardRef(utils.Formattable):
|
98
98
|
"""Forward type reference."""
|
99
99
|
|
100
100
|
def __init__(self, module: types.ModuleType, name: str):
|
@@ -139,14 +139,24 @@ class ForwardRef(object_utils.Formattable):
|
|
139
139
|
)
|
140
140
|
return reference
|
141
141
|
|
142
|
-
def format(
|
142
|
+
def format(
|
143
|
+
self,
|
144
|
+
compact: bool = False,
|
145
|
+
verbose: bool = True,
|
146
|
+
root_indent: int = 0,
|
147
|
+
**kwargs
|
148
|
+
) -> str:
|
143
149
|
"""Format this object."""
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
+
return utils.kvlist_str(
|
151
|
+
[
|
152
|
+
('module', self.module.__name__, None),
|
153
|
+
('name', self.name, None),
|
154
|
+
],
|
155
|
+
label=self.__class__.__name__,
|
156
|
+
compact=compact,
|
157
|
+
verbose=verbose,
|
158
|
+
root_indent=root_indent,
|
159
|
+
**kwargs,
|
150
160
|
)
|
151
161
|
|
152
162
|
def __eq__(self, other: Any) -> bool:
|
@@ -156,7 +166,7 @@ class ForwardRef(object_utils.Formattable):
|
|
156
166
|
elif isinstance(other, ForwardRef):
|
157
167
|
return self.module is other.module and self.name == other.name
|
158
168
|
elif inspect.isclass(other):
|
159
|
-
return self.resolved and self.cls is other
|
169
|
+
return self.resolved and self.cls is other # pytype: disable=bad-return-type
|
160
170
|
|
161
171
|
def __ne__(self, other: Any) -> bool:
|
162
172
|
"""Operator!=."""
|
@@ -170,7 +180,7 @@ class ForwardRef(object_utils.Formattable):
|
|
170
180
|
return ForwardRef(self.module, self.name)
|
171
181
|
|
172
182
|
|
173
|
-
class ValueSpec(
|
183
|
+
class ValueSpec(utils.Formattable, utils.JSONConvertible):
|
174
184
|
"""Interface for value specifications.
|
175
185
|
|
176
186
|
A value specification defines what values are acceptable for a symbolic
|
@@ -334,6 +344,10 @@ class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
|
|
334
344
|
Tuple[Type[Any], ...]]: # pyformat: disable
|
335
345
|
"""Returns acceptable (resolved) value type(s)."""
|
336
346
|
|
347
|
+
@abc.abstractmethod
|
348
|
+
def __call__(self, *args, **kwargs) -> Any:
|
349
|
+
"""Instantiates a value based on the spec.."""
|
350
|
+
|
337
351
|
@property
|
338
352
|
@abc.abstractmethod
|
339
353
|
def forward_refs(self) -> Set[ForwardRef]:
|
@@ -349,15 +363,19 @@ class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
|
|
349
363
|
"""Returns True if current value spec accepts None."""
|
350
364
|
|
351
365
|
@abc.abstractmethod
|
352
|
-
def set_default(
|
353
|
-
|
354
|
-
|
366
|
+
def set_default(
|
367
|
+
self,
|
368
|
+
default: Any,
|
369
|
+
use_default_apply: bool = True,
|
370
|
+
root_path: Optional[utils.KeyPath] = None,
|
371
|
+
) -> 'ValueSpec':
|
355
372
|
"""Sets the default value and returns `self`.
|
356
373
|
|
357
374
|
Args:
|
358
375
|
default: Default value.
|
359
376
|
use_default_apply: If True, invoke `apply` to the value, otherwise use
|
360
377
|
default value as is.
|
378
|
+
root_path: (Optional) The path of the field.
|
361
379
|
|
362
380
|
Returns:
|
363
381
|
ValueSpec itself.
|
@@ -380,13 +398,14 @@ class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
|
|
380
398
|
@property
|
381
399
|
def has_default(self) -> bool:
|
382
400
|
"""Returns True if the default value is provided."""
|
383
|
-
return self.default !=
|
401
|
+
return self.default != utils.MISSING_VALUE
|
384
402
|
|
385
403
|
@abc.abstractmethod
|
386
404
|
def freeze(
|
387
405
|
self,
|
388
|
-
permanent_value: Any =
|
389
|
-
apply_before_use: bool = True
|
406
|
+
permanent_value: Any = utils.MISSING_VALUE,
|
407
|
+
apply_before_use: bool = True,
|
408
|
+
) -> 'ValueSpec':
|
390
409
|
"""Sets the default value using a permanent value and freezes current spec.
|
391
410
|
|
392
411
|
A frozen value spec will not accept any value that is not the default
|
@@ -453,10 +472,11 @@ class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
|
|
453
472
|
self,
|
454
473
|
value: Any,
|
455
474
|
allow_partial: bool = False,
|
456
|
-
child_transform: Optional[
|
457
|
-
[
|
458
|
-
|
459
|
-
|
475
|
+
child_transform: Optional[
|
476
|
+
Callable[[utils.KeyPath, 'Field', Any], Any]
|
477
|
+
] = None,
|
478
|
+
root_path: Optional[utils.KeyPath] = None,
|
479
|
+
) -> Any:
|
460
480
|
"""Validates, completes and transforms the input value.
|
461
481
|
|
462
482
|
Here is the procedure of ``apply``::
|
@@ -524,14 +544,16 @@ class ValueSpec(object_utils.Formattable, object_utils.JSONConvertible):
|
|
524
544
|
def from_annotation(
|
525
545
|
cls,
|
526
546
|
annotation: Any,
|
527
|
-
auto_typing=False,
|
528
|
-
accept_value_as_annotation=False
|
547
|
+
auto_typing: bool = False,
|
548
|
+
accept_value_as_annotation: bool = False,
|
549
|
+
parent_module: Optional[types.ModuleType] = None
|
550
|
+
) -> 'ValueSpec':
|
529
551
|
"""Gets a concrete ValueSpec from annotation."""
|
530
|
-
del annotation
|
552
|
+
del annotation, auto_typing, accept_value_as_annotation, parent_module
|
531
553
|
assert False, 'Overridden in `annotation_conversion.py`.'
|
532
554
|
|
533
555
|
|
534
|
-
class Field(
|
556
|
+
class Field(utils.Formattable, utils.JSONConvertible):
|
535
557
|
"""Class that represents the definition of one or a group of attributes.
|
536
558
|
|
537
559
|
``Field`` is held by a :class:`pyglove.Schema` object for defining the
|
@@ -661,9 +683,11 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
|
|
661
683
|
self,
|
662
684
|
value: Any,
|
663
685
|
allow_partial: bool = False,
|
664
|
-
transform_fn: Optional[
|
665
|
-
[
|
666
|
-
|
686
|
+
transform_fn: Optional[
|
687
|
+
Callable[[utils.KeyPath, 'Field', Any], Any]
|
688
|
+
] = None,
|
689
|
+
root_path: Optional[utils.KeyPath] = None,
|
690
|
+
) -> Any:
|
667
691
|
"""Apply current field to a value, which validate and complete the value.
|
668
692
|
|
669
693
|
Args:
|
@@ -690,7 +714,8 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
|
|
690
714
|
value,
|
691
715
|
allow_partial=allow_partial,
|
692
716
|
child_transform=transform_fn,
|
693
|
-
root_path=root_path
|
717
|
+
root_path=root_path
|
718
|
+
)
|
694
719
|
|
695
720
|
if transform_fn:
|
696
721
|
value = transform_fn(root_path, self, value)
|
@@ -711,34 +736,22 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
|
|
711
736
|
compact: bool = False,
|
712
737
|
verbose: bool = True,
|
713
738
|
root_indent: int = 0,
|
714
|
-
*,
|
715
|
-
markdown: bool = False,
|
716
739
|
**kwargs,
|
717
740
|
) -> str:
|
718
741
|
"""Format this field into a string."""
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
742
|
+
return utils.kvlist_str(
|
743
|
+
[
|
744
|
+
('key', self._key, None),
|
745
|
+
('value', self._value, None),
|
746
|
+
('description', self._description, None),
|
747
|
+
('metadata', self._metadata, {}),
|
748
|
+
],
|
749
|
+
label=self.__class__.__name__,
|
725
750
|
compact=compact,
|
726
751
|
verbose=verbose,
|
727
|
-
root_indent=root_indent
|
728
|
-
**kwargs
|
729
|
-
|
730
|
-
metadata = '{...}'
|
731
|
-
attr_str = object_utils.kvlist_str([
|
732
|
-
('key', self._key, None),
|
733
|
-
('value', self._value.format(
|
734
|
-
compact=compact,
|
735
|
-
verbose=verbose,
|
736
|
-
root_indent=root_indent + 1,
|
737
|
-
**kwargs), None),
|
738
|
-
('description', object_utils.quote_if_str(description), None),
|
739
|
-
('metadata', metadata, '{}')
|
740
|
-
])
|
741
|
-
return object_utils.maybe_markdown_quote(f'Field({attr_str})', markdown)
|
752
|
+
root_indent=root_indent,
|
753
|
+
**kwargs,
|
754
|
+
)
|
742
755
|
|
743
756
|
def to_json(self, **kwargs: Any) -> Dict[str, Any]:
|
744
757
|
return self.to_json_dict(
|
@@ -766,7 +779,7 @@ class Field(object_utils.Formattable, object_utils.JSONConvertible):
|
|
766
779
|
return not self.__eq__(other)
|
767
780
|
|
768
781
|
|
769
|
-
class Schema(
|
782
|
+
class Schema(utils.Formattable, utils.JSONConvertible):
|
770
783
|
"""Class that represents a schema.
|
771
784
|
|
772
785
|
PyGlove's runtime type system is based on the concept of ``Schema`` (
|
@@ -897,15 +910,51 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
|
|
897
910
|
break
|
898
911
|
|
899
912
|
if base_schema_list:
|
900
|
-
|
901
|
-
|
902
|
-
self.extend(base)
|
913
|
+
base = Schema.merge(base_schema_list)
|
914
|
+
self.extend(base)
|
903
915
|
|
904
916
|
if not allow_nonconst_keys and self._dynamic_field is not None:
|
905
917
|
raise ValueError(
|
906
918
|
f'NonConstKey is not allowed in schema. '
|
907
919
|
f'Encountered \'{self._dynamic_field.key}\'.')
|
908
920
|
|
921
|
+
@classmethod
|
922
|
+
def merge(
|
923
|
+
cls,
|
924
|
+
schema_list: Sequence['Schema'],
|
925
|
+
name: Optional[str] = None,
|
926
|
+
description: Optional[str] = None
|
927
|
+
) -> 'Schema':
|
928
|
+
"""Merge multiple schemas into one.
|
929
|
+
|
930
|
+
For fields shared by multiple schemas, the first appeared onces will be
|
931
|
+
used in the merged schema.
|
932
|
+
|
933
|
+
Args:
|
934
|
+
schema_list: A list of schemas to merge.
|
935
|
+
name: (Optional) name of the merged schema.
|
936
|
+
description: (Optinoal) description of the schema.
|
937
|
+
|
938
|
+
Returns:
|
939
|
+
The merged schema.
|
940
|
+
"""
|
941
|
+
field_names = set()
|
942
|
+
fields = []
|
943
|
+
kw_field = None
|
944
|
+
for schema in schema_list:
|
945
|
+
for key, field in schema.fields.items():
|
946
|
+
if key.is_const and key not in field_names:
|
947
|
+
fields.append(field)
|
948
|
+
field_names.add(key)
|
949
|
+
elif not key.is_const and kw_field is None:
|
950
|
+
kw_field = field
|
951
|
+
|
952
|
+
if kw_field is not None:
|
953
|
+
fields.append(kw_field)
|
954
|
+
return Schema(
|
955
|
+
fields, name=name, description=description, allow_nonconst_keys=True
|
956
|
+
)
|
957
|
+
|
909
958
|
def extend(self, base: 'Schema') -> 'Schema':
|
910
959
|
"""Extend current schema based on a base schema."""
|
911
960
|
|
@@ -914,13 +963,12 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
|
|
914
963
|
parent_field: Field,
|
915
964
|
child_field: Field) -> Field:
|
916
965
|
"""Merge function on field with the same key."""
|
917
|
-
if parent_field !=
|
918
|
-
if
|
966
|
+
if parent_field != utils.MISSING_VALUE:
|
967
|
+
if utils.MISSING_VALUE == child_field:
|
919
968
|
if (not self._allow_nonconst_keys and not parent_field.key.is_const):
|
920
|
-
hints =
|
921
|
-
('base',
|
922
|
-
|
923
|
-
])
|
969
|
+
hints = utils.kvlist_str(
|
970
|
+
[('base', base.name, None), ('path', path, None)]
|
971
|
+
)
|
924
972
|
raise ValueError(
|
925
973
|
f'Non-const key {parent_field.key} is not allowed to be '
|
926
974
|
f'added to the schema. ({hints})')
|
@@ -929,16 +977,15 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
|
|
929
977
|
try:
|
930
978
|
child_field.extend(parent_field)
|
931
979
|
except Exception as e: # pylint: disable=broad-except
|
932
|
-
hints =
|
933
|
-
('base',
|
934
|
-
|
935
|
-
])
|
980
|
+
hints = utils.kvlist_str(
|
981
|
+
[('base', base.name, None), ('path', path, None)]
|
982
|
+
)
|
936
983
|
raise e.__class__(f'{e} ({hints})').with_traceback(
|
937
984
|
sys.exc_info()[2])
|
938
985
|
return child_field
|
939
986
|
|
940
|
-
self._fields =
|
941
|
-
self._metadata =
|
987
|
+
self._fields = utils.merge([base.fields, self.fields], _merge_field)
|
988
|
+
self._metadata = utils.merge([base.metadata, self.metadata])
|
942
989
|
|
943
990
|
# Inherit dynamic field from base if it's not present in the child.
|
944
991
|
if self._dynamic_field is None:
|
@@ -1061,8 +1108,8 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
|
|
1061
1108
|
dict_obj: Dict[str, Any],
|
1062
1109
|
allow_partial: bool = False,
|
1063
1110
|
child_transform: Optional[Callable[
|
1064
|
-
[
|
1065
|
-
root_path: Optional[
|
1111
|
+
[utils.KeyPath, Field, Any], Any]] = None,
|
1112
|
+
root_path: Optional[utils.KeyPath] = None,
|
1066
1113
|
) -> Dict[str, Any]: # pyformat: disable
|
1067
1114
|
# pyformat: disable
|
1068
1115
|
"""Apply this schema to a dict object, validate and transform it.
|
@@ -1109,7 +1156,8 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
|
|
1109
1156
|
if unmatched_keys:
|
1110
1157
|
raise KeyError(
|
1111
1158
|
f'Keys {unmatched_keys} are not allowed in Schema. '
|
1112
|
-
f'(parent=\'{root_path}\')'
|
1159
|
+
f'(parent=\'{root_path}\')'
|
1160
|
+
)
|
1113
1161
|
|
1114
1162
|
for key_spec, keys in matched_keys.items():
|
1115
1163
|
field = self._fields[key_spec]
|
@@ -1118,19 +1166,19 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
|
|
1118
1166
|
keys.append(str(key_spec))
|
1119
1167
|
for key in keys:
|
1120
1168
|
if dict_obj:
|
1121
|
-
value = dict_obj.get(key,
|
1169
|
+
value = dict_obj.get(key, utils.MISSING_VALUE)
|
1122
1170
|
else:
|
1123
|
-
value =
|
1171
|
+
value = utils.MISSING_VALUE
|
1124
1172
|
# NOTE(daiyip): field.default_value may be MISSING_VALUE too
|
1125
1173
|
# or partial.
|
1126
|
-
if
|
1174
|
+
if utils.MISSING_VALUE == value:
|
1127
1175
|
value = copy.deepcopy(field.default_value)
|
1128
|
-
|
1129
1176
|
new_value = field.apply(
|
1130
1177
|
value,
|
1131
1178
|
allow_partial=allow_partial,
|
1132
1179
|
transform_fn=child_transform,
|
1133
|
-
root_path=
|
1180
|
+
root_path=utils.KeyPath(key, root_path),
|
1181
|
+
)
|
1134
1182
|
|
1135
1183
|
# NOTE(daiyip): `pg.Dict.__getitem__`` has special logics in handling
|
1136
1184
|
# `pg.Contextual`` values. Therefore, we user `dict.__getitem__()`` to
|
@@ -1143,10 +1191,12 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
|
|
1143
1191
|
dict_obj[key] = new_value
|
1144
1192
|
return dict_obj
|
1145
1193
|
|
1146
|
-
def validate(
|
1147
|
-
|
1148
|
-
|
1149
|
-
|
1194
|
+
def validate(
|
1195
|
+
self,
|
1196
|
+
dict_obj: Dict[str, Any],
|
1197
|
+
allow_partial: bool = False,
|
1198
|
+
root_path: Optional[utils.KeyPath] = None,
|
1199
|
+
) -> None:
|
1150
1200
|
"""Validates whether dict object is conformed with the schema."""
|
1151
1201
|
self.apply(
|
1152
1202
|
copy.deepcopy(dict_obj),
|
@@ -1210,50 +1260,27 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
|
|
1210
1260
|
verbose: bool = True,
|
1211
1261
|
root_indent: int = 0,
|
1212
1262
|
*,
|
1213
|
-
markdown: bool = False,
|
1214
1263
|
cls_name: Optional[str] = None,
|
1215
|
-
bracket_type:
|
1264
|
+
bracket_type: utils.BracketType = utils.BracketType.ROUND,
|
1265
|
+
fields_only: bool = False,
|
1216
1266
|
**kwargs,
|
1217
1267
|
) -> str:
|
1218
1268
|
"""Format current Schema into nicely printed string."""
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
s = [f'{cls_name}{open_bracket}']
|
1235
|
-
s.append(', '.join([
|
1236
|
-
f'{f.key}={_format_child(f.value)}'
|
1237
|
-
for f in self.fields.values()
|
1238
|
-
]))
|
1239
|
-
s.append(close_bracket)
|
1240
|
-
else:
|
1241
|
-
s = [f'{cls_name}{open_bracket}\n']
|
1242
|
-
last_field_show_description = False
|
1243
|
-
for i, f in enumerate(self.fields.values()):
|
1244
|
-
this_field_show_description = verbose and f.description
|
1245
|
-
if i != 0:
|
1246
|
-
s.append(',\n')
|
1247
|
-
if last_field_show_description or this_field_show_description:
|
1248
|
-
s.append('\n')
|
1249
|
-
if this_field_show_description:
|
1250
|
-
s.append(_indent(f'# {f.description}\n', root_indent + 1))
|
1251
|
-
last_field_show_description = this_field_show_description
|
1252
|
-
s.append(
|
1253
|
-
_indent(f'{f.key} = {_format_child(f.value)}', root_indent + 1))
|
1254
|
-
s.append('\n')
|
1255
|
-
s.append(_indent(close_bracket, root_indent))
|
1256
|
-
return object_utils.maybe_markdown_quote(''.join(s), markdown)
|
1269
|
+
return utils.kvlist_str(
|
1270
|
+
[
|
1271
|
+
('name', self.name, None),
|
1272
|
+
('description', self.description, None),
|
1273
|
+
('fields', list(self.fields.values()), []),
|
1274
|
+
('allow_nonconst_keys', self.allow_nonconst_keys, True),
|
1275
|
+
('metadata', self.metadata, {}),
|
1276
|
+
],
|
1277
|
+
label=cls_name or self.__class__.__name__,
|
1278
|
+
bracket_type=bracket_type,
|
1279
|
+
compact=compact,
|
1280
|
+
verbose=verbose,
|
1281
|
+
root_indent=root_indent,
|
1282
|
+
**kwargs,
|
1283
|
+
)
|
1257
1284
|
|
1258
1285
|
def to_json(self, **kwargs) -> Dict[str, Any]:
|
1259
1286
|
return self.to_json_dict(
|
@@ -1277,15 +1304,41 @@ class Schema(object_utils.Formattable, object_utils.JSONConvertible):
|
|
1277
1304
|
return not self.__eq__(other)
|
1278
1305
|
|
1279
1306
|
|
1307
|
+
FieldDef = Union[
|
1308
|
+
# Key, Value spec/annotation.
|
1309
|
+
Tuple[Union[str, KeySpec], Any],
|
1310
|
+
|
1311
|
+
# Key, Value spec/annotation, field docstr.
|
1312
|
+
Tuple[Union[str, KeySpec], Any, str],
|
1313
|
+
|
1314
|
+
# Key, Value spec/annotation, field docstr, field metadata.
|
1315
|
+
Tuple[Union[str, KeySpec], Any, str, Dict[str, Any]],
|
1316
|
+
]
|
1317
|
+
|
1318
|
+
FieldKeyDef = Union[str, KeySpec]
|
1319
|
+
|
1320
|
+
FieldValueDef = Union[
|
1321
|
+
# Value spec/annotation.
|
1322
|
+
Any,
|
1323
|
+
|
1324
|
+
# Value spec/annotation, field docstr.
|
1325
|
+
Tuple[Any, str],
|
1326
|
+
|
1327
|
+
# Value spec/annotation, field docstr, field metadata.
|
1328
|
+
Tuple[Any, str, Dict[str, Any]]
|
1329
|
+
]
|
1330
|
+
|
1331
|
+
|
1280
1332
|
def create_field(
|
1281
|
-
|
1333
|
+
field_or_def: Union[Field, FieldDef],
|
1282
1334
|
auto_typing: bool = True,
|
1283
|
-
accept_value_as_annotation: bool = True
|
1335
|
+
accept_value_as_annotation: bool = True,
|
1336
|
+
parent_module: Optional[types.ModuleType] = None
|
1284
1337
|
) -> Field:
|
1285
1338
|
"""Creates ``Field`` from its equivalence.
|
1286
1339
|
|
1287
1340
|
Args:
|
1288
|
-
|
1341
|
+
field_or_def: a ``Field`` object or its equivalence, which is a tuple of
|
1289
1342
|
2 - 4 elements:
|
1290
1343
|
`(<key>, <value>, [description], [metadata])`.
|
1291
1344
|
`key` can be a KeySpec subclass object or string. `value` can be a
|
@@ -1298,31 +1351,34 @@ def create_field(
|
|
1298
1351
|
``pg.typing.Any()`` will be used.
|
1299
1352
|
accept_value_as_annotation: If True, allow default values to be used as
|
1300
1353
|
annotations when creating the value spec.
|
1354
|
+
parent_module: (Optional) parent module for defining this field, which will
|
1355
|
+
be used for forward reference lookup.
|
1301
1356
|
|
1302
1357
|
Returns:
|
1303
1358
|
A ``Field`` object.
|
1304
1359
|
"""
|
1305
|
-
if isinstance(
|
1306
|
-
return
|
1360
|
+
if isinstance(field_or_def, Field):
|
1361
|
+
return field_or_def
|
1307
1362
|
|
1308
|
-
if not isinstance(
|
1363
|
+
if not isinstance(field_or_def, tuple):
|
1309
1364
|
raise TypeError(
|
1310
1365
|
f'Field definition should be tuples with 2 to 4 elements. '
|
1311
|
-
f'Encountered: {
|
1366
|
+
f'Encountered: {field_or_def}.')
|
1312
1367
|
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1368
|
+
field_def = list(field_or_def)
|
1369
|
+
if len(field_def) == 4:
|
1370
|
+
maybe_key_spec, maybe_value_spec, description, field_metadata = field_def
|
1371
|
+
elif len(field_def) == 3:
|
1372
|
+
maybe_key_spec, maybe_value_spec, description = field_def
|
1317
1373
|
field_metadata = {}
|
1318
|
-
elif len(
|
1319
|
-
maybe_key_spec, maybe_value_spec =
|
1374
|
+
elif len(field_def) == 2:
|
1375
|
+
maybe_key_spec, maybe_value_spec = field_def
|
1320
1376
|
description = None
|
1321
1377
|
field_metadata = {}
|
1322
1378
|
else:
|
1323
1379
|
raise TypeError(
|
1324
1380
|
f'Field definition should be tuples with 2 to 4 elements. '
|
1325
|
-
f'Encountered: {
|
1381
|
+
f'Encountered: {field_or_def}.')
|
1326
1382
|
|
1327
1383
|
if isinstance(maybe_key_spec, (str, KeySpec)):
|
1328
1384
|
key = maybe_key_spec
|
@@ -1330,10 +1386,14 @@ def create_field(
|
|
1330
1386
|
raise TypeError(
|
1331
1387
|
f'The 1st element of field definition should be of '
|
1332
1388
|
f'<class \'str\'> or KeySpec. Encountered: {maybe_key_spec}.')
|
1389
|
+
|
1333
1390
|
value = ValueSpec.from_annotation(
|
1334
1391
|
maybe_value_spec,
|
1335
1392
|
auto_typing=auto_typing,
|
1336
|
-
accept_value_as_annotation=accept_value_as_annotation
|
1393
|
+
accept_value_as_annotation=accept_value_as_annotation,
|
1394
|
+
parent_module=parent_module,
|
1395
|
+
)
|
1396
|
+
|
1337
1397
|
if (description is not None and
|
1338
1398
|
not isinstance(description, str)):
|
1339
1399
|
raise TypeError(f'Description (the 3rd element) of field definition '
|
@@ -1347,14 +1407,15 @@ def create_field(
|
|
1347
1407
|
|
1348
1408
|
def create_schema(
|
1349
1409
|
fields: Union[
|
1350
|
-
Dict[str,
|
1351
|
-
List[Union[Field,
|
1410
|
+
Dict[str, FieldValueDef],
|
1411
|
+
List[Union[Field, FieldDef]] # pylint: disable=g-bare-generic
|
1352
1412
|
],
|
1353
1413
|
name: Optional[str] = None,
|
1354
1414
|
base_schema_list: Optional[List[Schema]] = None,
|
1355
1415
|
allow_nonconst_keys: bool = False,
|
1356
1416
|
metadata: Optional[Dict[str, Any]] = None,
|
1357
1417
|
description: Optional[str] = None,
|
1418
|
+
parent_module: Optional[types.ModuleType] = None
|
1358
1419
|
) -> Schema:
|
1359
1420
|
"""Creates ``Schema`` from a list of ``Field``s or equivalences.
|
1360
1421
|
|
@@ -1395,6 +1456,8 @@ def create_schema(
|
|
1395
1456
|
allow_nonconst_keys: Whether to allow non const keys in schema.
|
1396
1457
|
metadata: Optional dict of user objects as schema-level metadata.
|
1397
1458
|
description: Optional description of the schema.
|
1459
|
+
parent_module: (Optional) parent module for defining this schema, which will
|
1460
|
+
be used for forward reference lookup.
|
1398
1461
|
|
1399
1462
|
Returns:
|
1400
1463
|
Schema object.
|
@@ -1402,30 +1465,42 @@ def create_schema(
|
|
1402
1465
|
Raises:
|
1403
1466
|
TypeError: If input type is incorrect.
|
1404
1467
|
"""
|
1405
|
-
|
1406
|
-
normalized_fields = []
|
1407
|
-
for k, v in fields.items():
|
1408
|
-
if not isinstance(v, (tuple, list)):
|
1409
|
-
v = (v,)
|
1410
|
-
normalized_fields.append(tuple([k] + list(v)))
|
1411
|
-
fields = normalized_fields
|
1412
|
-
|
1413
|
-
if not isinstance(fields, list):
|
1414
|
-
raise TypeError(
|
1415
|
-
'Schema definition should be a dict of field names to their '
|
1416
|
-
'definitions, a list of `pg.typing.Field` objects or a list of tuples '
|
1417
|
-
'in format (key, value, description, metadata).')
|
1418
|
-
|
1468
|
+
fields = _normalize_field_defs(fields)
|
1419
1469
|
metadata = metadata or {}
|
1420
1470
|
if not isinstance(metadata, dict):
|
1421
1471
|
raise TypeError(f'Metadata of schema should be a dict. '
|
1422
1472
|
f'Encountered: {metadata}.')
|
1423
1473
|
|
1424
1474
|
return Schema(
|
1425
|
-
fields=[create_field(
|
1475
|
+
fields=[create_field(field_or_def, parent_module=parent_module)
|
1476
|
+
for field_or_def in fields],
|
1426
1477
|
name=name,
|
1427
1478
|
base_schema_list=base_schema_list,
|
1428
1479
|
allow_nonconst_keys=allow_nonconst_keys,
|
1429
1480
|
metadata=metadata,
|
1430
1481
|
description=description,
|
1431
1482
|
)
|
1483
|
+
|
1484
|
+
|
1485
|
+
def _normalize_field_defs(
|
1486
|
+
fields: Union[
|
1487
|
+
Dict[str, FieldValueDef],
|
1488
|
+
List[Union[Field, FieldDef]] # pylint: disable=g-bare-generic
|
1489
|
+
]
|
1490
|
+
) -> List[Union[Field, FieldDef]]:
|
1491
|
+
"""Normalizes field definitions."""
|
1492
|
+
if isinstance(fields, dict):
|
1493
|
+
normalized_fields = []
|
1494
|
+
for k, v in fields.items():
|
1495
|
+
if not isinstance(v, (tuple, list)):
|
1496
|
+
v = (v,)
|
1497
|
+
normalized_fields.append(tuple([k] + list(v)))
|
1498
|
+
return normalized_fields # pytype: disable=bad-return-type
|
1499
|
+
elif not isinstance(fields, list):
|
1500
|
+
raise TypeError(
|
1501
|
+
'Schema definition should be a dict of field names to their '
|
1502
|
+
'definitions, a list of `pg.typing.Field` objects or a list of tuples '
|
1503
|
+
'in format (key, value, description, metadata). '
|
1504
|
+
f'Encountered: {fields}.'
|
1505
|
+
)
|
1506
|
+
return fields
|