pyglove 0.4.5.dev20240318__py3-none-any.whl → 0.4.5.dev202501132210__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyglove/core/__init__.py +54 -20
- pyglove/core/coding/__init__.py +42 -0
- pyglove/core/coding/errors.py +111 -0
- pyglove/core/coding/errors_test.py +98 -0
- pyglove/core/coding/execution.py +309 -0
- pyglove/core/coding/execution_test.py +333 -0
- pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
- pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
- pyglove/core/coding/parsing.py +153 -0
- pyglove/core/coding/parsing_test.py +150 -0
- pyglove/core/coding/permissions.py +100 -0
- pyglove/core/coding/permissions_test.py +93 -0
- pyglove/core/geno/base.py +54 -41
- pyglove/core/geno/base_test.py +2 -4
- pyglove/core/geno/categorical.py +37 -28
- pyglove/core/geno/custom.py +19 -16
- pyglove/core/geno/numerical.py +20 -17
- pyglove/core/geno/space.py +4 -5
- pyglove/core/hyper/base.py +6 -6
- pyglove/core/hyper/categorical.py +94 -55
- pyglove/core/hyper/custom.py +7 -7
- pyglove/core/hyper/custom_test.py +9 -10
- pyglove/core/hyper/derived.py +30 -22
- pyglove/core/hyper/derived_test.py +2 -4
- pyglove/core/hyper/dynamic_evaluation.py +5 -6
- pyglove/core/hyper/evolvable.py +57 -46
- pyglove/core/hyper/numerical.py +48 -24
- pyglove/core/hyper/numerical_test.py +9 -9
- pyglove/core/hyper/object_template.py +58 -46
- pyglove/core/io/__init__.py +1 -0
- pyglove/core/io/file_system.py +17 -7
- pyglove/core/io/file_system_test.py +2 -0
- pyglove/core/io/sequence.py +299 -0
- pyglove/core/io/sequence_test.py +124 -0
- pyglove/core/logging_test.py +0 -2
- pyglove/core/patching/object_factory.py +4 -4
- pyglove/core/patching/pattern_based.py +4 -4
- pyglove/core/patching/rule_based.py +17 -5
- pyglove/core/patching/rule_based_test.py +27 -4
- pyglove/core/symbolic/__init__.py +2 -7
- pyglove/core/symbolic/base.py +320 -183
- pyglove/core/symbolic/base_test.py +123 -19
- pyglove/core/symbolic/boilerplate.py +7 -13
- pyglove/core/symbolic/boilerplate_test.py +25 -23
- pyglove/core/symbolic/class_wrapper.py +48 -45
- pyglove/core/symbolic/class_wrapper_test.py +2 -2
- pyglove/core/symbolic/compounding.py +9 -15
- pyglove/core/symbolic/compounding_test.py +2 -4
- pyglove/core/symbolic/dict.py +154 -110
- pyglove/core/symbolic/dict_test.py +238 -130
- pyglove/core/symbolic/diff.py +199 -10
- pyglove/core/symbolic/diff_test.py +226 -0
- pyglove/core/symbolic/flags.py +1 -1
- pyglove/core/symbolic/functor.py +29 -26
- pyglove/core/symbolic/functor_test.py +102 -50
- pyglove/core/symbolic/inferred.py +2 -2
- pyglove/core/symbolic/list.py +81 -50
- pyglove/core/symbolic/list_test.py +119 -97
- pyglove/core/symbolic/object.py +225 -113
- pyglove/core/symbolic/object_test.py +320 -108
- pyglove/core/symbolic/origin.py +17 -14
- pyglove/core/symbolic/origin_test.py +4 -2
- pyglove/core/symbolic/pure_symbolic.py +4 -3
- pyglove/core/symbolic/ref.py +108 -21
- pyglove/core/symbolic/ref_test.py +93 -0
- pyglove/core/symbolic/symbolize_test.py +10 -2
- pyglove/core/tuning/local_backend.py +2 -2
- pyglove/core/tuning/protocols.py +3 -3
- pyglove/core/tuning/sample_test.py +3 -3
- pyglove/core/typing/__init__.py +14 -5
- pyglove/core/typing/annotation_conversion.py +43 -27
- pyglove/core/typing/annotation_conversion_test.py +23 -0
- pyglove/core/typing/callable_ext.py +241 -3
- pyglove/core/typing/callable_ext_test.py +255 -0
- pyglove/core/typing/callable_signature.py +510 -66
- pyglove/core/typing/callable_signature_test.py +619 -99
- pyglove/core/typing/class_schema.py +229 -154
- pyglove/core/typing/class_schema_test.py +149 -95
- pyglove/core/typing/custom_typing.py +5 -4
- pyglove/core/typing/inspect.py +63 -0
- pyglove/core/typing/inspect_test.py +39 -0
- pyglove/core/typing/key_specs.py +10 -11
- pyglove/core/typing/key_specs_test.py +7 -4
- pyglove/core/typing/type_conversion.py +4 -5
- pyglove/core/typing/type_conversion_test.py +12 -12
- pyglove/core/typing/typed_missing.py +6 -7
- pyglove/core/typing/typed_missing_test.py +7 -8
- pyglove/core/typing/value_specs.py +604 -362
- pyglove/core/typing/value_specs_test.py +328 -90
- pyglove/core/utils/__init__.py +164 -0
- pyglove/core/{object_utils → utils}/common_traits.py +3 -67
- pyglove/core/utils/common_traits_test.py +36 -0
- pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
- pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
- pyglove/core/{object_utils → utils}/error_utils.py +78 -9
- pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
- pyglove/core/utils/formatting.py +464 -0
- pyglove/core/utils/formatting_test.py +453 -0
- pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
- pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
- pyglove/core/{object_utils → utils}/json_conversion.py +177 -52
- pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
- pyglove/core/{object_utils → utils}/missing.py +3 -3
- pyglove/core/{object_utils → utils}/missing_test.py +2 -4
- pyglove/core/utils/text_color.py +128 -0
- pyglove/core/utils/text_color_test.py +94 -0
- pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
- pyglove/core/utils/timing.py +236 -0
- pyglove/core/utils/timing_test.py +154 -0
- pyglove/core/{object_utils → utils}/value_location.py +275 -6
- pyglove/core/utils/value_location_test.py +707 -0
- pyglove/core/views/__init__.py +32 -0
- pyglove/core/views/base.py +804 -0
- pyglove/core/views/base_test.py +580 -0
- pyglove/core/views/html/__init__.py +27 -0
- pyglove/core/views/html/base.py +547 -0
- pyglove/core/views/html/base_test.py +830 -0
- pyglove/core/views/html/controls/__init__.py +35 -0
- pyglove/core/views/html/controls/base.py +275 -0
- pyglove/core/views/html/controls/label.py +207 -0
- pyglove/core/views/html/controls/label_test.py +157 -0
- pyglove/core/views/html/controls/progress_bar.py +183 -0
- pyglove/core/views/html/controls/progress_bar_test.py +97 -0
- pyglove/core/views/html/controls/tab.py +320 -0
- pyglove/core/views/html/controls/tab_test.py +87 -0
- pyglove/core/views/html/controls/tooltip.py +99 -0
- pyglove/core/views/html/controls/tooltip_test.py +99 -0
- pyglove/core/views/html/tree_view.py +1517 -0
- pyglove/core/views/html/tree_view_test.py +1461 -0
- {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/METADATA +18 -4
- pyglove-0.4.5.dev202501132210.dist-info/RECORD +214 -0
- {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/WHEEL +1 -1
- pyglove/core/object_utils/__init__.py +0 -154
- pyglove/core/object_utils/common_traits_test.py +0 -82
- pyglove/core/object_utils/formatting.py +0 -234
- pyglove/core/object_utils/formatting_test.py +0 -223
- pyglove/core/object_utils/value_location_test.py +0 -385
- pyglove/core/symbolic/schema_utils.py +0 -327
- pyglove/core/symbolic/schema_utils_test.py +0 -57
- pyglove/core/typing/class_schema_utils.py +0 -202
- pyglove/core/typing/class_schema_utils_test.py +0 -194
- pyglove-0.4.5.dev20240318.dist-info/RECORD +0 -185
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,453 @@
|
|
1
|
+
# Copyright 2022 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 inspect
|
15
|
+
import unittest
|
16
|
+
|
17
|
+
from pyglove.core.utils import formatting
|
18
|
+
|
19
|
+
|
20
|
+
class Foo(formatting.Formattable):
|
21
|
+
|
22
|
+
def format(
|
23
|
+
self, compact: bool = False, verbose: bool = True, **kwargs):
|
24
|
+
return f'{self.__class__.__name__}(compact={compact}, verbose={verbose})'
|
25
|
+
|
26
|
+
|
27
|
+
class Bar(formatting.Formattable):
|
28
|
+
|
29
|
+
def __init__(self, foo: Foo):
|
30
|
+
self._foo = foo
|
31
|
+
|
32
|
+
def format(
|
33
|
+
self, compact: bool = False, verbose: bool = True,
|
34
|
+
root_indent: int = 0, **kwargs):
|
35
|
+
foo_str = self._foo.format(
|
36
|
+
compact=compact, verbose=verbose, root_indent=root_indent + 1)
|
37
|
+
return f'{self.__class__.__name__}(foo={foo_str})'
|
38
|
+
|
39
|
+
|
40
|
+
class FormattableTest(unittest.TestCase):
|
41
|
+
|
42
|
+
def test_formattable(self):
|
43
|
+
foo = Foo()
|
44
|
+
self.assertEqual(repr(foo), 'Foo(compact=True, verbose=True)')
|
45
|
+
self.assertEqual(str(foo), 'Foo(compact=False, verbose=True)')
|
46
|
+
|
47
|
+
def test_formattable_with_custom_format(self):
|
48
|
+
class Baz(Foo):
|
49
|
+
__str_format_kwargs__ = {'compact': False, 'verbose': False}
|
50
|
+
__repr_format_kwargs__ = {'compact': True, 'verbose': False}
|
51
|
+
|
52
|
+
bar = Baz()
|
53
|
+
self.assertEqual(repr(bar), 'Baz(compact=True, verbose=False)')
|
54
|
+
self.assertEqual(str(bar), 'Baz(compact=False, verbose=False)')
|
55
|
+
|
56
|
+
def test_formattable_with_context_managers(self):
|
57
|
+
foo = Foo()
|
58
|
+
with formatting.str_format(verbose=False):
|
59
|
+
with formatting.repr_format(compact=False):
|
60
|
+
self.assertEqual(repr(foo), 'Foo(compact=False, verbose=True)')
|
61
|
+
self.assertEqual(str(foo), 'Foo(compact=False, verbose=False)')
|
62
|
+
|
63
|
+
|
64
|
+
class StringHelperTest(unittest.TestCase):
|
65
|
+
"""Tests for string helper methods in formatting."""
|
66
|
+
|
67
|
+
def test_raw_text(self):
|
68
|
+
raw = formatting.RawText('abc')
|
69
|
+
self.assertEqual(formatting.format(raw), 'abc')
|
70
|
+
self.assertEqual(formatting.format(raw, compact=True), 'abc')
|
71
|
+
self.assertEqual(raw, formatting.RawText('abc'))
|
72
|
+
self.assertEqual(raw, 'abc')
|
73
|
+
self.assertNotEqual(raw, formatting.RawText('abcd'))
|
74
|
+
self.assertNotEqual(raw, 'abcd')
|
75
|
+
|
76
|
+
def test_camel_to_snake(self):
|
77
|
+
self.assertEqual(formatting.camel_to_snake('foo'), 'foo')
|
78
|
+
self.assertEqual(formatting.camel_to_snake('Foo'), 'foo')
|
79
|
+
self.assertEqual(formatting.camel_to_snake('FooBar'), 'foo_bar')
|
80
|
+
self.assertEqual(formatting.camel_to_snake('AI'), 'ai')
|
81
|
+
self.assertEqual(formatting.camel_to_snake('AIMessage'), 'ai_message')
|
82
|
+
self.assertEqual(formatting.camel_to_snake('ABCMeta'), 'abc_meta')
|
83
|
+
self.assertEqual(formatting.camel_to_snake('ABC123Meta'), 'abc123_meta')
|
84
|
+
|
85
|
+
def test_special_format_support(self):
|
86
|
+
|
87
|
+
class NewLine:
|
88
|
+
def _repr_html_(self):
|
89
|
+
return '<hr>'
|
90
|
+
|
91
|
+
def __str__(self):
|
92
|
+
return 'NewLine()'
|
93
|
+
|
94
|
+
def __repr__(self):
|
95
|
+
return 'NewLine()'
|
96
|
+
|
97
|
+
v = NewLine()
|
98
|
+
self.assertEqual(formatting.str_ext(v), 'NewLine()')
|
99
|
+
def _method(attr_name: str):
|
100
|
+
def fn(v, root_indent):
|
101
|
+
del root_indent
|
102
|
+
f = getattr(v, attr_name, None)
|
103
|
+
return f() if f is not None else None
|
104
|
+
return fn
|
105
|
+
|
106
|
+
self.assertEqual(
|
107
|
+
formatting.str_ext(v, custom_format=_method('_repr_html_')),
|
108
|
+
'<hr>'
|
109
|
+
)
|
110
|
+
self.assertEqual(
|
111
|
+
formatting.str_ext(v, custom_format=_method('_repr_xml_')),
|
112
|
+
'NewLine()'
|
113
|
+
)
|
114
|
+
self.assertEqual(formatting.repr_ext(v), 'NewLine()')
|
115
|
+
self.assertEqual(
|
116
|
+
formatting.repr_ext(v, custom_format=_method('_repr_html_')),
|
117
|
+
'<hr>'
|
118
|
+
)
|
119
|
+
self.assertEqual(
|
120
|
+
formatting.repr_ext(v, custom_format=_method('_repr_xml_')),
|
121
|
+
'NewLine()'
|
122
|
+
)
|
123
|
+
|
124
|
+
def test_kvlist_str(self):
|
125
|
+
self.assertEqual(
|
126
|
+
formatting.kvlist_str([
|
127
|
+
('', 'foo', None),
|
128
|
+
('a', 1, None),
|
129
|
+
('b', 'str', (None, 'str')),
|
130
|
+
('c', [1, 2, 3], False),
|
131
|
+
], label='Foo'),
|
132
|
+
'Foo(\'foo\', a=1, c=[1, 2, 3])'
|
133
|
+
)
|
134
|
+
|
135
|
+
self.assertEqual(
|
136
|
+
formatting.kvlist_str([
|
137
|
+
('', 'foo', None),
|
138
|
+
('a', 1, None),
|
139
|
+
('b', 'str', (None, 'str')),
|
140
|
+
('c', True, False),
|
141
|
+
]),
|
142
|
+
'\'foo\', a=1, c=True'
|
143
|
+
)
|
144
|
+
|
145
|
+
self.assertEqual(
|
146
|
+
formatting.kvlist_str([
|
147
|
+
('', 'foo', None),
|
148
|
+
('a', 1, None),
|
149
|
+
('b', 'str', (None, 'str')),
|
150
|
+
('c', True, False),
|
151
|
+
], label='Foo', compact=False),
|
152
|
+
'Foo(\n \'foo\',\n a=1,\n c=True\n)'
|
153
|
+
)
|
154
|
+
|
155
|
+
self.assertEqual(
|
156
|
+
formatting.kvlist_str([
|
157
|
+
('', 'foo', None),
|
158
|
+
('a', 1, None),
|
159
|
+
('b', 'str', (None, 'str')),
|
160
|
+
('c', dict(x=1), False),
|
161
|
+
], compact=False),
|
162
|
+
'\'foo\',\na=1,\nc={\n \'x\': 1\n}'
|
163
|
+
)
|
164
|
+
|
165
|
+
self.assertEqual(
|
166
|
+
formatting.kvlist_str([
|
167
|
+
('', 'foo', 'foo')
|
168
|
+
], label='Foo', compact=False),
|
169
|
+
'Foo()'
|
170
|
+
)
|
171
|
+
|
172
|
+
class Foo: # pylint: disable=redefined-outer-name
|
173
|
+
def _repr_xml_(self):
|
174
|
+
return '<foo/>'
|
175
|
+
|
176
|
+
def __str__(self):
|
177
|
+
return 'Foo()'
|
178
|
+
|
179
|
+
self.assertEqual(
|
180
|
+
formatting.kvlist_str([
|
181
|
+
('', Foo(), None)
|
182
|
+
], compact=False),
|
183
|
+
'Foo()'
|
184
|
+
)
|
185
|
+
def _custom_format(v, root_indent):
|
186
|
+
del root_indent
|
187
|
+
f = getattr(v, '_repr_xml_', None)
|
188
|
+
return f() if f is not None else None
|
189
|
+
|
190
|
+
self.assertEqual(
|
191
|
+
formatting.kvlist_str([
|
192
|
+
('', Foo(), None)
|
193
|
+
], compact=False, custom_format=_custom_format),
|
194
|
+
'<foo/>'
|
195
|
+
)
|
196
|
+
self.assertEqual(
|
197
|
+
formatting.kvlist_str([
|
198
|
+
('', (Foo(), 1), None)
|
199
|
+
], compact=True, custom_format=_custom_format),
|
200
|
+
'(<foo/>, 1)'
|
201
|
+
)
|
202
|
+
|
203
|
+
def test_quote_if_str(self):
|
204
|
+
self.assertEqual(formatting.quote_if_str(1), 1)
|
205
|
+
self.assertEqual(formatting.quote_if_str('foo'), '\'foo\'')
|
206
|
+
self.assertEqual(formatting.quote_if_str('foo\'s\na'), '"foo\'s\\na"')
|
207
|
+
|
208
|
+
def test_comma_delimited_str(self):
|
209
|
+
self.assertEqual(
|
210
|
+
formatting.comma_delimited_str([1, 2, 'abc']), '1, 2, \'abc\'')
|
211
|
+
|
212
|
+
def test_auto_plural(self):
|
213
|
+
self.assertEqual(formatting.auto_plural(2, 'number'), 'numbers')
|
214
|
+
self.assertEqual(formatting.auto_plural(2, 'was', 'were'), 'were')
|
215
|
+
|
216
|
+
|
217
|
+
class FormatTest(unittest.TestCase):
|
218
|
+
"""Tests for formatting.format."""
|
219
|
+
|
220
|
+
def test_formattable(self):
|
221
|
+
|
222
|
+
class A(formatting.Formattable):
|
223
|
+
|
224
|
+
def format(self, compact=True, **kwargs):
|
225
|
+
if compact:
|
226
|
+
return 'A()'
|
227
|
+
else:
|
228
|
+
return 'A(...)'
|
229
|
+
|
230
|
+
self.assertEqual(str(A()), 'A(...)')
|
231
|
+
self.assertEqual(repr(A()), 'A()')
|
232
|
+
|
233
|
+
def test_simple_types(self):
|
234
|
+
self.assertEqual(formatting.format(True, compact=True), 'True')
|
235
|
+
self.assertEqual(formatting.format(1, compact=True), '1')
|
236
|
+
self.assertEqual(formatting.format(1.0, compact=True), '1.0')
|
237
|
+
self.assertEqual(formatting.format('foo', compact=True), '\'foo\'')
|
238
|
+
self.assertEqual(
|
239
|
+
formatting.format('foo\'s\na', compact=True), '"foo\'s\\na"')
|
240
|
+
|
241
|
+
# Compact=False has no impact on simple types.
|
242
|
+
self.assertEqual(formatting.format(True, compact=False), 'True')
|
243
|
+
self.assertEqual(formatting.format(1, compact=False), '1')
|
244
|
+
self.assertEqual(formatting.format(1.0, compact=False), '1.0')
|
245
|
+
self.assertEqual(formatting.format('foo', compact=False), '\'foo\'')
|
246
|
+
self.assertEqual(
|
247
|
+
formatting.format('foo\'s\na', compact=False), '"foo\'s\\na"')
|
248
|
+
|
249
|
+
# Verbose has no impact on simple types.
|
250
|
+
self.assertEqual(formatting.format(True, verbose=True), 'True')
|
251
|
+
self.assertEqual(formatting.format(1, verbose=True), '1')
|
252
|
+
self.assertEqual(formatting.format(1.0, verbose=True), '1.0')
|
253
|
+
self.assertEqual(formatting.format('foo', verbose=True), '\'foo\'')
|
254
|
+
self.assertEqual(
|
255
|
+
formatting.format('foo\'s\na', verbose=True), '"foo\'s\\na"')
|
256
|
+
|
257
|
+
# Root indent has no impact on simple types.
|
258
|
+
self.assertEqual(formatting.format(True, root_indent=4), 'True')
|
259
|
+
self.assertEqual(formatting.format(1, root_indent=4), '1')
|
260
|
+
self.assertEqual(formatting.format(1.0, root_indent=4), '1.0')
|
261
|
+
self.assertEqual(formatting.format('foo', root_indent=4), '\'foo\'')
|
262
|
+
self.assertEqual(
|
263
|
+
formatting.format('foo\'s\na', root_indent=4), '"foo\'s\\na"')
|
264
|
+
|
265
|
+
def test_complex_types(self):
|
266
|
+
|
267
|
+
class CustomFormattable(formatting.Formattable):
|
268
|
+
"""Custom formattable."""
|
269
|
+
|
270
|
+
def format(self, custom_param=None, **kwargs):
|
271
|
+
return f'CustomFormattable({custom_param})'
|
272
|
+
|
273
|
+
class A:
|
274
|
+
pass
|
275
|
+
|
276
|
+
self.assertEqual(
|
277
|
+
formatting.format(
|
278
|
+
{
|
279
|
+
'a': CustomFormattable(),
|
280
|
+
'b': {
|
281
|
+
'c': [1, 2, 3],
|
282
|
+
'd': ['foo', 'bar\na', 3, 4, 5]
|
283
|
+
}
|
284
|
+
},
|
285
|
+
compact=True,
|
286
|
+
custom_param='foo'),
|
287
|
+
"{'a': CustomFormattable(foo), 'b': {'c': [1, 2, 3], "
|
288
|
+
"'d': ['foo', 'bar\\na', 3, 4, 5]}}")
|
289
|
+
|
290
|
+
self.assertEqual(
|
291
|
+
formatting.format(
|
292
|
+
{
|
293
|
+
'a': A(),
|
294
|
+
'b': {
|
295
|
+
'c': [1, 2, 3],
|
296
|
+
'd': ['foo', 'bar\na', 3, 4, 5]
|
297
|
+
}
|
298
|
+
},
|
299
|
+
compact=False,
|
300
|
+
list_wrap_threshold=15,
|
301
|
+
strip_object_id=True),
|
302
|
+
inspect.cleandoc("""{
|
303
|
+
'a': A(...),
|
304
|
+
'b': {
|
305
|
+
'c': [1, 2, 3],
|
306
|
+
'd': [
|
307
|
+
'foo',
|
308
|
+
'bar\\na',
|
309
|
+
3,
|
310
|
+
4,
|
311
|
+
5
|
312
|
+
]
|
313
|
+
}
|
314
|
+
}"""))
|
315
|
+
|
316
|
+
def test_include_exclude_keys(self):
|
317
|
+
"""Test format with excluded keys."""
|
318
|
+
|
319
|
+
class A:
|
320
|
+
pass
|
321
|
+
|
322
|
+
class B(formatting.Formattable):
|
323
|
+
"""Custom formattable."""
|
324
|
+
|
325
|
+
def format(
|
326
|
+
self, custom_param=None,
|
327
|
+
include_keys=None, exclude_keys=None, **kwargs):
|
328
|
+
exclude_keys = exclude_keys or set()
|
329
|
+
kv = dict(a=1, b=2, c=3)
|
330
|
+
def _should_include(k):
|
331
|
+
if include_keys:
|
332
|
+
return k in include_keys
|
333
|
+
return k not in exclude_keys
|
334
|
+
kv_pairs = [(k, v, None) for k, v in kv.items() if _should_include(k)]
|
335
|
+
return f'B({formatting.kvlist_str(kv_pairs, compact=True)})'
|
336
|
+
|
337
|
+
self.assertEqual(
|
338
|
+
formatting.format(B(), compact=False, include_keys=set(['a', 'c'])),
|
339
|
+
'B(a=1, c=3)')
|
340
|
+
self.assertEqual(
|
341
|
+
formatting.format(B(), compact=False, exclude_keys=set(['a', 'c'])),
|
342
|
+
'B(b=2)')
|
343
|
+
self.assertEqual(
|
344
|
+
formatting.format(
|
345
|
+
{
|
346
|
+
'a': A(),
|
347
|
+
'b': B(),
|
348
|
+
'c': {
|
349
|
+
'd': [1, 2, 3],
|
350
|
+
}
|
351
|
+
},
|
352
|
+
compact=False,
|
353
|
+
list_wrap_threshold=15,
|
354
|
+
strip_object_id=True,
|
355
|
+
# 'a' should be removed, but 'b.a', 'c.d' should be kept as they are
|
356
|
+
# not at the top level.
|
357
|
+
exclude_keys=set(['a', 'd'])),
|
358
|
+
inspect.cleandoc("""{
|
359
|
+
'b': B(a=1, b=2, c=3),
|
360
|
+
'c': {
|
361
|
+
'd': [1, 2, 3]
|
362
|
+
}
|
363
|
+
}"""))
|
364
|
+
|
365
|
+
def test_custom_format(self):
|
366
|
+
|
367
|
+
class A:
|
368
|
+
|
369
|
+
def _repr_xml_(self):
|
370
|
+
return '<a/>'
|
371
|
+
|
372
|
+
def __repr__(self):
|
373
|
+
return 'A()'
|
374
|
+
|
375
|
+
def __str__(self):
|
376
|
+
return 'AA()'
|
377
|
+
|
378
|
+
self.assertEqual(formatting.format(A), str(A))
|
379
|
+
self.assertEqual(formatting.format(A()), 'AA()')
|
380
|
+
def _custom_format(v, root_indent):
|
381
|
+
del root_indent
|
382
|
+
f = getattr(v, '_repr_xml_', None)
|
383
|
+
return f() if f else None
|
384
|
+
|
385
|
+
self.assertEqual(
|
386
|
+
formatting.format(A(), custom_format=_custom_format), '<a/>'
|
387
|
+
)
|
388
|
+
self.assertEqual(
|
389
|
+
formatting.format(A(), compact=True),
|
390
|
+
'A()'
|
391
|
+
)
|
392
|
+
self.assertEqual(
|
393
|
+
formatting.format(A(), compact=True, custom_format=_custom_format),
|
394
|
+
'<a/>'
|
395
|
+
)
|
396
|
+
self.assertEqual(
|
397
|
+
formatting.format([A()], compact=True, custom_format=_custom_format),
|
398
|
+
'[<a/>]'
|
399
|
+
)
|
400
|
+
self.assertEqual(
|
401
|
+
formatting.format((A(), 1), compact=True, custom_format=_custom_format),
|
402
|
+
'(<a/>, 1)'
|
403
|
+
)
|
404
|
+
self.assertEqual(
|
405
|
+
formatting.format(
|
406
|
+
dict(x=A()), compact=True, custom_format=_custom_format
|
407
|
+
),
|
408
|
+
'{\'x\': <a/>}'
|
409
|
+
)
|
410
|
+
|
411
|
+
def test_markdown(self):
|
412
|
+
|
413
|
+
class A(formatting.Formattable):
|
414
|
+
def __init__(self, x):
|
415
|
+
self.x = x
|
416
|
+
|
417
|
+
def format(self, *args, **kwargs):
|
418
|
+
del args, kwargs
|
419
|
+
return 'A(' + formatting.format(self.x) + ')'
|
420
|
+
|
421
|
+
with formatting.str_format(markdown=True):
|
422
|
+
self.assertEqual(str(A(1)), '`A(1)`')
|
423
|
+
self.assertEqual(str(A([A(1)])), '`A([A(1)])`')
|
424
|
+
self.assertEqual(
|
425
|
+
formatting.format([1], compact=True, markdown=True), '`[1]`'
|
426
|
+
)
|
427
|
+
|
428
|
+
self.assertEqual(
|
429
|
+
formatting.format(
|
430
|
+
[1, 2, 3], list_wrap_threshold=5, compact=False, markdown=True
|
431
|
+
),
|
432
|
+
inspect.cleandoc("""
|
433
|
+
```
|
434
|
+
[
|
435
|
+
1,
|
436
|
+
2,
|
437
|
+
3
|
438
|
+
]
|
439
|
+
```
|
440
|
+
"""),
|
441
|
+
)
|
442
|
+
|
443
|
+
def test_max_len(self):
|
444
|
+
self.assertEqual(
|
445
|
+
formatting.format('foo', max_str_len=2), '\'fo...\''
|
446
|
+
)
|
447
|
+
self.assertEqual(
|
448
|
+
formatting.format(b'bar', max_bytes_len=2), 'b\'ba...\''
|
449
|
+
)
|
450
|
+
|
451
|
+
|
452
|
+
if __name__ == '__main__':
|
453
|
+
unittest.main()
|
@@ -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):
|