pyglove 0.4.5.dev20240319__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.dev20240319.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.dev20240319.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.dev20240319.dist-info/RECORD +0 -185
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,707 @@
|
|
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 unittest
|
15
|
+
from pyglove.core.utils import formatting
|
16
|
+
from pyglove.core.utils import value_location
|
17
|
+
|
18
|
+
|
19
|
+
KeyPath = value_location.KeyPath
|
20
|
+
KeyPathSet = value_location.KeyPathSet
|
21
|
+
|
22
|
+
|
23
|
+
class KeyPathTest(unittest.TestCase):
|
24
|
+
"""Tests for class KeyPath."""
|
25
|
+
|
26
|
+
def test_basics(self):
|
27
|
+
# Root element.
|
28
|
+
r = KeyPath()
|
29
|
+
self.assertTrue(r.is_root)
|
30
|
+
self.assertFalse(r)
|
31
|
+
self.assertEqual(r, '')
|
32
|
+
self.assertEqual(r.depth, 0)
|
33
|
+
self.assertEqual(len(r), 0)
|
34
|
+
self.assertEqual(r.path, '')
|
35
|
+
with self.assertRaisesRegex(
|
36
|
+
KeyError, 'Parent of a root KeyPath does not exist.'):
|
37
|
+
_ = r.parent
|
38
|
+
|
39
|
+
with self.assertRaisesRegex(
|
40
|
+
KeyError, 'Key of root KeyPath does not exist.'):
|
41
|
+
_ = r.key
|
42
|
+
|
43
|
+
# 1-level deep.
|
44
|
+
a = KeyPath('a')
|
45
|
+
self.assertFalse(a.is_root)
|
46
|
+
self.assertEqual(a.key, 'a')
|
47
|
+
self.assertEqual(a.path, 'a')
|
48
|
+
self.assertEqual(a, 'a') # Relative path compare.
|
49
|
+
self.assertNotEqual(a, '')
|
50
|
+
self.assertEqual(a.depth, 1)
|
51
|
+
self.assertEqual(len(a), 1)
|
52
|
+
self.assertEqual(a.parent, r)
|
53
|
+
|
54
|
+
a2 = KeyPath(0)
|
55
|
+
self.assertFalse(a2.is_root)
|
56
|
+
self.assertEqual(a2.key, 0)
|
57
|
+
with formatting.str_format(markdown=True):
|
58
|
+
self.assertEqual(str(a2), '[0]')
|
59
|
+
with formatting.repr_format(markdown=True):
|
60
|
+
self.assertEqual(repr(a2), '[0]')
|
61
|
+
self.assertEqual(a2.path, '[0]')
|
62
|
+
self.assertEqual(a2, '[0]')
|
63
|
+
self.assertNotEqual(a2, '')
|
64
|
+
self.assertEqual(a2.depth, 1)
|
65
|
+
self.assertEqual(len(a2), 1)
|
66
|
+
self.assertEqual(a2.parent, r)
|
67
|
+
|
68
|
+
a3 = KeyPath('x.y')
|
69
|
+
self.assertFalse(a3.is_root)
|
70
|
+
self.assertEqual(a3.key, 'x.y')
|
71
|
+
self.assertEqual(a3.path, '[x.y]')
|
72
|
+
self.assertEqual(a3.path_str(False), 'x.y')
|
73
|
+
self.assertEqual(a3, '[x.y]') # Relative path compare.
|
74
|
+
self.assertNotEqual(a3, 'x.y')
|
75
|
+
self.assertEqual(a3.depth, 1)
|
76
|
+
self.assertEqual(len(a3), 1)
|
77
|
+
self.assertEqual(a3.parent, r)
|
78
|
+
|
79
|
+
# Multiple levels.
|
80
|
+
b = KeyPath([1, 'b'])
|
81
|
+
self.assertEqual(b, '[1].b')
|
82
|
+
self.assertEqual(b.path, '[1].b')
|
83
|
+
self.assertNotEqual(a, b)
|
84
|
+
self.assertEqual(len(b), 2)
|
85
|
+
self.assertEqual(b.parent, '[1]')
|
86
|
+
|
87
|
+
c = KeyPath('c', b)
|
88
|
+
self.assertEqual(c.key, 'c')
|
89
|
+
self.assertEqual(c, '[1].b.c')
|
90
|
+
self.assertEqual(c.keys, [1, 'b', 'c'])
|
91
|
+
self.assertEqual(len(c), 3)
|
92
|
+
self.assertEqual(c.parent, b)
|
93
|
+
|
94
|
+
d = KeyPath(['d', 0], c)
|
95
|
+
self.assertEqual(d.key, 0)
|
96
|
+
self.assertEqual(d, '[1].b.c.d[0]')
|
97
|
+
self.assertEqual(d.keys, [1, 'b', 'c', 'd', 0])
|
98
|
+
self.assertEqual(d.parent, '[1].b.c.d')
|
99
|
+
self.assertEqual(len(d), 5)
|
100
|
+
|
101
|
+
d2 = KeyPath(('d', 0), c)
|
102
|
+
self.assertEqual(d, d2)
|
103
|
+
|
104
|
+
def test_complex_key_type(self):
|
105
|
+
|
106
|
+
class A:
|
107
|
+
|
108
|
+
def __init__(self, text):
|
109
|
+
self._text = text
|
110
|
+
|
111
|
+
def __str__(self):
|
112
|
+
return f'A({self._text})'
|
113
|
+
|
114
|
+
p = KeyPath([A('a'), A('b'), 'c'])
|
115
|
+
self.assertEqual(p.path, '[A(a)][A(b)].c')
|
116
|
+
|
117
|
+
# Key may have '.' in their string form.
|
118
|
+
p = KeyPath([A('a.*'), A('$b')])
|
119
|
+
self.assertEqual(p.path, '[A(a.*)][A($b)]')
|
120
|
+
|
121
|
+
# NOTE: We cannot really parse KeyPath with complex types.
|
122
|
+
|
123
|
+
class B(value_location.StrKey):
|
124
|
+
"""Class that implements StrKey will be treated as string key."""
|
125
|
+
|
126
|
+
def __init__(self, text):
|
127
|
+
self._text = text
|
128
|
+
|
129
|
+
def __str__(self):
|
130
|
+
return f'B({self._text})'
|
131
|
+
|
132
|
+
p = KeyPath([B('a'), B('b'), 'c'])
|
133
|
+
self.assertEqual(p.path, 'B(a).B(b).c')
|
134
|
+
|
135
|
+
def test_parse(self):
|
136
|
+
"""Test KeyPath.parse method."""
|
137
|
+
self.assertEqual(KeyPath.parse('a').keys, ['a'])
|
138
|
+
self.assertEqual(len(KeyPath.parse('')), 0)
|
139
|
+
|
140
|
+
self.assertEqual(KeyPath.parse('a').keys, ['a'])
|
141
|
+
self.assertEqual(KeyPath.parse('[a ]').keys, ['a '])
|
142
|
+
self.assertEqual(KeyPath.parse('[0].a').keys, [0, 'a'])
|
143
|
+
self.assertEqual(
|
144
|
+
KeyPath.parse('[0][1].a').keys, [0, 1, 'a'])
|
145
|
+
self.assertEqual(
|
146
|
+
KeyPath.parse('a.b[1].c').keys, ['a', 'b', 1, 'c'])
|
147
|
+
self.assertEqual(
|
148
|
+
KeyPath.parse('a[x[0]].b[y.z].c').keys,
|
149
|
+
['a', 'x[0]', 'b', 'y.z', 'c'])
|
150
|
+
|
151
|
+
with self.assertRaisesRegex(
|
152
|
+
ValueError, '\'path_str\' must be a string type.'):
|
153
|
+
KeyPath.parse(0)
|
154
|
+
|
155
|
+
with self.assertRaisesRegex(
|
156
|
+
ValueError,
|
157
|
+
'KeyPath parse failed: unmatched open bracket at position 0'):
|
158
|
+
KeyPath.parse('[0')
|
159
|
+
|
160
|
+
with self.assertRaisesRegex(
|
161
|
+
ValueError,
|
162
|
+
'KeyPath parse failed: unmatched open bracket at position 0'):
|
163
|
+
KeyPath.parse('[[0]')
|
164
|
+
|
165
|
+
with self.assertRaisesRegex(
|
166
|
+
ValueError,
|
167
|
+
'KeyPath parse failed: unmatched close bracket at position 3'):
|
168
|
+
KeyPath.parse('[0]]')
|
169
|
+
|
170
|
+
def test_from_value(self):
|
171
|
+
"""Test KeyPath.from_value."""
|
172
|
+
self.assertEqual(
|
173
|
+
KeyPath.from_value('x.y'),
|
174
|
+
KeyPath(['x', 'y']))
|
175
|
+
|
176
|
+
self.assertEqual(
|
177
|
+
KeyPath.from_value(1),
|
178
|
+
KeyPath([1]))
|
179
|
+
|
180
|
+
path = KeyPath(['x'])
|
181
|
+
self.assertIs(
|
182
|
+
KeyPath.from_value(path),
|
183
|
+
path)
|
184
|
+
|
185
|
+
with self.assertRaisesRegex(
|
186
|
+
ValueError, '.* is not a valid KeyPath equivalence'):
|
187
|
+
KeyPath.from_value(0.1)
|
188
|
+
|
189
|
+
def test_arithmetics(self):
|
190
|
+
"""Test KeyPath arithmetics."""
|
191
|
+
|
192
|
+
# Test operator +.
|
193
|
+
self.assertEqual(KeyPath('a') + 'b.c', 'a.b.c')
|
194
|
+
self.assertEqual(KeyPath('a') + '[0].b', 'a[0].b')
|
195
|
+
self.assertEqual(KeyPath('a') + None, 'a')
|
196
|
+
self.assertEqual(KeyPath('a') + 1, 'a[1]')
|
197
|
+
self.assertEqual(
|
198
|
+
KeyPath('a') + KeyPath('b'), 'a.b')
|
199
|
+
self.assertEqual(KeyPath.parse('a.b') + 1.0, 'a.b[1.0]')
|
200
|
+
|
201
|
+
# Test operator -.
|
202
|
+
self.assertEqual(
|
203
|
+
KeyPath('a') - KeyPath('a'), '')
|
204
|
+
self.assertEqual(KeyPath('a') - 'a', '')
|
205
|
+
self.assertEqual(KeyPath('a') - '', 'a')
|
206
|
+
self.assertEqual(KeyPath('a') - None, 'a')
|
207
|
+
self.assertEqual(
|
208
|
+
KeyPath('a') - KeyPath(), 'a')
|
209
|
+
self.assertEqual(KeyPath.parse('a.b.c.d') - 'a.b', 'c.d')
|
210
|
+
self.assertEqual(KeyPath.parse('[0].a') - 0, 'a')
|
211
|
+
|
212
|
+
with self.assertRaisesRegex(
|
213
|
+
ValueError, 'KeyPath subtraction failed: .* are in different subtree.'):
|
214
|
+
_ = KeyPath('a') - 'b'
|
215
|
+
|
216
|
+
with self.assertRaisesRegex(
|
217
|
+
ValueError, 'KeyPath subtraction failed: .* are in different subtree.'):
|
218
|
+
_ = KeyPath.parse('a.b') - 'a.c'
|
219
|
+
|
220
|
+
with self.assertRaisesRegex(
|
221
|
+
ValueError, 'KeyPath subtraction failed: .* are in different subtree.'):
|
222
|
+
_ = KeyPath.parse('a[0]') - 'a[1]'
|
223
|
+
|
224
|
+
with self.assertRaisesRegex(
|
225
|
+
ValueError, 'KeyPath subtraction failed: .* is an ancestor'):
|
226
|
+
_ = KeyPath.parse('a.b') - 'a.b.c'
|
227
|
+
|
228
|
+
with self.assertRaisesRegex(TypeError, 'Cannot subtract KeyPath'):
|
229
|
+
_ = KeyPath.parse('a.b') - 1.0
|
230
|
+
|
231
|
+
def test_is_relative_to(self):
|
232
|
+
self.assertTrue(
|
233
|
+
KeyPath.parse('a.b.c').is_relative_to(
|
234
|
+
KeyPath())
|
235
|
+
)
|
236
|
+
self.assertTrue(
|
237
|
+
KeyPath.parse('a.b.c').is_relative_to(
|
238
|
+
KeyPath.parse('a.b'))
|
239
|
+
)
|
240
|
+
self.assertTrue(
|
241
|
+
KeyPath.parse('a.b.c').is_relative_to(
|
242
|
+
KeyPath.parse('a.b.c'))
|
243
|
+
)
|
244
|
+
self.assertFalse(
|
245
|
+
KeyPath.parse('a.b').is_relative_to(
|
246
|
+
KeyPath.parse('a.b.c'))
|
247
|
+
)
|
248
|
+
self.assertFalse(
|
249
|
+
KeyPath.parse('a.b.d').is_relative_to(
|
250
|
+
KeyPath.parse('a.b.c'))
|
251
|
+
)
|
252
|
+
|
253
|
+
def test_hash(self):
|
254
|
+
self.assertIn(KeyPath.parse('a.b.c'), {'a.b.c': 1})
|
255
|
+
self.assertNotIn(KeyPath.parse('a.b.c'), {'a.b': 1})
|
256
|
+
|
257
|
+
def test_comparison(self):
|
258
|
+
keypath = KeyPath.parse
|
259
|
+
# Equality should only hold true for KeyPaths that are identical.
|
260
|
+
self.assertEqual(
|
261
|
+
KeyPath(), KeyPath.parse(''))
|
262
|
+
self.assertEqual(keypath('a[1][2].b[3][4]'), keypath('a[1][2].b[3][4]'))
|
263
|
+
self.assertNotEqual(keypath('a[1][2].b[3][4]'), keypath('a[1][2].a[3][4]'))
|
264
|
+
self.assertNotEqual(keypath('a[1][2].b[3][4]'), keypath('a[1][2].b[4][4]'))
|
265
|
+
# Earlier keys in the path should be prioritized over later ones.
|
266
|
+
self.assertLess(KeyPath(), 'a')
|
267
|
+
self.assertLess(KeyPath(), keypath('a'))
|
268
|
+
self.assertLess(keypath('a'), keypath('a.a'))
|
269
|
+
self.assertLess(keypath('a.a'), keypath('a.b'))
|
270
|
+
self.assertGreater(keypath('a'), KeyPath())
|
271
|
+
self.assertGreater(keypath('a[1].b'), keypath('a[1].a'))
|
272
|
+
self.assertGreater(keypath('a[1].a'), keypath('a[1]'))
|
273
|
+
# Numbers should be compared numerically - not lexicographically.
|
274
|
+
self.assertLessEqual(keypath('a[1]'), keypath('a[2]'))
|
275
|
+
self.assertLessEqual(keypath('a[2]'), keypath('a[10]'))
|
276
|
+
self.assertGreaterEqual(keypath('a[10]'), keypath('a[2]'))
|
277
|
+
self.assertGreaterEqual(keypath('a[2]'), keypath('a[1]'))
|
278
|
+
# It should be possible to compare numeric keys with string keys.
|
279
|
+
self.assertLess(keypath('a[1]'), keypath('a.b'))
|
280
|
+
self.assertGreater(keypath('a.b'), keypath('a[1]'))
|
281
|
+
self.assertLessEqual(keypath('a[1]'), keypath('a.b'))
|
282
|
+
self.assertGreaterEqual(keypath('a.b'), keypath('a[1]'))
|
283
|
+
|
284
|
+
class CustomKey(value_location.StrKey):
|
285
|
+
|
286
|
+
def __init__(self, text):
|
287
|
+
self.text = text
|
288
|
+
|
289
|
+
def __lt__(self, other):
|
290
|
+
if isinstance(other, CustomKey):
|
291
|
+
return self.text < other.text
|
292
|
+
return False
|
293
|
+
|
294
|
+
self.assertLess(
|
295
|
+
KeyPath([CustomKey('a'), 'b']),
|
296
|
+
KeyPath([CustomKey('b'), 'b']))
|
297
|
+
|
298
|
+
with self.assertRaisesRegex(
|
299
|
+
TypeError, 'Comparison is not supported between instances'):
|
300
|
+
_ = KeyPath() < 1
|
301
|
+
|
302
|
+
def test_query(self):
|
303
|
+
|
304
|
+
def query_shall_succeed(path_str, obj, expected_value, use_inferred=False):
|
305
|
+
self.assertEqual(
|
306
|
+
KeyPath.parse(path_str).query(obj, use_inferred),
|
307
|
+
expected_value)
|
308
|
+
|
309
|
+
def query_shall_fail(path_str,
|
310
|
+
obj,
|
311
|
+
error='Cannot query sub-key .* of object .*'):
|
312
|
+
with self.assertRaisesRegex(KeyError, error):
|
313
|
+
KeyPath.parse(path_str).query(obj)
|
314
|
+
|
315
|
+
def get_shall_succeed(path_str, obj, default, expected_value):
|
316
|
+
self.assertEqual(
|
317
|
+
KeyPath.parse(path_str).get(obj, default),
|
318
|
+
expected_value)
|
319
|
+
|
320
|
+
def assert_exists(path_str, obj, should_exists):
|
321
|
+
self.assertEqual(
|
322
|
+
KeyPath.parse(path_str).exists(obj), should_exists)
|
323
|
+
|
324
|
+
# Query at root level.
|
325
|
+
query_shall_succeed('', 1, 1)
|
326
|
+
query_shall_succeed('', None, None)
|
327
|
+
query_shall_succeed('', [1, 2], [1, 2])
|
328
|
+
query_shall_succeed('', {'a': 'foo'}, {'a': 'foo'})
|
329
|
+
|
330
|
+
# Query simple types with more than 1 depth.
|
331
|
+
query_shall_fail('a', 1)
|
332
|
+
query_shall_fail('[0]', None)
|
333
|
+
|
334
|
+
# Query complex types.
|
335
|
+
class Foo:
|
336
|
+
"""Custom object."""
|
337
|
+
|
338
|
+
def __init__(self, values):
|
339
|
+
self._values = values
|
340
|
+
|
341
|
+
def __getitem__(self, key):
|
342
|
+
return self._values[key]
|
343
|
+
|
344
|
+
# NOTE(daiyip): __len__ and __contains__ is intentional omitted.
|
345
|
+
|
346
|
+
class Bar:
|
347
|
+
"""Custom object with sym_getattr."""
|
348
|
+
|
349
|
+
def __init__(self, **kwargs):
|
350
|
+
self._map = kwargs
|
351
|
+
|
352
|
+
def sym_hasattr(self, name):
|
353
|
+
return name in self._map
|
354
|
+
|
355
|
+
def sym_getattr(self, name):
|
356
|
+
return self._map[name]
|
357
|
+
|
358
|
+
def sym_inferred(self, key):
|
359
|
+
return Bar(z=self._map[key])
|
360
|
+
|
361
|
+
def __contains__(self, key):
|
362
|
+
return key in self._map
|
363
|
+
|
364
|
+
def __eq__(self, other):
|
365
|
+
return self._map == other._map
|
366
|
+
|
367
|
+
def __ne__(self, other):
|
368
|
+
return not self.__eq__(other)
|
369
|
+
|
370
|
+
src = {'a': [{'c': 'foo'},
|
371
|
+
{'d': [1, 2]}],
|
372
|
+
'b': True,
|
373
|
+
'e': Foo([1, 2, 3]),
|
374
|
+
'f': Bar(x=0, y=1)}
|
375
|
+
|
376
|
+
query_shall_succeed('', src, src)
|
377
|
+
query_shall_succeed('a', src, src['a'])
|
378
|
+
query_shall_succeed('a[0]', src, src['a'][0])
|
379
|
+
query_shall_succeed('a[0].c', src, src['a'][0]['c'])
|
380
|
+
query_shall_succeed('a[1].d[1]', src, src['a'][1]['d'][1])
|
381
|
+
query_shall_succeed('b', src, src['b'])
|
382
|
+
query_shall_succeed('f.x', src, 0)
|
383
|
+
query_shall_succeed('f.x.z', src, Bar(z=0), use_inferred=True)
|
384
|
+
|
385
|
+
query_shall_fail('c', src, 'Path .* does not exist: key .* is absent')
|
386
|
+
query_shall_fail('a.c', src, 'Path .* does not exist: key .* is absent')
|
387
|
+
query_shall_fail('a[2]', src, 'Path .* does not exist: key .* is absent')
|
388
|
+
query_shall_fail('a[1].e', src, 'Path .* does not exist: key .* is absent')
|
389
|
+
query_shall_fail(
|
390
|
+
'e[0]', src, 'Cannot query index .* on object .*: '
|
391
|
+
'\'__len__\' does not exist')
|
392
|
+
query_shall_fail(
|
393
|
+
'e.f', src, 'Cannot query key .* on object .*: '
|
394
|
+
'\'__contains__\' does not exist')
|
395
|
+
query_shall_fail(
|
396
|
+
'f.z', src, 'Path .* does not exist: key .* is absent')
|
397
|
+
query_shall_fail('f.x.z', src, 'Cannot query sub-key .* does not exist')
|
398
|
+
|
399
|
+
# Test get method.
|
400
|
+
get_shall_succeed('', src, None, src)
|
401
|
+
get_shall_succeed('a[1].d[1]', src, None, src['a'][1]['d'][1])
|
402
|
+
get_shall_succeed('c', src, None, None)
|
403
|
+
get_shall_succeed('b.c', src, 1, 1)
|
404
|
+
|
405
|
+
# Test exists method.
|
406
|
+
assert_exists('', src, True)
|
407
|
+
assert_exists('a[1].d[1]', src, True)
|
408
|
+
assert_exists('c', src, False)
|
409
|
+
assert_exists('b.c', src, False)
|
410
|
+
|
411
|
+
def test_message_on_path(self):
|
412
|
+
self.assertEqual(value_location.message_on_path('hi.', None), 'hi.')
|
413
|
+
self.assertEqual(
|
414
|
+
value_location.message_on_path('hi.', KeyPath()),
|
415
|
+
'hi. (path=)')
|
416
|
+
self.assertEqual(
|
417
|
+
value_location.message_on_path('hi.', KeyPath(['a'])),
|
418
|
+
'hi. (path=a)')
|
419
|
+
|
420
|
+
|
421
|
+
class KeyPathSetTest(unittest.TestCase):
|
422
|
+
"""Tests for class KeyPathSet."""
|
423
|
+
|
424
|
+
def test_empty_set(self):
|
425
|
+
s1 = KeyPathSet()
|
426
|
+
self.assertFalse(s1)
|
427
|
+
self.assertNotIn('', s1)
|
428
|
+
self.assertNotIn(KeyPath(), s1)
|
429
|
+
self.assertNotIn('abc', s1)
|
430
|
+
self.assertNotIn(1, s1)
|
431
|
+
self.assertEqual(list(s1), [])
|
432
|
+
self.assertIs(s1.subtree(KeyPath()), s1)
|
433
|
+
self.assertFalse(s1.subtree('a.b.c'))
|
434
|
+
self.assertEqual(s1, KeyPathSet())
|
435
|
+
self.assertNotEqual(s1, 1)
|
436
|
+
self.assertNotEqual(s1, KeyPathSet([1]))
|
437
|
+
|
438
|
+
def test_add(self):
|
439
|
+
s1 = KeyPathSet(
|
440
|
+
['a.b.c', 1, KeyPath([1, 'x']), 'a.b']
|
441
|
+
)
|
442
|
+
self.assertEqual(
|
443
|
+
s1._trie,
|
444
|
+
{
|
445
|
+
1: {
|
446
|
+
'x': {
|
447
|
+
'$': True,
|
448
|
+
},
|
449
|
+
'$': True
|
450
|
+
},
|
451
|
+
'a': {
|
452
|
+
'b': {
|
453
|
+
'c': {
|
454
|
+
'$': True
|
455
|
+
},
|
456
|
+
'$': True,
|
457
|
+
},
|
458
|
+
}
|
459
|
+
}
|
460
|
+
)
|
461
|
+
self.assertNotIn('', s1)
|
462
|
+
self.assertNotIn('a', s1)
|
463
|
+
self.assertIn(KeyPath(['a', 'b']), s1)
|
464
|
+
self.assertIn('a.b.c', s1)
|
465
|
+
self.assertIn(1, s1)
|
466
|
+
self.assertIn('[1]', s1)
|
467
|
+
self.assertIn('[1].x', s1)
|
468
|
+
self.assertIn(KeyPath([1, 'x']), s1)
|
469
|
+
|
470
|
+
self.assertTrue(s1.add(''))
|
471
|
+
self.assertIn('', s1)
|
472
|
+
self.assertFalse(s1.add('a.b.c'))
|
473
|
+
|
474
|
+
# Test include_intermediate.
|
475
|
+
s1 = KeyPathSet()
|
476
|
+
self.assertTrue(s1.add('a.b.c', include_intermediate=True))
|
477
|
+
self.assertIn('a', s1)
|
478
|
+
self.assertIn('a.b', s1)
|
479
|
+
self.assertIn('a.b.c', s1)
|
480
|
+
|
481
|
+
def test_remove(self):
|
482
|
+
s1 = KeyPathSet(
|
483
|
+
['a.b.c', 1, KeyPath([1, 'x']), 'a.b', 'c.d']
|
484
|
+
)
|
485
|
+
self.assertFalse(s1.remove('b'))
|
486
|
+
self.assertFalse(s1.remove('c'))
|
487
|
+
self.assertTrue(s1.remove('a.b.c'))
|
488
|
+
self.assertTrue(s1.remove('a.b'))
|
489
|
+
self.assertTrue(s1.remove(1))
|
490
|
+
self.assertEqual(
|
491
|
+
s1._trie,
|
492
|
+
{
|
493
|
+
1: {
|
494
|
+
'x': {
|
495
|
+
'$': True,
|
496
|
+
},
|
497
|
+
},
|
498
|
+
'c': {
|
499
|
+
'd': {
|
500
|
+
'$': True,
|
501
|
+
},
|
502
|
+
},
|
503
|
+
}
|
504
|
+
)
|
505
|
+
self.assertNotIn(1, s1)
|
506
|
+
self.assertTrue(s1.has_prefix(1))
|
507
|
+
|
508
|
+
def test_iter(self):
|
509
|
+
self.assertEqual(
|
510
|
+
list(KeyPathSet(['', 'a.b.c', 1, KeyPath([1, 'x']), 'a.b'])),
|
511
|
+
[
|
512
|
+
KeyPath(), KeyPath.parse('a.b.c'), KeyPath.parse('a.b'),
|
513
|
+
KeyPath([1]), KeyPath([1, 'x'])
|
514
|
+
]
|
515
|
+
)
|
516
|
+
|
517
|
+
def test_has_prefix(self):
|
518
|
+
s1 = KeyPathSet(['a.b.c', 1, KeyPath([1, 'x']), 'a.b'])
|
519
|
+
self.assertTrue(s1.has_prefix('a'))
|
520
|
+
self.assertTrue(s1.has_prefix('a.b'))
|
521
|
+
self.assertTrue(s1.has_prefix('a.b.c'))
|
522
|
+
self.assertTrue(s1.has_prefix(KeyPath(['a'])))
|
523
|
+
self.assertTrue(s1.has_prefix(1))
|
524
|
+
self.assertFalse(s1.has_prefix(2))
|
525
|
+
self.assertFalse(s1.has_prefix('a.b.c.d'))
|
526
|
+
|
527
|
+
def test_subpaths(self):
|
528
|
+
s1 = KeyPathSet(['a.b.c', 1, KeyPath([1, 'x']), 'a.b'])
|
529
|
+
self.assertIs(s1.subtree(''), s1)
|
530
|
+
self.assertEqual(
|
531
|
+
s1.subtree('a'), KeyPathSet(['b.c', 'b'])
|
532
|
+
)
|
533
|
+
self.assertEqual(s1.subtree(1), KeyPathSet(['', 'x']))
|
534
|
+
self.assertEqual(
|
535
|
+
s1.subtree(1), KeyPathSet(['', 'x'])
|
536
|
+
)
|
537
|
+
|
538
|
+
def test_clear(self):
|
539
|
+
s1 = KeyPathSet(['a.b.c', 1, KeyPath([1, 'x']), 'a.b'])
|
540
|
+
s1.clear()
|
541
|
+
self.assertEqual(s1, KeyPathSet())
|
542
|
+
|
543
|
+
def test_copy(self):
|
544
|
+
s1 = KeyPathSet(['a.b.c', 1, KeyPath([1, 'x']), 'a.b'])
|
545
|
+
s2 = s1.copy()
|
546
|
+
self.assertIsNot(s1, s2)
|
547
|
+
self.assertIsNot(s1._trie, s2._trie)
|
548
|
+
self.assertIsNot(s1._trie['a'], s2._trie['a'])
|
549
|
+
self.assertIsNot(s1._trie['a']['b'], s2._trie['a']['b'])
|
550
|
+
|
551
|
+
def test_update(self):
|
552
|
+
s1 = KeyPathSet(['a.b.c', 1, KeyPath([1, 'x']), 'a.b'])
|
553
|
+
s1.update(KeyPathSet(['a.b.d', 'a.c', '']))
|
554
|
+
self.assertEqual(
|
555
|
+
s1._trie,
|
556
|
+
{
|
557
|
+
1: {
|
558
|
+
'x': {
|
559
|
+
'$': True,
|
560
|
+
},
|
561
|
+
'$': True,
|
562
|
+
},
|
563
|
+
'a': {
|
564
|
+
'b': {
|
565
|
+
'c': {
|
566
|
+
'$': True
|
567
|
+
},
|
568
|
+
'd': {
|
569
|
+
'$': True
|
570
|
+
},
|
571
|
+
'$': True,
|
572
|
+
},
|
573
|
+
'c': {
|
574
|
+
'$': True
|
575
|
+
},
|
576
|
+
},
|
577
|
+
'$': True,
|
578
|
+
}
|
579
|
+
)
|
580
|
+
|
581
|
+
def test_union(self):
|
582
|
+
s1 = KeyPathSet(['a.b.c', 1, KeyPath([1, 'x']), 'a.b'])
|
583
|
+
s2 = s1.union(KeyPathSet(['a.b.d', 'a.c', '']))
|
584
|
+
self.assertEqual(
|
585
|
+
list(s2),
|
586
|
+
[
|
587
|
+
KeyPath.parse('a.b.c'),
|
588
|
+
KeyPath.parse('a.b'),
|
589
|
+
KeyPath.parse('a.b.d'),
|
590
|
+
KeyPath.parse('a.c'),
|
591
|
+
KeyPath([1]),
|
592
|
+
KeyPath([1, 'x']),
|
593
|
+
KeyPath(),
|
594
|
+
]
|
595
|
+
)
|
596
|
+
self.assertIsNot(s2._trie['a'], s1._trie['a'])
|
597
|
+
self.assertIsNot(s2._trie['a']['b'], s1._trie['a']['b'])
|
598
|
+
self.assertIsNot(
|
599
|
+
s2._trie['a']['b']['c'], s1._trie['a']['b']['c']
|
600
|
+
)
|
601
|
+
|
602
|
+
def test_difference(self):
|
603
|
+
s1 = KeyPathSet(['a.b.c', 1, KeyPath([1, 'x']), 'a.b', ''])
|
604
|
+
s2 = s1.difference(
|
605
|
+
KeyPathSet(['a.b', 'a.c.b', '[1].x', ''])
|
606
|
+
)
|
607
|
+
self.assertEqual(
|
608
|
+
s2._trie,
|
609
|
+
{
|
610
|
+
1: {
|
611
|
+
'$': True
|
612
|
+
},
|
613
|
+
'a': {
|
614
|
+
'b': {
|
615
|
+
'c': {
|
616
|
+
'$': True
|
617
|
+
}
|
618
|
+
}
|
619
|
+
}
|
620
|
+
}
|
621
|
+
)
|
622
|
+
self.assertIsNot(s2._trie['a'], s1._trie['a'])
|
623
|
+
self.assertIsNot(s2._trie['a']['b'], s1._trie['a']['b'])
|
624
|
+
self.assertIsNot(s2._trie[1], s1._trie[1])
|
625
|
+
|
626
|
+
s1.difference_update(KeyPathSet(['a.b', 'a.c.b', '[1].x', '']))
|
627
|
+
self.assertEqual(list(s1), ['a.b.c', '[1]'])
|
628
|
+
|
629
|
+
def test_intersection(self):
|
630
|
+
s1 = KeyPathSet(['a.b.c', 1, KeyPath([1, 'x']), 'a.b', 'a.c.d', ''])
|
631
|
+
s2 = s1.intersection(
|
632
|
+
KeyPathSet(['a.b', 'a.b.d', 'a.c.b', '[1].x', ''])
|
633
|
+
)
|
634
|
+
self.assertEqual(
|
635
|
+
s2._trie,
|
636
|
+
{
|
637
|
+
1: {
|
638
|
+
'x': {
|
639
|
+
'$': True,
|
640
|
+
},
|
641
|
+
},
|
642
|
+
'a': {
|
643
|
+
'b': {
|
644
|
+
'$': True,
|
645
|
+
},
|
646
|
+
},
|
647
|
+
'$': True,
|
648
|
+
}
|
649
|
+
)
|
650
|
+
self.assertIsNot(s2._trie['a'], s1._trie['a'])
|
651
|
+
self.assertIsNot(s2._trie['a']['b'], s1._trie['a']['b'])
|
652
|
+
self.assertIsNot(s2._trie[1], s1._trie[1])
|
653
|
+
|
654
|
+
s1.intersection_update(
|
655
|
+
KeyPathSet(['a.b', 'a.c.b', '[1].x', ''])
|
656
|
+
)
|
657
|
+
self.assertEqual(list(s1), ['a.b', '[1].x', ''])
|
658
|
+
|
659
|
+
def test_rebase(self):
|
660
|
+
s1 = KeyPathSet(['x.y', 'y', 'y.z.w'])
|
661
|
+
s1.rebase('a.b.')
|
662
|
+
self.assertEqual(
|
663
|
+
list(s1),
|
664
|
+
[
|
665
|
+
KeyPath.parse('a.b.x.y'),
|
666
|
+
KeyPath.parse('a.b.y'),
|
667
|
+
KeyPath.parse('a.b.y.z.w'),
|
668
|
+
]
|
669
|
+
)
|
670
|
+
|
671
|
+
def test_operator_add(self):
|
672
|
+
self.assertEqual(
|
673
|
+
KeyPathSet(['a.b.c', 'a.b']) + KeyPathSet(['a.b', '[1].a', '']),
|
674
|
+
KeyPathSet(['a.b.c', 'a.b', '[1].a', ''])
|
675
|
+
)
|
676
|
+
self.assertEqual(
|
677
|
+
KeyPath.parse('x[0]') + KeyPathSet(['a.b.c', 'a.b']),
|
678
|
+
KeyPathSet(['x[0].a.b.c', 'x[0].a.b'])
|
679
|
+
)
|
680
|
+
|
681
|
+
def test_format(self):
|
682
|
+
self.assertEqual(
|
683
|
+
KeyPathSet(['a.b.c', 'a.b']).format(),
|
684
|
+
'KeyPathSet([a.b.c, a.b])'
|
685
|
+
)
|
686
|
+
|
687
|
+
def test_from_value(self):
|
688
|
+
"""Test KeyPathSet.from_value."""
|
689
|
+
s = KeyPathSet(['a.b.c'])
|
690
|
+
self.assertIs(
|
691
|
+
KeyPathSet.from_value(s), s
|
692
|
+
)
|
693
|
+
self.assertEqual(
|
694
|
+
KeyPathSet.from_value(['a.b']),
|
695
|
+
KeyPathSet(['a.b'])
|
696
|
+
)
|
697
|
+
self.assertEqual(
|
698
|
+
KeyPathSet.from_value(['a.b'], include_intermediate=True),
|
699
|
+
KeyPathSet(['a', 'a.b', ''])
|
700
|
+
)
|
701
|
+
with self.assertRaisesRegex(
|
702
|
+
ValueError, 'Cannot convert .* to KeyPathSet'
|
703
|
+
):
|
704
|
+
KeyPathSet.from_value(1)
|
705
|
+
|
706
|
+
if __name__ == '__main__':
|
707
|
+
unittest.main()
|