pyglove 0.4.5.dev20240319__py3-none-any.whl → 0.4.5.dev202501140808__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.dev202501140808.dist-info}/METADATA +18 -4
  131. pyglove-0.4.5.dev202501140808.dist-info/RECORD +214 -0
  132. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.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.dev202501140808.dist-info}/LICENSE +0 -0
  145. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.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 object_utils
23
+ from pyglove.core import utils
24
24
 
25
25
 
26
- class KeySpec(object_utils.Formattable, object_utils.JSONConvertible):
26
+ class KeySpec(utils.Formattable, utils.JSONConvertible):
27
27
  """Interface for key specifications.
28
28
 
29
29
  A key specification determines what keys are acceptable for a symbolic
@@ -94,7 +94,7 @@ class KeySpec(object_utils.Formattable, object_utils.JSONConvertible):
94
94
  assert False, 'Overridden in `key_specs.py`.'
95
95
 
96
96
 
97
- class ForwardRef(object_utils.Formattable):
97
+ class ForwardRef(utils.Formattable):
98
98
  """Forward type reference."""
99
99
 
100
100
  def __init__(self, module: types.ModuleType, name: str):
@@ -139,14 +139,24 @@ class ForwardRef(object_utils.Formattable):
139
139
  )
140
140
  return reference
141
141
 
142
- def format(self, *args, markdown: bool = False, **kwargs) -> str:
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
- details = object_utils.kvlist_str([
145
- ('module', self.module.__name__, None),
146
- ('name', self.name, None),
147
- ])
148
- return object_utils.maybe_markdown_quote(
149
- f'{self.__class__.__name__}({details})', markdown
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(object_utils.Formattable, object_utils.JSONConvertible):
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(self,
353
- default: Any,
354
- use_default_apply: bool = True) -> 'ValueSpec':
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 != object_utils.MISSING_VALUE
401
+ return self.default != utils.MISSING_VALUE
384
402
 
385
403
  @abc.abstractmethod
386
404
  def freeze(
387
405
  self,
388
- permanent_value: Any = object_utils.MISSING_VALUE,
389
- apply_before_use: bool = True) -> 'ValueSpec':
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[Callable[
457
- [object_utils.KeyPath, 'Field', Any], Any]] = None,
458
- root_path: Optional[object_utils.KeyPath] = None,
459
- ) -> Any:
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) -> 'ValueSpec':
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(object_utils.Formattable, object_utils.JSONConvertible):
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[Callable[
665
- [object_utils.KeyPath, 'Field', Any], Any]] = None,
666
- root_path: Optional[object_utils.KeyPath] = None) -> Any:
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
- description = self._description
720
- if not verbose and self._description and len(self._description) > 20:
721
- description = self._description[:20] + '...'
722
-
723
- metadata = object_utils.format(
724
- self._metadata,
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 + 1,
728
- **kwargs)
729
- if not verbose and len(metadata) > 24:
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(object_utils.Formattable, object_utils.JSONConvertible):
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
- # Extend base schema from the nearest ancestor to the farthest.
901
- for base in reversed(base_schema_list):
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 != object_utils.MISSING_VALUE:
918
- if object_utils.MISSING_VALUE == child_field:
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 = object_utils.kvlist_str([
921
- ('base', object_utils.quote_if_str(base.name), None),
922
- ('path', path, None)
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 = object_utils.kvlist_str([
933
- ('base', object_utils.quote_if_str(base.name), None),
934
- ('path', path, None)
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 = object_utils.merge([base.fields, self.fields], _merge_field)
941
- self._metadata = object_utils.merge([base.metadata, 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
- [object_utils.KeyPath, Field, Any], Any]] = None,
1065
- root_path: Optional[object_utils.KeyPath] = None,
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, object_utils.MISSING_VALUE)
1169
+ value = dict_obj.get(key, utils.MISSING_VALUE)
1122
1170
  else:
1123
- value = object_utils.MISSING_VALUE
1171
+ value = utils.MISSING_VALUE
1124
1172
  # NOTE(daiyip): field.default_value may be MISSING_VALUE too
1125
1173
  # or partial.
1126
- if object_utils.MISSING_VALUE == value:
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=object_utils.KeyPath(key, 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(self,
1147
- dict_obj: Dict[str, Any],
1148
- allow_partial: bool = False,
1149
- root_path: Optional[object_utils.KeyPath] = None) -> None:
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: object_utils.BracketType = object_utils.BracketType.ROUND,
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
- if cls_name is None:
1220
- cls_name = 'Schema'
1221
-
1222
- def _indent(text, indent):
1223
- return ' ' * 2 * indent + text
1224
-
1225
- def _format_child(child):
1226
- return child.format(
1227
- compact=compact,
1228
- verbose=verbose,
1229
- root_indent=root_indent + 1,
1230
- **kwargs)
1231
-
1232
- open_bracket, close_bracket = object_utils.bracket_chars(bracket_type)
1233
- if compact:
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
- maybe_field: Union[Field, Tuple], # pylint: disable=g-bare-generic
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
- maybe_field: a ``Field`` object or its equivalence, which is a tuple of
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(maybe_field, Field):
1306
- return maybe_field
1360
+ if isinstance(field_or_def, Field):
1361
+ return field_or_def
1307
1362
 
1308
- if not isinstance(maybe_field, tuple):
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: {maybe_field}.')
1366
+ f'Encountered: {field_or_def}.')
1312
1367
 
1313
- if len(maybe_field) == 4:
1314
- maybe_key_spec, maybe_value_spec, description, field_metadata = maybe_field
1315
- elif len(maybe_field) == 3:
1316
- maybe_key_spec, maybe_value_spec, description = maybe_field
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(maybe_field) == 2:
1319
- maybe_key_spec, maybe_value_spec = maybe_field
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: {maybe_field}.')
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, Any],
1351
- List[Union[Field, Tuple]] # pylint: disable=g-bare-generic
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
- if isinstance(fields, dict):
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(maybe_field) for maybe_field in fields],
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