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
@@ -14,13 +14,13 @@
14
14
  """Handling locations in a hierarchical object."""
15
15
 
16
16
  import abc
17
- import copy
17
+ import copy as copy_lib
18
18
  import operator
19
- from typing import Any, Callable, List, Optional, Union
20
- from pyglove.core.object_utils import common_traits
19
+ from typing import Any, Callable, Iterable, Iterator, List, Optional, Union
20
+ from pyglove.core.utils import formatting
21
21
 
22
22
 
23
- class KeyPath(common_traits.Formattable):
23
+ class KeyPath(formatting.Formattable):
24
24
  """Represents a path of keys from the root to a node in a tree.
25
25
 
26
26
  ``KeyPath`` is an important concept in PyGlove, which is used for representing
@@ -187,7 +187,7 @@ class KeyPath(common_traits.Formattable):
187
187
  @property
188
188
  def keys(self) -> List[Any]:
189
189
  """A list of keys in this path."""
190
- return copy.copy(self._keys)
190
+ return copy_lib.copy(self._keys)
191
191
 
192
192
  @property
193
193
  def key(self) -> Any:
@@ -287,6 +287,10 @@ class KeyPath(common_traits.Formattable):
287
287
  return self
288
288
  if isinstance(other, str):
289
289
  other = KeyPath.parse(other)
290
+ elif isinstance(other, KeyPathSet):
291
+ other = other.copy()
292
+ other.rebase(self)
293
+ return other
290
294
  elif not isinstance(other, KeyPath):
291
295
  other = KeyPath(other)
292
296
  assert isinstance(other, KeyPath)
@@ -400,6 +404,16 @@ class KeyPath(common_traits.Formattable):
400
404
  except KeyError:
401
405
  return False
402
406
 
407
+ def is_relative_to(self, other: Union[int, str, 'KeyPath']) -> bool:
408
+ """Returns whether current path is relative to another path."""
409
+ other = KeyPath.from_value(other)
410
+ if len(self) < len(other):
411
+ return False
412
+ for i in range(len(other)):
413
+ if self.keys[i] != other.keys[i]:
414
+ return False
415
+ return True
416
+
403
417
  @property
404
418
  def path(self) -> str:
405
419
  """JSONPath representation of current path."""
@@ -437,6 +451,14 @@ class KeyPath(common_traits.Formattable):
437
451
  """Use depth as length of current path."""
438
452
  return self.depth
439
453
 
454
+ # Use path for both string/repr, which will not be controlled by
455
+ # `pg.str_format` and `pg.repr_format`.
456
+ def __str__(self) -> str:
457
+ return self.path
458
+
459
+ def __repr__(self) -> str:
460
+ return self.path
461
+
440
462
  def format(self, *args, **kwargs):
441
463
  """Format current path."""
442
464
  return self.path
@@ -552,6 +574,245 @@ class KeyPath(common_traits.Formattable):
552
574
  return comparison(self.key, other.key)
553
575
 
554
576
 
577
+ class KeyPathSet(formatting.Formattable):
578
+ """A KeyPath set based on trie-like data structure."""
579
+
580
+ def __init__(
581
+ self,
582
+ paths: Optional[Iterable[KeyPath]] = None,
583
+ *,
584
+ include_intermediate: bool = False
585
+ ):
586
+ self._trie = {}
587
+ if paths:
588
+ for path in paths:
589
+ self.add(path, include_intermediate=include_intermediate)
590
+
591
+ def add(
592
+ self,
593
+ path: Union[str, int, KeyPath],
594
+ include_intermediate: bool = False,
595
+ ) -> bool:
596
+ """Adds a path to the set."""
597
+ path = KeyPath.from_value(path)
598
+ root = self._trie
599
+ updated = False
600
+ for key in path.keys:
601
+ if key not in root:
602
+ root[key] = {}
603
+ if include_intermediate:
604
+ root['$'] = True
605
+ updated = True
606
+ root = root[key]
607
+
608
+ assert isinstance(root, dict), root
609
+ if '$' not in root:
610
+ root['$'] = True
611
+ updated = True
612
+ return updated
613
+
614
+ def remove(self, path: Union[str, int, KeyPath]) -> bool:
615
+ """Removes a path from the set."""
616
+ path = KeyPath.from_value(path)
617
+ stack = [self._trie]
618
+ for key in path.keys:
619
+ if key not in stack[-1]:
620
+ return False
621
+ value = stack[-1][key]
622
+ assert isinstance(value, dict), value
623
+ stack.append(value)
624
+
625
+ if '$' in stack[-1]:
626
+ stack[-1].pop('$')
627
+ stack.pop(-1)
628
+ assert len(stack) == len(path.keys), (path.keys, stack)
629
+ for key, parent_node in zip(reversed(path.keys), reversed(stack)):
630
+ if not parent_node[key]:
631
+ del parent_node[key]
632
+ return True
633
+ return False
634
+
635
+ def __contains__(self, path: Union[str, int, KeyPath]) -> bool:
636
+ """Returns True if the path is in the set."""
637
+ path = KeyPath.from_value(path)
638
+ root = self._trie
639
+ for key in path.keys:
640
+ if key not in root:
641
+ return False
642
+ root = root[key]
643
+ return '$' in root
644
+
645
+ def __bool__(self) -> bool:
646
+ """Returns True if the set is not empty."""
647
+ return bool(self._trie)
648
+
649
+ def __iter__(self) -> Iterator[KeyPath]:
650
+ """Iterates all paths in the set."""
651
+ def _traverse(node, keys):
652
+ for k, v in node.items():
653
+ if k == '$':
654
+ yield KeyPath(keys)
655
+ else:
656
+ keys.append(k)
657
+ for path in _traverse(v, keys):
658
+ yield path
659
+ keys.pop(-1)
660
+ return _traverse(self._trie, [])
661
+
662
+ def __eq__(self, other: Any):
663
+ return isinstance(other, KeyPathSet) and self._trie == other._trie
664
+
665
+ def __ne__(self, other: Any) -> bool:
666
+ return not self.__eq__(other)
667
+
668
+ def has_prefix(self, root_path: Union[int, str, KeyPath]) -> bool:
669
+ """Returns True if the set has a path with the given prefix."""
670
+ root_path = KeyPath.from_value(root_path)
671
+ root = self._trie
672
+ for key in root_path.keys:
673
+ if key not in root:
674
+ return False
675
+ root = root[key]
676
+ return True
677
+
678
+ def __add__(self, other: 'KeyPathSet') -> 'KeyPathSet':
679
+ return self.union(other, copy=True)
680
+
681
+ def rebase(
682
+ self,
683
+ root_path: Union[int, str, KeyPath],
684
+ ) -> None:
685
+ """Returns a KeyPathSet with the given prefix path added."""
686
+ root_path = KeyPath.from_value(root_path)
687
+ root = self._trie
688
+ for key in reversed(root_path.keys):
689
+ root = {key: root}
690
+ self._trie = root
691
+
692
+ def clear(self) -> None:
693
+ """Clears the set."""
694
+ self._trie.clear()
695
+
696
+ def copy(self) -> 'KeyPathSet':
697
+ """Returns a deep copy of the set."""
698
+ return copy_lib.deepcopy(self)
699
+
700
+ def difference_update(self, other: 'KeyPathSet') -> None:
701
+ """Removes the paths in the other set from the current set."""
702
+ def _remove_same(target_dict, src_dict):
703
+ keys_to_remove = []
704
+ for key, value in target_dict.items():
705
+ if key in src_dict:
706
+ if key == '$' or _remove_same(value, src_dict[key]):
707
+ keys_to_remove.append(key)
708
+ for key in keys_to_remove:
709
+ del target_dict[key]
710
+ if not target_dict:
711
+ return True
712
+ return False
713
+ _remove_same(self._trie, other._trie) # pylint: disable=protected-access
714
+
715
+ def difference(
716
+ self, other: 'KeyPathSet',
717
+ ) -> 'KeyPathSet':
718
+ """Returns the subset KeyPathSet based on a prefix path."""
719
+ x = self.copy()
720
+ x.difference_update(other)
721
+ return x
722
+
723
+ def intersection_update(self, other: 'KeyPathSet') -> None:
724
+ """Removes the paths in the other set from the current set."""
725
+ def _remove_diff(target_dict, src_dict):
726
+ keys_to_remove = []
727
+ for key, value in target_dict.items():
728
+ if key not in src_dict:
729
+ keys_to_remove.append(key)
730
+ elif key != '$':
731
+ _remove_diff(value, src_dict[key])
732
+ if not value:
733
+ keys_to_remove.append(key)
734
+ for key in keys_to_remove:
735
+ del target_dict[key]
736
+ _remove_diff(self._trie, other._trie) # pylint: disable=protected-access
737
+
738
+ def intersection(self, other: 'KeyPathSet') -> 'KeyPathSet':
739
+ """Returns the intersection KeyPathSet."""
740
+ copy = self.copy()
741
+ copy.intersection_update(other)
742
+ return copy
743
+
744
+ def update(self, other: 'KeyPathSet') -> None:
745
+ """Updates the current set with the other set."""
746
+ def _merge(target_dict, src_dict):
747
+ for key, value in src_dict.items():
748
+ if key != '$' and key in target_dict:
749
+ _merge(target_dict[key], value)
750
+ else:
751
+ target_dict[key] = copy_lib.deepcopy(value)
752
+ _merge(self._trie, other._trie) # pylint: disable=protected-access
753
+
754
+ def union(
755
+ self, other: 'KeyPathSet', copy: bool = False) -> 'KeyPathSet':
756
+ """Returns the union KeyPathSet."""
757
+ x = self.copy()
758
+ x.update(other)
759
+ return x
760
+
761
+ def subtree(
762
+ self,
763
+ root_path: Union[int, str, KeyPath],
764
+ ) -> Optional['KeyPathSet']:
765
+ """Returns the relative paths of the sub-tree rooted at the given path.
766
+
767
+ Args:
768
+ root_path: A KeyPath for the root of the sub-tree.
769
+
770
+ Returns:
771
+ A KeyPathSet that contains all the child paths of the given root path.
772
+ Please note that the returned value share the same trie as the current
773
+ value. So addition/removal of paths in the returned value will also
774
+ affect the current value. If there is no child path under the given root
775
+ path, None will be returned.
776
+ """
777
+ root_path = KeyPath.from_value(root_path)
778
+ if not root_path:
779
+ return self
780
+ root = self._trie
781
+ for key in root_path.keys:
782
+ if key not in root:
783
+ return None
784
+ root = root[key]
785
+ ret = KeyPathSet()
786
+ ret._trie = root # pylint: disable=protected-access
787
+ return ret
788
+
789
+ def format(self, *args, **kwargs) -> str:
790
+ """Formats the set."""
791
+ return formatting.kvlist_str(
792
+ [
793
+ ('', list(self), [])
794
+ ],
795
+ label=self.__class__.__name__,
796
+ **kwargs
797
+ )
798
+
799
+ @classmethod
800
+ def from_value(
801
+ cls,
802
+ value: Union[Iterable[Union[int, str, KeyPath]], 'KeyPathSet'],
803
+ include_intermediate: bool = False,
804
+ ):
805
+ """Returns a KeyPathSet from a compatible value."""
806
+ if isinstance(value, KeyPathSet):
807
+ return value
808
+ if isinstance(value, (list, set, tuple)):
809
+ return cls(value, include_intermediate=include_intermediate)
810
+ raise ValueError(
811
+ f'Cannot convert {value!r} to KeyPathSet. '
812
+ f'Expected a list, set, tuple, or KeyPathSet.'
813
+ )
814
+
815
+
555
816
  class StrKey(metaclass=abc.ABCMeta):
556
817
  """Interface for classes whose instances can be treated as str in ``KeyPath``.
557
818
 
@@ -561,7 +822,7 @@ class StrKey(metaclass=abc.ABCMeta):
561
822
 
562
823
  Example::
563
824
 
564
- class MyKey(pg.object_utils.StrKey):
825
+ class MyKey(pg.utils.StrKey):
565
826
 
566
827
  def __init__(self, name):
567
828
  self.name = name
@@ -572,3 +833,11 @@ class StrKey(metaclass=abc.ABCMeta):
572
833
  path = pg.KeyPath(['a', MyKey('b')])
573
834
  print(str(path)) # Should print "a.__b__"
574
835
  """
836
+
837
+
838
+ def message_on_path(
839
+ message: str, path: KeyPath) -> str:
840
+ """Formats a message that is associated with a `KeyPath`."""
841
+ if path is None:
842
+ return message
843
+ return f'{message} (path={path})'