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.
Files changed (145) hide show
  1. pyglove/core/__init__.py +54 -20
  2. pyglove/core/coding/__init__.py +42 -0
  3. pyglove/core/coding/errors.py +111 -0
  4. pyglove/core/coding/errors_test.py +98 -0
  5. pyglove/core/coding/execution.py +309 -0
  6. pyglove/core/coding/execution_test.py +333 -0
  7. pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
  8. pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
  9. pyglove/core/coding/parsing.py +153 -0
  10. pyglove/core/coding/parsing_test.py +150 -0
  11. pyglove/core/coding/permissions.py +100 -0
  12. pyglove/core/coding/permissions_test.py +93 -0
  13. pyglove/core/geno/base.py +54 -41
  14. pyglove/core/geno/base_test.py +2 -4
  15. pyglove/core/geno/categorical.py +37 -28
  16. pyglove/core/geno/custom.py +19 -16
  17. pyglove/core/geno/numerical.py +20 -17
  18. pyglove/core/geno/space.py +4 -5
  19. pyglove/core/hyper/base.py +6 -6
  20. pyglove/core/hyper/categorical.py +94 -55
  21. pyglove/core/hyper/custom.py +7 -7
  22. pyglove/core/hyper/custom_test.py +9 -10
  23. pyglove/core/hyper/derived.py +30 -22
  24. pyglove/core/hyper/derived_test.py +2 -4
  25. pyglove/core/hyper/dynamic_evaluation.py +5 -6
  26. pyglove/core/hyper/evolvable.py +57 -46
  27. pyglove/core/hyper/numerical.py +48 -24
  28. pyglove/core/hyper/numerical_test.py +9 -9
  29. pyglove/core/hyper/object_template.py +58 -46
  30. pyglove/core/io/__init__.py +1 -0
  31. pyglove/core/io/file_system.py +17 -7
  32. pyglove/core/io/file_system_test.py +2 -0
  33. pyglove/core/io/sequence.py +299 -0
  34. pyglove/core/io/sequence_test.py +124 -0
  35. pyglove/core/logging_test.py +0 -2
  36. pyglove/core/patching/object_factory.py +4 -4
  37. pyglove/core/patching/pattern_based.py +4 -4
  38. pyglove/core/patching/rule_based.py +17 -5
  39. pyglove/core/patching/rule_based_test.py +27 -4
  40. pyglove/core/symbolic/__init__.py +2 -7
  41. pyglove/core/symbolic/base.py +320 -183
  42. pyglove/core/symbolic/base_test.py +123 -19
  43. pyglove/core/symbolic/boilerplate.py +7 -13
  44. pyglove/core/symbolic/boilerplate_test.py +25 -23
  45. pyglove/core/symbolic/class_wrapper.py +48 -45
  46. pyglove/core/symbolic/class_wrapper_test.py +2 -2
  47. pyglove/core/symbolic/compounding.py +9 -15
  48. pyglove/core/symbolic/compounding_test.py +2 -4
  49. pyglove/core/symbolic/dict.py +154 -110
  50. pyglove/core/symbolic/dict_test.py +238 -130
  51. pyglove/core/symbolic/diff.py +199 -10
  52. pyglove/core/symbolic/diff_test.py +226 -0
  53. pyglove/core/symbolic/flags.py +1 -1
  54. pyglove/core/symbolic/functor.py +29 -26
  55. pyglove/core/symbolic/functor_test.py +102 -50
  56. pyglove/core/symbolic/inferred.py +2 -2
  57. pyglove/core/symbolic/list.py +81 -50
  58. pyglove/core/symbolic/list_test.py +119 -97
  59. pyglove/core/symbolic/object.py +225 -113
  60. pyglove/core/symbolic/object_test.py +320 -108
  61. pyglove/core/symbolic/origin.py +17 -14
  62. pyglove/core/symbolic/origin_test.py +4 -2
  63. pyglove/core/symbolic/pure_symbolic.py +4 -3
  64. pyglove/core/symbolic/ref.py +108 -21
  65. pyglove/core/symbolic/ref_test.py +93 -0
  66. pyglove/core/symbolic/symbolize_test.py +10 -2
  67. pyglove/core/tuning/local_backend.py +2 -2
  68. pyglove/core/tuning/protocols.py +3 -3
  69. pyglove/core/tuning/sample_test.py +3 -3
  70. pyglove/core/typing/__init__.py +14 -5
  71. pyglove/core/typing/annotation_conversion.py +43 -27
  72. pyglove/core/typing/annotation_conversion_test.py +23 -0
  73. pyglove/core/typing/callable_ext.py +241 -3
  74. pyglove/core/typing/callable_ext_test.py +255 -0
  75. pyglove/core/typing/callable_signature.py +510 -66
  76. pyglove/core/typing/callable_signature_test.py +619 -99
  77. pyglove/core/typing/class_schema.py +229 -154
  78. pyglove/core/typing/class_schema_test.py +149 -95
  79. pyglove/core/typing/custom_typing.py +5 -4
  80. pyglove/core/typing/inspect.py +63 -0
  81. pyglove/core/typing/inspect_test.py +39 -0
  82. pyglove/core/typing/key_specs.py +10 -11
  83. pyglove/core/typing/key_specs_test.py +7 -4
  84. pyglove/core/typing/type_conversion.py +4 -5
  85. pyglove/core/typing/type_conversion_test.py +12 -12
  86. pyglove/core/typing/typed_missing.py +6 -7
  87. pyglove/core/typing/typed_missing_test.py +7 -8
  88. pyglove/core/typing/value_specs.py +604 -362
  89. pyglove/core/typing/value_specs_test.py +328 -90
  90. pyglove/core/utils/__init__.py +164 -0
  91. pyglove/core/{object_utils → utils}/common_traits.py +3 -67
  92. pyglove/core/utils/common_traits_test.py +36 -0
  93. pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
  94. pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
  95. pyglove/core/{object_utils → utils}/error_utils.py +78 -9
  96. pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
  97. pyglove/core/utils/formatting.py +464 -0
  98. pyglove/core/utils/formatting_test.py +453 -0
  99. pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
  100. pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
  101. pyglove/core/{object_utils → utils}/json_conversion.py +177 -52
  102. pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
  103. pyglove/core/{object_utils → utils}/missing.py +3 -3
  104. pyglove/core/{object_utils → utils}/missing_test.py +2 -4
  105. pyglove/core/utils/text_color.py +128 -0
  106. pyglove/core/utils/text_color_test.py +94 -0
  107. pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
  108. pyglove/core/utils/timing.py +236 -0
  109. pyglove/core/utils/timing_test.py +154 -0
  110. pyglove/core/{object_utils → utils}/value_location.py +275 -6
  111. pyglove/core/utils/value_location_test.py +707 -0
  112. pyglove/core/views/__init__.py +32 -0
  113. pyglove/core/views/base.py +804 -0
  114. pyglove/core/views/base_test.py +580 -0
  115. pyglove/core/views/html/__init__.py +27 -0
  116. pyglove/core/views/html/base.py +547 -0
  117. pyglove/core/views/html/base_test.py +830 -0
  118. pyglove/core/views/html/controls/__init__.py +35 -0
  119. pyglove/core/views/html/controls/base.py +275 -0
  120. pyglove/core/views/html/controls/label.py +207 -0
  121. pyglove/core/views/html/controls/label_test.py +157 -0
  122. pyglove/core/views/html/controls/progress_bar.py +183 -0
  123. pyglove/core/views/html/controls/progress_bar_test.py +97 -0
  124. pyglove/core/views/html/controls/tab.py +320 -0
  125. pyglove/core/views/html/controls/tab_test.py +87 -0
  126. pyglove/core/views/html/controls/tooltip.py +99 -0
  127. pyglove/core/views/html/controls/tooltip_test.py +99 -0
  128. pyglove/core/views/html/tree_view.py +1517 -0
  129. pyglove/core/views/html/tree_view_test.py +1461 -0
  130. {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/METADATA +18 -4
  131. pyglove-0.4.5.dev202501132210.dist-info/RECORD +214 -0
  132. {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/WHEEL +1 -1
  133. pyglove/core/object_utils/__init__.py +0 -154
  134. pyglove/core/object_utils/common_traits_test.py +0 -82
  135. pyglove/core/object_utils/formatting.py +0 -234
  136. pyglove/core/object_utils/formatting_test.py +0 -223
  137. pyglove/core/object_utils/value_location_test.py +0 -385
  138. pyglove/core/symbolic/schema_utils.py +0 -327
  139. pyglove/core/symbolic/schema_utils_test.py +0 -57
  140. pyglove/core/typing/class_schema_utils.py +0 -202
  141. pyglove/core/typing/class_schema_utils_test.py +0 -194
  142. pyglove-0.4.5.dev20240318.dist-info/RECORD +0 -185
  143. /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
  144. {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
  145. {pyglove-0.4.5.dev20240318.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()