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.
- pyglove/core/__init__.py +40 -21
- 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 +312 -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 +53 -38
- pyglove/core/geno/base_test.py +2 -4
- pyglove/core/geno/categorical.py +36 -27
- pyglove/core/geno/custom.py +18 -15
- pyglove/core/geno/numerical.py +19 -16
- pyglove/core/geno/space.py +3 -4
- pyglove/core/hyper/base.py +6 -6
- pyglove/core/hyper/categorical.py +91 -52
- 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 +3 -5
- pyglove/core/hyper/dynamic_evaluation.py +3 -4
- 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/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 +4 -3
- pyglove/core/symbolic/__init__.py +4 -0
- pyglove/core/symbolic/base.py +200 -136
- pyglove/core/symbolic/base_test.py +17 -19
- pyglove/core/symbolic/boilerplate.py +4 -5
- pyglove/core/symbolic/class_wrapper.py +10 -14
- pyglove/core/symbolic/class_wrapper_test.py +2 -2
- pyglove/core/symbolic/compounding.py +2 -2
- pyglove/core/symbolic/compounding_test.py +2 -4
- pyglove/core/symbolic/contextual_object.py +288 -0
- pyglove/core/symbolic/contextual_object_test.py +327 -0
- pyglove/core/symbolic/dict.py +115 -87
- pyglove/core/symbolic/dict_test.py +188 -131
- pyglove/core/symbolic/diff.py +12 -12
- pyglove/core/symbolic/flags.py +1 -1
- pyglove/core/symbolic/functor.py +16 -15
- pyglove/core/symbolic/functor_test.py +2 -4
- pyglove/core/symbolic/inferred.py +2 -2
- pyglove/core/symbolic/list.py +70 -47
- pyglove/core/symbolic/list_test.py +117 -98
- pyglove/core/symbolic/object.py +59 -58
- pyglove/core/symbolic/object_test.py +143 -90
- pyglove/core/symbolic/origin.py +5 -7
- pyglove/core/symbolic/pure_symbolic.py +4 -3
- pyglove/core/symbolic/ref.py +33 -16
- pyglove/core/symbolic/ref_test.py +17 -0
- pyglove/core/tuning/local_backend.py +2 -2
- pyglove/core/tuning/protocols.py +3 -3
- pyglove/core/typing/annotation_conversion.py +8 -3
- pyglove/core/typing/annotation_conversion_test.py +8 -0
- pyglove/core/typing/callable_ext.py +11 -13
- pyglove/core/typing/callable_signature.py +22 -19
- pyglove/core/typing/callable_signature_test.py +3 -5
- pyglove/core/typing/class_schema.py +93 -54
- pyglove/core/typing/class_schema_test.py +4 -5
- pyglove/core/typing/custom_typing.py +5 -4
- pyglove/core/typing/key_specs.py +5 -7
- pyglove/core/typing/key_specs_test.py +4 -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 +287 -144
- pyglove/core/typing/value_specs_test.py +148 -25
- pyglove/core/utils/__init__.py +172 -0
- pyglove/core/{object_utils → utils}/common_traits.py +2 -2
- pyglove/core/{object_utils → utils}/common_traits_test.py +1 -3
- pyglove/core/utils/contextual.py +147 -0
- pyglove/core/utils/contextual_test.py +88 -0
- pyglove/core/{object_utils → utils}/docstr_utils_test.py +1 -3
- pyglove/core/{object_utils → utils}/error_utils.py +3 -3
- pyglove/core/{object_utils → utils}/error_utils_test.py +1 -1
- pyglove/core/{object_utils → utils}/formatting.py +1 -1
- pyglove/core/{object_utils → utils}/formatting_test.py +1 -2
- 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 +1 -1
- pyglove/core/{object_utils → utils}/json_conversion_test.py +1 -3
- pyglove/core/{object_utils → utils}/missing.py +2 -2
- 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/{object_utils → utils}/timing.py +21 -10
- pyglove/core/{object_utils → utils}/timing_test.py +14 -12
- pyglove/core/{object_utils → utils}/value_location.py +2 -2
- pyglove/core/{object_utils → utils}/value_location_test.py +2 -4
- pyglove/core/views/base.py +25 -29
- pyglove/core/views/html/base.py +15 -16
- pyglove/core/views/html/controls/base.py +46 -9
- pyglove/core/views/html/controls/label.py +13 -2
- pyglove/core/views/html/controls/label_test.py +27 -8
- pyglove/core/views/html/controls/progress_bar.py +3 -5
- pyglove/core/views/html/controls/progress_bar_test.py +2 -2
- pyglove/core/views/html/controls/tab.py +217 -66
- pyglove/core/views/html/controls/tab_test.py +46 -15
- pyglove/core/views/html/tree_view.py +39 -37
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/METADATA +17 -3
- pyglove-0.4.5.dev202501250807.dist-info/RECORD +218 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/WHEEL +1 -1
- pyglove/core/object_utils/__init__.py +0 -164
- pyglove-0.4.5.dev202411132359.dist-info/RECORD +0 -203
- /pyglove/core/{object_utils → utils}/docstr_utils.py +0 -0
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/LICENSE +0 -0
- {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.
|
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.
|
25
|
-
from pyglove.core.
|
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.
|
105
|
+
with pg.utils.catch_errors(
|
106
106
|
[
|
107
107
|
RuntimeErrror,
|
108
108
|
(ValueError, 'Input is wrong.')
|
@@ -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.
|
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.
|
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.
|
18
|
-
from pyglove.core.
|
19
|
-
from pyglove.core.
|
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.
|
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
|
-
|
53
|
-
postorder_visitor_fn: Postorder visitor function.
|
54
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
|
204
|
-
{'a
|
205
|
-
|
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.
|
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
|
-
|
491
|
-
|
492
|
-
|
493
|
-
If
|
494
|
-
|
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.
|
18
|
-
from pyglove.core.
|
19
|
-
from pyglove.core.
|
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.
|
18
|
-
from pyglove.core.
|
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.
|
18
|
-
from pyglove.core.
|
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)
|