pyglove 0.4.5.dev202411132359__py3-none-any.whl → 0.4.5.dev202501250807__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 (118) hide show
  1. pyglove/core/__init__.py +40 -21
  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 +312 -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 +53 -38
  14. pyglove/core/geno/base_test.py +2 -4
  15. pyglove/core/geno/categorical.py +36 -27
  16. pyglove/core/geno/custom.py +18 -15
  17. pyglove/core/geno/numerical.py +19 -16
  18. pyglove/core/geno/space.py +3 -4
  19. pyglove/core/hyper/base.py +6 -6
  20. pyglove/core/hyper/categorical.py +91 -52
  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 +3 -5
  25. pyglove/core/hyper/dynamic_evaluation.py +3 -4
  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/logging_test.py +0 -2
  31. pyglove/core/patching/object_factory.py +4 -4
  32. pyglove/core/patching/pattern_based.py +4 -4
  33. pyglove/core/patching/rule_based.py +4 -3
  34. pyglove/core/symbolic/__init__.py +4 -0
  35. pyglove/core/symbolic/base.py +200 -136
  36. pyglove/core/symbolic/base_test.py +17 -19
  37. pyglove/core/symbolic/boilerplate.py +4 -5
  38. pyglove/core/symbolic/class_wrapper.py +10 -14
  39. pyglove/core/symbolic/class_wrapper_test.py +2 -2
  40. pyglove/core/symbolic/compounding.py +2 -2
  41. pyglove/core/symbolic/compounding_test.py +2 -4
  42. pyglove/core/symbolic/contextual_object.py +288 -0
  43. pyglove/core/symbolic/contextual_object_test.py +327 -0
  44. pyglove/core/symbolic/dict.py +115 -87
  45. pyglove/core/symbolic/dict_test.py +188 -131
  46. pyglove/core/symbolic/diff.py +12 -12
  47. pyglove/core/symbolic/flags.py +1 -1
  48. pyglove/core/symbolic/functor.py +16 -15
  49. pyglove/core/symbolic/functor_test.py +2 -4
  50. pyglove/core/symbolic/inferred.py +2 -2
  51. pyglove/core/symbolic/list.py +70 -47
  52. pyglove/core/symbolic/list_test.py +117 -98
  53. pyglove/core/symbolic/object.py +59 -58
  54. pyglove/core/symbolic/object_test.py +143 -90
  55. pyglove/core/symbolic/origin.py +5 -7
  56. pyglove/core/symbolic/pure_symbolic.py +4 -3
  57. pyglove/core/symbolic/ref.py +33 -16
  58. pyglove/core/symbolic/ref_test.py +17 -0
  59. pyglove/core/tuning/local_backend.py +2 -2
  60. pyglove/core/tuning/protocols.py +3 -3
  61. pyglove/core/typing/annotation_conversion.py +8 -3
  62. pyglove/core/typing/annotation_conversion_test.py +8 -0
  63. pyglove/core/typing/callable_ext.py +11 -13
  64. pyglove/core/typing/callable_signature.py +22 -19
  65. pyglove/core/typing/callable_signature_test.py +3 -5
  66. pyglove/core/typing/class_schema.py +93 -54
  67. pyglove/core/typing/class_schema_test.py +4 -5
  68. pyglove/core/typing/custom_typing.py +5 -4
  69. pyglove/core/typing/key_specs.py +5 -7
  70. pyglove/core/typing/key_specs_test.py +4 -4
  71. pyglove/core/typing/type_conversion.py +4 -5
  72. pyglove/core/typing/type_conversion_test.py +12 -12
  73. pyglove/core/typing/typed_missing.py +6 -7
  74. pyglove/core/typing/typed_missing_test.py +7 -8
  75. pyglove/core/typing/value_specs.py +287 -144
  76. pyglove/core/typing/value_specs_test.py +148 -25
  77. pyglove/core/utils/__init__.py +172 -0
  78. pyglove/core/{object_utils → utils}/common_traits.py +2 -2
  79. pyglove/core/{object_utils → utils}/common_traits_test.py +1 -3
  80. pyglove/core/utils/contextual.py +147 -0
  81. pyglove/core/utils/contextual_test.py +88 -0
  82. pyglove/core/{object_utils → utils}/docstr_utils_test.py +1 -3
  83. pyglove/core/{object_utils → utils}/error_utils.py +3 -3
  84. pyglove/core/{object_utils → utils}/error_utils_test.py +1 -1
  85. pyglove/core/{object_utils → utils}/formatting.py +1 -1
  86. pyglove/core/{object_utils → utils}/formatting_test.py +1 -2
  87. pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
  88. pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
  89. pyglove/core/{object_utils → utils}/json_conversion.py +1 -1
  90. pyglove/core/{object_utils → utils}/json_conversion_test.py +1 -3
  91. pyglove/core/{object_utils → utils}/missing.py +2 -2
  92. pyglove/core/{object_utils → utils}/missing_test.py +2 -4
  93. pyglove/core/utils/text_color.py +128 -0
  94. pyglove/core/utils/text_color_test.py +94 -0
  95. pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
  96. pyglove/core/{object_utils → utils}/timing.py +21 -10
  97. pyglove/core/{object_utils → utils}/timing_test.py +14 -12
  98. pyglove/core/{object_utils → utils}/value_location.py +2 -2
  99. pyglove/core/{object_utils → utils}/value_location_test.py +2 -4
  100. pyglove/core/views/base.py +25 -29
  101. pyglove/core/views/html/base.py +15 -16
  102. pyglove/core/views/html/controls/base.py +46 -9
  103. pyglove/core/views/html/controls/label.py +13 -2
  104. pyglove/core/views/html/controls/label_test.py +27 -8
  105. pyglove/core/views/html/controls/progress_bar.py +3 -5
  106. pyglove/core/views/html/controls/progress_bar_test.py +2 -2
  107. pyglove/core/views/html/controls/tab.py +217 -66
  108. pyglove/core/views/html/controls/tab_test.py +46 -15
  109. pyglove/core/views/html/tree_view.py +39 -37
  110. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/METADATA +17 -3
  111. pyglove-0.4.5.dev202501250807.dist-info/RECORD +218 -0
  112. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/WHEEL +1 -1
  113. pyglove/core/object_utils/__init__.py +0 -164
  114. pyglove-0.4.5.dev202411132359.dist-info/RECORD +0 -203
  115. /pyglove/core/{object_utils → utils}/docstr_utils.py +0 -0
  116. /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
  117. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/LICENSE +0 -0
  118. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,147 @@
1
+ # Copyright 2025 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Injecting and manipulating values through context managers."""
15
+
16
+ import contextlib
17
+ import dataclasses
18
+ import threading
19
+ from typing import Any, Callable, ContextManager, Iterator, Optional
20
+
21
+ from pyglove.core.utils import missing
22
+
23
+ RAISE_IF_HAS_ERROR = (missing.MISSING_VALUE,)
24
+ _TLS_KEY_CONTEXTUAL_OVERRIDES = '__contextual_overrides__'
25
+ _global_contextual_overrides = threading.local()
26
+
27
+
28
+ @dataclasses.dataclass(frozen=True)
29
+ class ContextualOverride:
30
+ """Value marker for contextual override for an attribute."""
31
+
32
+ # Overridden value.
33
+ value: Any
34
+
35
+ # If True, this override will apply to both current scope and nested scope,
36
+ # meaning current `pg.contextual_override` will take precedence over all
37
+ # nested `pg.contextual_override` on this attribute.
38
+ cascade: bool = False
39
+
40
+ # If True, this override will apply to attributes that already have values.
41
+ override_attrs: bool = False
42
+
43
+
44
+ def contextual_override(
45
+ *,
46
+ cascade: bool = False,
47
+ override_attrs: bool = False,
48
+ **variables,
49
+ ) -> ContextManager[dict[str, ContextualOverride]]:
50
+ """Context manager to provide contextual values under a scope.
51
+
52
+ Please be aware that contextual value override are per-thread. If you want
53
+ to propagate the contextual value override to other threads, please obtain
54
+ a wrapper function for a user function using
55
+ `pg.with_contextual_override(func)`.
56
+
57
+ Args:
58
+ cascade: If True, this override will apply to both current scope and nested
59
+ scope, meaning that this `pg.contextual_override` will take precedence
60
+ over all nested `pg.contextual_override` on the overriden variables.
61
+ override_attrs: If True, this override will apply to attributes that already
62
+ have values. Otherwise overridden variables will only be used for
63
+ contextual attributes whose values are not present.
64
+ **variables: Key/values as override for contextual attributes.
65
+
66
+ Returns:
67
+ A dict of attribute names to their contextual overrides.
68
+ """
69
+ vs = {}
70
+ for k, v in variables.items():
71
+ if not isinstance(v, ContextualOverride):
72
+ v = ContextualOverride(v, cascade, override_attrs)
73
+ vs[k] = v
74
+ return contextual_scope(_global_contextual_overrides, **vs)
75
+
76
+
77
+ def with_contextual_override(func: Callable[..., Any]) -> Callable[..., Any]:
78
+ """Wraps a user function with the access to the current contextual override.
79
+
80
+ The wrapped function can be called from another thread.
81
+
82
+ Args:
83
+ func: The user function to be wrapped.
84
+
85
+ Returns:
86
+ A wrapper function that have the access to the current contextual override,
87
+ which can be called from another thread.
88
+ """
89
+ with contextual_override() as current_context:
90
+ pass
91
+
92
+ def _func(*args, **kwargs) -> Any:
93
+ with contextual_override(**current_context):
94
+ return func(*args, **kwargs)
95
+
96
+ return _func
97
+
98
+
99
+ def get_contextual_override(var_name: str) -> Optional[ContextualOverride]:
100
+ """Returns the overriden contextual value in current scope."""
101
+ return get_scoped_value(_global_contextual_overrides, var_name)
102
+
103
+
104
+ def contextual_value(var_name: str, default: Any = RAISE_IF_HAS_ERROR) -> Any:
105
+ """Returns the value of a variable defined in `pg.contextual_override`."""
106
+ override = get_contextual_override(var_name)
107
+ if override is None:
108
+ if default == RAISE_IF_HAS_ERROR:
109
+ raise KeyError(f'{var_name!r} does not exist in current context.')
110
+ return default
111
+ return override.value
112
+
113
+
114
+ def all_contextual_values() -> dict[str, Any]:
115
+ """Returns all values provided from `pg.contextual_override` in scope."""
116
+ overrides = getattr(
117
+ _global_contextual_overrides, _TLS_KEY_CONTEXTUAL_OVERRIDES, {}
118
+ )
119
+ return {k: v.value for k, v in overrides.items()}
120
+
121
+
122
+ @contextlib.contextmanager
123
+ def contextual_scope(
124
+ tls: threading.local, **variables
125
+ ) -> Iterator[dict[str, ContextualOverride]]:
126
+ """Context manager to set variables within a scope."""
127
+ previous_values = getattr(tls, _TLS_KEY_CONTEXTUAL_OVERRIDES, {})
128
+ current_values = dict(previous_values)
129
+ for k, v in variables.items():
130
+ old_v = current_values.get(k, None)
131
+ if old_v and old_v.cascade:
132
+ v = old_v
133
+ current_values[k] = v
134
+ try:
135
+ setattr(tls, _TLS_KEY_CONTEXTUAL_OVERRIDES, current_values)
136
+ yield current_values
137
+ finally:
138
+ setattr(tls, _TLS_KEY_CONTEXTUAL_OVERRIDES, previous_values)
139
+
140
+
141
+ def get_scoped_value(
142
+ tls: threading.local, var_name: str, default: Any = None
143
+ ) -> ContextualOverride:
144
+ """Gets the value for requested variable from current scope."""
145
+ scoped_values = getattr(tls, _TLS_KEY_CONTEXTUAL_OVERRIDES, {})
146
+ return scoped_values.get(var_name, default)
147
+
@@ -0,0 +1,88 @@
1
+ # Copyright 2025 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import concurrent.futures
15
+ import unittest
16
+ from pyglove.core.utils import contextual
17
+
18
+
19
+ class ContextualTest(unittest.TestCase):
20
+
21
+ def test_contextual_override(self):
22
+ with contextual.contextual_override(x=3, y=3, z=3) as parent_override:
23
+ self.assertEqual(
24
+ parent_override,
25
+ dict(
26
+ x=contextual.ContextualOverride(
27
+ 3, cascade=False, override_attrs=False
28
+ ),
29
+ y=contextual.ContextualOverride(
30
+ 3, cascade=False, override_attrs=False
31
+ ),
32
+ z=contextual.ContextualOverride(
33
+ 3, cascade=False, override_attrs=False
34
+ ),
35
+ ),
36
+ )
37
+ self.assertEqual(
38
+ contextual.get_contextual_override('y'),
39
+ contextual.ContextualOverride(3, cascade=False, override_attrs=False),
40
+ )
41
+ self.assertEqual(contextual.contextual_value('x'), 3)
42
+ self.assertIsNone(contextual.contextual_value('f', None))
43
+ with self.assertRaisesRegex(KeyError, '.* does not exist'):
44
+ contextual.contextual_value('f')
45
+
46
+ self.assertEqual(contextual.all_contextual_values(), dict(x=3, y=3, z=3))
47
+
48
+ # Test nested contextual override with override_attrs=True (default).
49
+ with contextual.contextual_override(
50
+ y=4, z=4, override_attrs=True) as nested_override:
51
+ self.assertEqual(
52
+ nested_override,
53
+ dict(
54
+ x=contextual.ContextualOverride(
55
+ 3, cascade=False, override_attrs=False
56
+ ),
57
+ y=contextual.ContextualOverride(
58
+ 4, cascade=False, override_attrs=True
59
+ ),
60
+ z=contextual.ContextualOverride(
61
+ 4, cascade=False, override_attrs=True
62
+ ),
63
+ ),
64
+ )
65
+
66
+ # Test nested contextual override with cascade=True.
67
+ with contextual.contextual_override(x=3, y=3, z=3, cascade=True):
68
+ with contextual.contextual_override(y=4, z=4, cascade=True):
69
+ self.assertEqual(contextual.contextual_value('x'), 3)
70
+ self.assertEqual(contextual.contextual_value('y'), 3)
71
+ self.assertEqual(contextual.contextual_value('z'), 3)
72
+
73
+ def test_with_contextual_override(self):
74
+ def func(i):
75
+ del i
76
+ return contextual.contextual_value('x')
77
+
78
+ pool = concurrent.futures.ThreadPoolExecutor()
79
+ with contextual.contextual_override(x=3):
80
+ self.assertEqual(contextual.with_contextual_override(func)(0), 3)
81
+ self.assertEqual(
82
+ list(pool.map(contextual.with_contextual_override(func), range(1))),
83
+ [3]
84
+ )
85
+
86
+
87
+ if __name__ == '__main__':
88
+ unittest.main()
@@ -11,11 +11,9 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- """Tests for pyglove.object_utils.docstr_utils."""
15
-
16
14
  import inspect
17
15
  import unittest
18
- from pyglove.core.object_utils import docstr_utils
16
+ from pyglove.core.utils import docstr_utils
19
17
 
20
18
 
21
19
  class DocStrTest(unittest.TestCase):
@@ -21,8 +21,8 @@ import sys
21
21
  import traceback
22
22
  from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
23
23
 
24
- from pyglove.core.object_utils import formatting
25
- from pyglove.core.object_utils import json_conversion
24
+ from pyglove.core.utils import formatting
25
+ from pyglove.core.utils import json_conversion
26
26
 
27
27
 
28
28
  @dataclasses.dataclass(frozen=True)
@@ -102,7 +102,7 @@ def catch_errors(
102
102
 
103
103
  Examples::
104
104
 
105
- with pg.object_utils.catch_errors(
105
+ with pg.utils.catch_errors(
106
106
  [
107
107
  RuntimeErrror,
108
108
  (ValueError, 'Input is wrong.')
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
  import inspect
15
15
  import unittest
16
- from pyglove.core.object_utils import error_utils
16
+ from pyglove.core.utils import error_utils
17
17
 
18
18
 
19
19
  class ErrorInfoTest(unittest.TestCase):
@@ -18,7 +18,7 @@ import enum
18
18
  import io
19
19
  import sys
20
20
  from typing import Any, Callable, ContextManager, Dict, List, Optional, Sequence, Set, Tuple
21
- from pyglove.core.object_utils import thread_local
21
+ from pyglove.core.utils import thread_local
22
22
 
23
23
 
24
24
  _TLS_STR_FORMAT_KWARGS = '_str_format_kwargs'
@@ -11,11 +11,10 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- """Tests for pyglove.object_utils.formatting."""
15
14
  import inspect
16
15
  import unittest
17
16
 
18
- from pyglove.core.object_utils import formatting
17
+ from pyglove.core.utils import formatting
19
18
 
20
19
 
21
20
  class Foo(formatting.Formattable):
@@ -14,9 +14,9 @@
14
14
  """Operating hierarchical object."""
15
15
 
16
16
  from typing import Any, Callable, Dict, List, Optional, Tuple, Union
17
- from pyglove.core.object_utils import common_traits
18
- from pyglove.core.object_utils.missing import MISSING_VALUE
19
- from pyglove.core.object_utils.value_location import KeyPath
17
+ from pyglove.core.utils import common_traits
18
+ from pyglove.core.utils.missing import MISSING_VALUE
19
+ from pyglove.core.utils.value_location import KeyPath
20
20
 
21
21
 
22
22
  def traverse(value: Any,
@@ -33,7 +33,7 @@ def traverse(value: Any,
33
33
  print(path)
34
34
 
35
35
  tree = {'a': [{'c': [1, 2]}, {'d': {'g': (3, 4)}}], 'b': 'foo'}
36
- pg.object_utils.traverse(tree, preorder_visit)
36
+ pg.utils.traverse(tree, preorder_visit)
37
37
 
38
38
  # Should print:
39
39
  # 'a'
@@ -48,10 +48,10 @@ def traverse(value: Any,
48
48
 
49
49
  Args:
50
50
  value: A maybe hierarchical value to traverse.
51
- preorder_visitor_fn: Preorder visitor function.
52
- Function signature is (path, value) -> should_continue.
53
- postorder_visitor_fn: Postorder visitor function.
54
- Function signature is (path, value) -> should_continue.
51
+ preorder_visitor_fn: Preorder visitor function. Function signature is (path,
52
+ value) -> should_continue.
53
+ postorder_visitor_fn: Postorder visitor function. Function signature is
54
+ (path, value) -> should_continue.
55
55
  root_path: The key path of the root value.
56
56
 
57
57
  Returns:
@@ -111,7 +111,7 @@ def transform(value: Any,
111
111
  'e': 'bar',
112
112
  'f': 4
113
113
  }
114
- output = pg.object_utils.transform(inputs, _remove_int)
114
+ output = pg.utils.transform(inputs, _remove_int)
115
115
  assert output == {
116
116
  'a': {
117
117
  'c': ['bar'],
@@ -123,11 +123,11 @@ def transform(value: Any,
123
123
  Args:
124
124
  value: Any python value type. If value is a list of dict, transformation
125
125
  will occur recursively.
126
- transform_fn: Transform function in signature
127
- (path, value) -> new value
128
- If new value is MISSING_VALUE, key will be deleted.
126
+ transform_fn: Transform function in signature (path, value) -> new value If
127
+ new value is MISSING_VALUE, key will be deleted.
129
128
  root_path: KeyPath of the root.
130
129
  inplace: If True, perform transformation in place.
130
+
131
131
  Returns:
132
132
  Transformed value.
133
133
  """
@@ -186,7 +186,7 @@ def flatten(src: Any, flatten_complex_keys: bool = True) -> Any:
186
186
  'b': 'hi',
187
187
  'c': None
188
188
  }
189
- output = pg.object_utils.flatten(inputs)
189
+ output = pg.utils.flatten(inputs)
190
190
  assert output == {
191
191
  'a.e': 1,
192
192
  'a.f[0].g': 2,
@@ -200,9 +200,9 @@ def flatten(src: Any, flatten_complex_keys: bool = True) -> Any:
200
200
  Args:
201
201
  src: source value to flatten.
202
202
  flatten_complex_keys: if True, complex keys such as 'x.y' will be flattened
203
- as 'x'.'y'. For example:
204
- {'a': {'b.c': 1}} will be flattened into {'a.b.c': 1} if this flag is on,
205
- otherwise it will be flattened as {'a[b.c]': 1}.
203
+ as 'x'.'y'. For example: {'a': {'b.c': 1}} will be flattened into
204
+ {'a.b.c': 1} if this flag is on, otherwise it will be flattened as
205
+ {'a[b.c]': 1}.
206
206
 
207
207
  Returns:
208
208
  For primitive value types, `src` itself will be returned.
@@ -464,7 +464,7 @@ def merge(value_list: List[Any],
464
464
  'f': 10
465
465
  }
466
466
  }
467
- output = pg.object_utils.merge([original, patch])
467
+ output = pg.utils.merge([original, patch])
468
468
  assert output == {
469
469
  'a': 1,
470
470
  # b is updated.
@@ -486,14 +486,12 @@ def merge(value_list: List[Any],
486
486
  value. The merge process will keep input values intact.
487
487
  merge_fn: A function to handle value merge that will be called for updated
488
488
  or added keys. If a branch is added/updated, the root of branch will be
489
- passed to merge_fn.
490
- the signature of function is:
491
- `(path, left_value, right_value) -> final_value`
492
- If a key is only present in src dict, old_value is MISSING_VALUE;
493
- If a key is only present in dest dict, new_value is MISSING_VALUE;
494
- otherwise both new_value and old_value are filled.
495
- If final_value is MISSING_VALUE for a path, it will be removed from its
496
- parent collection.
489
+ passed to merge_fn. the signature of function is: `(path, left_value,
490
+ right_value) -> final_value` If a key is only present in src dict,
491
+ old_value is MISSING_VALUE; If a key is only present in dest dict,
492
+ new_value is MISSING_VALUE; otherwise both new_value and old_value are
493
+ filled. If final_value is MISSING_VALUE for a path, it will be removed
494
+ from its parent collection.
497
495
 
498
496
  Returns:
499
497
  A merged value.
@@ -11,12 +11,10 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- """Tests for pyglove.object_utils.hierarchical."""
15
-
16
14
  import unittest
17
- from pyglove.core.object_utils import common_traits
18
- from pyglove.core.object_utils import hierarchical
19
- from pyglove.core.object_utils import value_location
15
+ from pyglove.core.utils import common_traits
16
+ from pyglove.core.utils import hierarchical
17
+ from pyglove.core.utils import value_location
20
18
 
21
19
 
22
20
  class TraverseTest(unittest.TestCase):
@@ -37,7 +37,7 @@ JSONPrimitiveType = Union[int, float, bool, str]
37
37
  # pytype doesn't support recursion. Use Any instead of 'JSONValueType'
38
38
  # in List and Dict.
39
39
  JSONListType = List[Any]
40
- JSONDictType = Dict[str, Any]
40
+ JSONDictType = Dict[Union[str, int], Any]
41
41
  JSONValueType = Union[JSONPrimitiveType, JSONListType, JSONDictType]
42
42
 
43
43
  # pylint: enable=invalid-name
@@ -11,13 +11,11 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- """Tests for pyglove.object_utils.json_conversion."""
15
-
16
14
  import abc
17
15
  import typing
18
16
  import unittest
19
- from pyglove.core.object_utils import json_conversion
20
17
  from pyglove.core.typing import inspect as pg_inspect
18
+ from pyglove.core.utils import json_conversion
21
19
 
22
20
 
23
21
  class X:
@@ -14,8 +14,8 @@
14
14
  """Representing missing value for a field."""
15
15
 
16
16
  from typing import Any, Dict
17
- from pyglove.core.object_utils import formatting
18
- from pyglove.core.object_utils import json_conversion
17
+ from pyglove.core.utils import formatting
18
+ from pyglove.core.utils import json_conversion
19
19
 
20
20
 
21
21
  class MissingValue(formatting.Formattable, json_conversion.JSONConvertible):
@@ -11,11 +11,9 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- """Tests for pyglove.object_utils.missing."""
15
-
16
14
  import unittest
17
- from pyglove.core.object_utils import json_conversion
18
- from pyglove.core.object_utils import missing
15
+ from pyglove.core.utils import json_conversion
16
+ from pyglove.core.utils import missing
19
17
 
20
18
 
21
19
  class MissingValueTest(unittest.TestCase):
@@ -0,0 +1,128 @@
1
+ # Copyright 2025 The PyGlove Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Utility library for text coloring."""
15
+
16
+ import re
17
+ from typing import List, Optional
18
+
19
+ try:
20
+ import termcolor # pylint: disable=g-import-not-at-top
21
+ except ImportError:
22
+ termcolor = None
23
+
24
+
25
+ # Regular expression for ANSI color characters.
26
+ _ANSI_COLOR_REGEX = re.compile(r'\x1b\[[0-9;]*m')
27
+
28
+
29
+ def decolor(text: str) -> str:
30
+ """De-colors a string that may contains ANSI color characters."""
31
+ return re.sub(_ANSI_COLOR_REGEX, '', text)
32
+
33
+
34
+ def colored(
35
+ text: str,
36
+ color: Optional[str] = None,
37
+ background: Optional[str] = None,
38
+ styles: Optional[List[str]] = None,
39
+ ) -> str:
40
+ """Returns the colored text with ANSI color characters.
41
+
42
+ Args:
43
+ text: A string that may or may not already has ANSI color characters.
44
+ color: A string for text colors. Applicable values are:
45
+ 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'.
46
+ background: A string for background colors. Applicable values are:
47
+ 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'.
48
+ styles: A list of strings for applying styles on the text.
49
+ Applicable values are:
50
+ 'bold', 'dark', 'underline', 'blink', 'reverse', 'concealed'.
51
+
52
+ Returns:
53
+ A string with ANSI color characters embracing the entire text.
54
+ """
55
+ if not termcolor:
56
+ return text
57
+ return termcolor.colored(
58
+ text,
59
+ color=color,
60
+ on_color=('on_' + background) if background else None,
61
+ attrs=styles
62
+ )
63
+
64
+
65
+ def colored_block(
66
+ text: str,
67
+ block_start: str,
68
+ block_end: str,
69
+ color: Optional[str] = None,
70
+ background: Optional[str] = None,
71
+ styles: Optional[List[str]] = None,
72
+ ) -> str:
73
+ """Apply colors to text blocks.
74
+
75
+ Args:
76
+ text: A string that may or may not already has ANSI color characters.
77
+ block_start: A string that signals the start of a block. E.g. '{{'
78
+ block_end: A string that signals the end of a block. E.g. '}}'.
79
+ color: A string for text colors. Applicable values are:
80
+ 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'.
81
+ background: A string for background colors. Applicable values are:
82
+ 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'.
83
+ styles: A list of strings for applying styles on the text.
84
+ Applicable values are:
85
+ 'bold', 'dark', 'underline', 'blink', 'reverse', 'concealed'.
86
+
87
+ Returns:
88
+ A string with ANSI color characters embracing the matched text blocks.
89
+ """
90
+ if not color and not background and not styles:
91
+ return text
92
+
93
+ s = []
94
+ start_index = 0
95
+ end_index = 0
96
+ previous_color = None
97
+
98
+ def write_nonblock_text(text: str, previous_color: Optional[str]):
99
+ if previous_color:
100
+ s.append(previous_color)
101
+ s.append(text)
102
+
103
+ while start_index < len(text):
104
+ start_index = text.find(block_start, end_index)
105
+ if start_index == -1:
106
+ write_nonblock_text(text[end_index:], previous_color)
107
+ break
108
+
109
+ # Deal with text since last block.
110
+ since_last_block = text[end_index:start_index]
111
+ write_nonblock_text(since_last_block, previous_color)
112
+ colors = re.findall(_ANSI_COLOR_REGEX, since_last_block)
113
+ if colors:
114
+ previous_color = colors[-1]
115
+
116
+ # Match block.
117
+ end_index = text.find(block_end, start_index + len(block_start))
118
+ if end_index == -1:
119
+ write_nonblock_text(text[start_index:], previous_color)
120
+ break
121
+ end_index += len(block_end)
122
+
123
+ # Write block text.
124
+ block = text[start_index:end_index]
125
+ block = colored(
126
+ block, color=color, background=background, styles=styles)
127
+ s.append(block)
128
+ return ''.join(s)