omlish 0.0.0.dev117__py3-none-any.whl → 0.0.0.dev118__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.
- omlish/__about__.py +2 -2
- omlish/collections/hasheq.py +0 -10
- omlish/fnpairs.py +1 -10
- omlish/formats/json/__init__.py +5 -0
- omlish/formats/json/literals.py +179 -0
- omlish/formats/json/render.py +2 -3
- omlish/formats/json/stream/render.py +1 -1
- omlish/formats/json/types.py +6 -0
- omlish/lang/classes/abstract.py +37 -14
- omlish/lang/imports.py +1 -1
- omlish/lang/maybes.py +10 -12
- omlish/specs/jmespath/ast.py +199 -60
- omlish/specs/jmespath/cli.py +43 -29
- omlish/specs/jmespath/functions.py +397 -274
- omlish/specs/jmespath/lexer.py +2 -2
- omlish/specs/jmespath/parser.py +169 -133
- omlish/specs/jmespath/scope.py +15 -11
- omlish/specs/jmespath/visitor.py +211 -137
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev118.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev118.dist-info}/RECORD +24 -22
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev118.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev118.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev118.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev118.dist-info}/top_level.txt +0 -0
omlish/specs/jmespath/scope.py
CHANGED
@@ -1,14 +1,18 @@
|
|
1
|
-
import
|
1
|
+
import typing as ta
|
2
2
|
|
3
3
|
|
4
|
-
|
4
|
+
K = ta.TypeVar('K')
|
5
|
+
V = ta.TypeVar('V')
|
6
|
+
|
7
|
+
|
8
|
+
class ScopedChainDict(ta.Generic[K, V]):
|
5
9
|
"""
|
6
10
|
Dictionary that can delegate lookups to multiple dicts. This provides a basic get/set dict interface that is backed
|
7
11
|
by multiple dicts. Each dict is searched from the top most (most recently pushed) scope dict until a match is
|
8
12
|
found.
|
9
13
|
"""
|
10
14
|
|
11
|
-
def __init__(self, *scopes):
|
15
|
+
def __init__(self, *scopes: ta.Mapping[K, V]) -> None:
|
12
16
|
super().__init__()
|
13
17
|
|
14
18
|
# The scopes are evaluated starting at the top of the stack (the most recently pushed scope via .push_scope()).
|
@@ -16,22 +20,22 @@ class ScopedChainDict:
|
|
16
20
|
# call reversed(self._scopes) whenever we resolve a key, because the end of the list is the top of the stack.
|
17
21
|
# To avoid this, we're using a deque so we can append to the front of the list via .appendleft() in constant
|
18
22
|
# time, and iterate over scopes without having to do so with a reversed() call each time.
|
19
|
-
self._scopes =
|
23
|
+
self._scopes: list[ta.Mapping[K, V]] = list(reversed(scopes))
|
20
24
|
|
21
|
-
def __getitem__(self, key):
|
22
|
-
for scope in self._scopes:
|
25
|
+
def __getitem__(self, key: K) -> V:
|
26
|
+
for scope in reversed(self._scopes):
|
23
27
|
if key in scope:
|
24
28
|
return scope[key]
|
25
29
|
raise KeyError(key)
|
26
30
|
|
27
|
-
def get(self, key, default=None):
|
31
|
+
def get(self, key: K, default: V | None = None) -> V | None:
|
28
32
|
try:
|
29
33
|
return self[key]
|
30
34
|
except KeyError:
|
31
35
|
return default
|
32
36
|
|
33
|
-
def push_scope(self, scope):
|
34
|
-
self._scopes.
|
37
|
+
def push_scope(self, scope: ta.Mapping[K, V]) -> None:
|
38
|
+
self._scopes.append(scope)
|
35
39
|
|
36
|
-
def pop_scope(self):
|
37
|
-
self._scopes.
|
40
|
+
def pop_scope(self) -> None:
|
41
|
+
self._scopes.pop()
|
omlish/specs/jmespath/visitor.py
CHANGED
@@ -2,12 +2,54 @@ import numbers
|
|
2
2
|
import operator
|
3
3
|
import typing as ta
|
4
4
|
|
5
|
-
from
|
6
|
-
from
|
5
|
+
from ... import check
|
6
|
+
from ... import lang
|
7
|
+
from .ast import AndExpression
|
8
|
+
from .ast import Arithmetic
|
9
|
+
from .ast import ArithmeticUnary
|
10
|
+
from .ast import Assign
|
11
|
+
from .ast import Comparator
|
12
|
+
from .ast import CurrentNode
|
13
|
+
from .ast import Expref
|
14
|
+
from .ast import Field
|
15
|
+
from .ast import FilterProjection
|
16
|
+
from .ast import Flatten
|
17
|
+
from .ast import FunctionExpression
|
18
|
+
from .ast import Identity
|
19
|
+
from .ast import Index
|
20
|
+
from .ast import IndexExpression
|
21
|
+
from .ast import KeyValPair
|
22
|
+
from .ast import LetExpression
|
23
|
+
from .ast import Literal
|
24
|
+
from .ast import MultiSelectDict
|
25
|
+
from .ast import MultiSelectList
|
7
26
|
from .ast import Node
|
27
|
+
from .ast import NotExpression
|
28
|
+
from .ast import OrExpression
|
29
|
+
from .ast import Pipe
|
30
|
+
from .ast import Projection
|
31
|
+
from .ast import RootNode
|
32
|
+
from .ast import Slice
|
33
|
+
from .ast import Subexpression
|
34
|
+
from .ast import ValueProjection
|
35
|
+
from .ast import VariableRef
|
36
|
+
from .exceptions import UndefinedVariableError
|
37
|
+
from .functions import DefaultFunctions
|
38
|
+
from .functions import Functions
|
8
39
|
from .scope import ScopedChainDict
|
9
40
|
|
10
41
|
|
42
|
+
if ta.TYPE_CHECKING:
|
43
|
+
import html
|
44
|
+
import json
|
45
|
+
else:
|
46
|
+
html = lang.proxy_import('html')
|
47
|
+
json = lang.proxy_import('json')
|
48
|
+
|
49
|
+
|
50
|
+
##
|
51
|
+
|
52
|
+
|
11
53
|
def _equals(x: ta.Any, y: ta.Any) -> bool:
|
12
54
|
if _is_special_number_case(x, y):
|
13
55
|
return False
|
@@ -58,58 +100,69 @@ def _is_actual_number(x: ta.Any) -> bool:
|
|
58
100
|
return isinstance(x, numbers.Number)
|
59
101
|
|
60
102
|
|
103
|
+
def node_type(n: Node) -> str:
|
104
|
+
return lang.snake_case(type(n).__name__)
|
105
|
+
|
106
|
+
|
107
|
+
##
|
108
|
+
|
109
|
+
|
110
|
+
class Visitor:
|
111
|
+
def __init__(self) -> None:
|
112
|
+
super().__init__()
|
113
|
+
|
114
|
+
self._method_cache: dict[str, ta.Callable] = {}
|
115
|
+
|
116
|
+
def visit(self, node: Node, *args: ta.Any, **kwargs: ta.Any) -> ta.Any:
|
117
|
+
nty = node_type(node)
|
118
|
+
method = self._method_cache.get(nty)
|
119
|
+
if method is None:
|
120
|
+
method = check.not_none(getattr(self, f'visit_{nty}', self.default_visit))
|
121
|
+
self._method_cache[nty] = method
|
122
|
+
return method(node, *args, **kwargs)
|
123
|
+
|
124
|
+
def default_visit(self, node, *args, **kwargs):
|
125
|
+
raise NotImplementedError('default_visit')
|
126
|
+
|
127
|
+
|
128
|
+
##
|
129
|
+
|
130
|
+
|
61
131
|
class Options:
|
62
132
|
"""Options to control how a Jmespath function is evaluated."""
|
63
133
|
|
64
134
|
def __init__(
|
65
135
|
self,
|
66
136
|
dict_cls: type | None = None,
|
67
|
-
custom_functions=None,
|
137
|
+
custom_functions: Functions | None = None,
|
68
138
|
enable_legacy_literals: bool = False,
|
69
139
|
) -> None:
|
70
140
|
super().__init__()
|
71
141
|
|
72
|
-
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
142
|
+
# The class to use when creating a dict. The interpreter may create dictionaries during the evaluation of a
|
143
|
+
# Jmespath expression. For example, a multi-select hash will create a dictionary. By default we use a dict()
|
144
|
+
# type. You can set this value to change what dict type is used. The most common reason you would change this is
|
145
|
+
# if you want to set a collections.OrderedDict so that you can have predictable key ordering.
|
76
146
|
self.dict_cls = dict_cls
|
77
147
|
self.custom_functions = custom_functions
|
78
148
|
|
79
|
-
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
149
|
+
# The flag to enable pre-JEP-12 literal compatibility.
|
150
|
+
# JEP-12 deprecates `foo` -> "foo" syntax.
|
151
|
+
# Valid expressions MUST use: `"foo"` -> "foo"
|
152
|
+
# Setting this flag to `True` enables support for legacy syntax.
|
83
153
|
self.enable_legacy_literals = enable_legacy_literals
|
84
154
|
|
85
155
|
|
86
156
|
class _Expression:
|
87
|
-
def __init__(self, expression, interpreter):
|
157
|
+
def __init__(self, expression: Node, interpreter: 'TreeInterpreter') -> None:
|
88
158
|
super().__init__()
|
89
159
|
self.expression = expression
|
90
160
|
self.interpreter = interpreter
|
91
161
|
|
92
|
-
def visit(self, node, *args, **kwargs):
|
162
|
+
def visit(self, node: Node, *args: ta.Any, **kwargs: ta.Any) -> ta.Any:
|
93
163
|
return self.interpreter.visit(node, *args, **kwargs)
|
94
164
|
|
95
165
|
|
96
|
-
class Visitor:
|
97
|
-
def __init__(self):
|
98
|
-
super().__init__()
|
99
|
-
self._method_cache = {}
|
100
|
-
|
101
|
-
def visit(self, node, *args, **kwargs):
|
102
|
-
node_type = node['type']
|
103
|
-
method = self._method_cache.get(node_type)
|
104
|
-
if method is None:
|
105
|
-
method = getattr(self, f'visit_{node["type"]}', self.default_visit)
|
106
|
-
self._method_cache[node_type] = method
|
107
|
-
return method(node, *args, **kwargs) # type: ignore
|
108
|
-
|
109
|
-
def default_visit(self, node, *args, **kwargs):
|
110
|
-
raise NotImplementedError('default_visit')
|
111
|
-
|
112
|
-
|
113
166
|
class TreeInterpreter(Visitor):
|
114
167
|
COMPARATOR_FUNC: ta.Mapping[str, ta.Callable] = {
|
115
168
|
'eq': _equals,
|
@@ -120,7 +173,10 @@ class TreeInterpreter(Visitor):
|
|
120
173
|
'gte': operator.ge,
|
121
174
|
}
|
122
175
|
|
123
|
-
_EQUALITY_OPS: ta.
|
176
|
+
_EQUALITY_OPS: ta.AbstractSet[str] = {
|
177
|
+
'eq',
|
178
|
+
'ne',
|
179
|
+
}
|
124
180
|
|
125
181
|
_ARITHMETIC_UNARY_FUNC: ta.Mapping[str, ta.Callable] = {
|
126
182
|
'minus': operator.neg,
|
@@ -136,115 +192,113 @@ class TreeInterpreter(Visitor):
|
|
136
192
|
'plus': operator.add,
|
137
193
|
}
|
138
194
|
|
139
|
-
|
140
|
-
|
141
|
-
def __init__(self, options=None):
|
195
|
+
def __init__(self, options: Options | None = None) -> None:
|
142
196
|
super().__init__()
|
143
197
|
|
144
|
-
self._dict_cls =
|
198
|
+
self._dict_cls: type = dict
|
145
199
|
|
146
200
|
if options is None:
|
147
201
|
options = Options()
|
148
202
|
self._options = options
|
149
203
|
|
150
204
|
if options.dict_cls is not None:
|
151
|
-
self._dict_cls =
|
205
|
+
self._dict_cls = options.dict_cls
|
152
206
|
|
153
207
|
if options.custom_functions is not None:
|
154
|
-
self._functions =
|
208
|
+
self._functions: Functions = options.custom_functions
|
155
209
|
else:
|
156
|
-
self._functions =
|
210
|
+
self._functions = DefaultFunctions()
|
157
211
|
|
158
|
-
self._root = None
|
159
|
-
self._scope = ScopedChainDict()
|
212
|
+
self._root: Node | None = None
|
213
|
+
self._scope: ScopedChainDict = ScopedChainDict()
|
160
214
|
|
161
|
-
def default_visit(self, node, *args, **kwargs):
|
162
|
-
raise NotImplementedError(node
|
215
|
+
def default_visit(self, node: Node, *args: ta.Any, **kwargs: ta.Any) -> ta.NoReturn:
|
216
|
+
raise NotImplementedError(node_type(node))
|
163
217
|
|
164
|
-
def evaluate(self, ast, root: Node) -> ta.Any:
|
218
|
+
def evaluate(self, ast: Node, root: Node) -> ta.Any:
|
165
219
|
self._root = root
|
166
220
|
return self.visit(ast, root)
|
167
221
|
|
168
|
-
def visit_subexpression(self, node, value):
|
222
|
+
def visit_subexpression(self, node: Subexpression, value: ta.Any) -> ta.Any:
|
169
223
|
result = value
|
170
|
-
for child in node
|
224
|
+
for child in node.children_nodes:
|
171
225
|
result = self.visit(child, result)
|
172
226
|
if result is None:
|
173
227
|
return None
|
174
228
|
return result
|
175
229
|
|
176
|
-
def visit_field(self, node, value):
|
230
|
+
def visit_field(self, node: Field, value: ta.Any) -> ta.Any:
|
177
231
|
try:
|
178
|
-
return value.get(node
|
232
|
+
return value.get(node.name)
|
179
233
|
except AttributeError:
|
180
234
|
return None
|
181
235
|
|
182
|
-
def visit_comparator(self, node, value):
|
236
|
+
def visit_comparator(self, node: Comparator, value: ta.Any) -> ta.Any:
|
183
237
|
# Common case: comparator is == or !=
|
184
|
-
comparator_func = self.COMPARATOR_FUNC[node
|
185
|
-
if node
|
238
|
+
comparator_func = self.COMPARATOR_FUNC[node.name]
|
239
|
+
if node.name in self._EQUALITY_OPS:
|
186
240
|
return comparator_func(
|
187
|
-
self.visit(node
|
188
|
-
self.visit(node
|
241
|
+
self.visit(node.first, value),
|
242
|
+
self.visit(node.second, value),
|
189
243
|
)
|
190
244
|
|
191
245
|
else:
|
192
246
|
# Ordering operators are only valid for numbers. Evaluating any other type with a comparison operator will
|
193
247
|
# yield a None value.
|
194
|
-
left = self.visit(node
|
195
|
-
right = self.visit(node
|
248
|
+
left = self.visit(node.first, value)
|
249
|
+
right = self.visit(node.second, value)
|
196
250
|
# num_types = (int, float)
|
197
251
|
if not (_is_comparable(left) and _is_comparable(right)):
|
198
252
|
return None
|
199
253
|
return comparator_func(left, right)
|
200
254
|
|
201
|
-
def visit_arithmetic_unary(self, node, value):
|
202
|
-
operation = self._ARITHMETIC_UNARY_FUNC[node
|
255
|
+
def visit_arithmetic_unary(self, node: ArithmeticUnary, value: ta.Any) -> ta.Any:
|
256
|
+
operation = self._ARITHMETIC_UNARY_FUNC[node.operator]
|
203
257
|
return operation(
|
204
|
-
self.visit(node
|
258
|
+
self.visit(node.expression, value),
|
205
259
|
)
|
206
260
|
|
207
|
-
def visit_arithmetic(self, node, value):
|
208
|
-
operation = self._ARITHMETIC_FUNC[node
|
261
|
+
def visit_arithmetic(self, node: Arithmetic, value: ta.Any) -> ta.Any:
|
262
|
+
operation = self._ARITHMETIC_FUNC[node.operator]
|
209
263
|
return operation(
|
210
|
-
self.visit(node
|
211
|
-
self.visit(node
|
264
|
+
self.visit(node.left, value),
|
265
|
+
self.visit(node.right, value),
|
212
266
|
)
|
213
267
|
|
214
|
-
def
|
268
|
+
def visit_current_node(self, node: CurrentNode, value: ta.Any) -> ta.Any:
|
215
269
|
return value
|
216
270
|
|
217
|
-
def
|
271
|
+
def visit_root_node(self, node: RootNode, value: ta.Any) -> ta.Any:
|
218
272
|
return self._root
|
219
273
|
|
220
|
-
def visit_expref(self, node, value):
|
221
|
-
return _Expression(node
|
274
|
+
def visit_expref(self, node: Expref, value: ta.Any) -> ta.Any:
|
275
|
+
return _Expression(node.expression, self)
|
222
276
|
|
223
|
-
def visit_function_expression(self, node, value):
|
277
|
+
def visit_function_expression(self, node: FunctionExpression, value: ta.Any) -> ta.Any:
|
224
278
|
resolved_args = []
|
225
|
-
for child in node
|
279
|
+
for child in node.args:
|
226
280
|
current = self.visit(child, value)
|
227
281
|
resolved_args.append(current)
|
228
282
|
|
229
|
-
return self._functions.call_function(node
|
283
|
+
return self._functions.call_function(node.name, resolved_args)
|
230
284
|
|
231
|
-
def visit_filter_projection(self, node, value):
|
232
|
-
base = self.visit(node
|
285
|
+
def visit_filter_projection(self, node: FilterProjection, value: ta.Any) -> ta.Any:
|
286
|
+
base = self.visit(node.left, value)
|
233
287
|
if not isinstance(base, list):
|
234
288
|
return None
|
235
289
|
|
236
|
-
comparator_node = node
|
290
|
+
comparator_node = node.comparator
|
237
291
|
collected = []
|
238
292
|
for element in base:
|
239
293
|
if self._is_true(self.visit(comparator_node, element)):
|
240
|
-
current = self.visit(node
|
294
|
+
current = self.visit(node.right, element)
|
241
295
|
if current is not None:
|
242
296
|
collected.append(current)
|
243
297
|
|
244
298
|
return collected
|
245
299
|
|
246
|
-
def visit_flatten(self, node, value):
|
247
|
-
base = self.visit(node
|
300
|
+
def visit_flatten(self, node: Flatten, value: ta.Any) -> ta.Any:
|
301
|
+
base = self.visit(node.node, value)
|
248
302
|
if not isinstance(base, list):
|
249
303
|
# Can't flatten the object if it's not a list.
|
250
304
|
return None
|
@@ -258,77 +312,74 @@ class TreeInterpreter(Visitor):
|
|
258
312
|
|
259
313
|
return merged_list
|
260
314
|
|
261
|
-
def visit_identity(self, node, value):
|
315
|
+
def visit_identity(self, node: Identity, value: ta.Any) -> ta.Any:
|
262
316
|
return value
|
263
317
|
|
264
|
-
def visit_index(self, node, value):
|
318
|
+
def visit_index(self, node: Index, value: ta.Any) -> ta.Any:
|
265
319
|
# Even though we can index strings, we don't want to support that.
|
266
320
|
if not isinstance(value, list):
|
267
321
|
return None
|
268
322
|
|
269
323
|
try:
|
270
|
-
return value[node
|
324
|
+
return value[node.index]
|
271
325
|
except IndexError:
|
272
326
|
return None
|
273
327
|
|
274
|
-
def visit_index_expression(self, node, value):
|
328
|
+
def visit_index_expression(self, node: IndexExpression, value: ta.Any) -> ta.Any:
|
275
329
|
result = value
|
276
|
-
for child in node
|
330
|
+
for child in node.nodes:
|
277
331
|
result = self.visit(child, result)
|
278
332
|
|
279
333
|
return result
|
280
334
|
|
281
|
-
def visit_slice(self, node, value):
|
335
|
+
def visit_slice(self, node: Slice, value: ta.Any) -> ta.Any:
|
282
336
|
if isinstance(value, str):
|
283
|
-
|
284
|
-
end = node['children'][1]
|
285
|
-
step = node['children'][2]
|
286
|
-
return value[start:end:step]
|
337
|
+
return value[node.start:node.end:node.step]
|
287
338
|
|
288
339
|
if not isinstance(value, list):
|
289
340
|
return None
|
290
341
|
|
291
|
-
s = slice(
|
342
|
+
s = slice(node.start, node.end, node.step)
|
292
343
|
return value[s]
|
293
344
|
|
294
|
-
def visit_key_val_pair(self, node, value):
|
295
|
-
return self.visit(node
|
345
|
+
def visit_key_val_pair(self, node: KeyValPair, value: ta.Any) -> ta.Any:
|
346
|
+
return self.visit(node.node, value)
|
296
347
|
|
297
|
-
def visit_literal(self, node, value):
|
298
|
-
return node
|
348
|
+
def visit_literal(self, node: Literal, value: ta.Any):
|
349
|
+
return node.literal_value
|
299
350
|
|
300
|
-
def visit_multi_select_dict(self, node, value):
|
351
|
+
def visit_multi_select_dict(self, node: MultiSelectDict, value: ta.Any) -> ta.Any:
|
301
352
|
collected = self._dict_cls()
|
302
|
-
for child in node
|
303
|
-
collected[child
|
353
|
+
for child in node.nodes:
|
354
|
+
collected[child.key_name] = self.visit(child, value)
|
304
355
|
|
305
356
|
return collected
|
306
357
|
|
307
|
-
def visit_multi_select_list(self, node, value):
|
358
|
+
def visit_multi_select_list(self, node: MultiSelectList, value: ta.Any) -> ta.Any:
|
308
359
|
collected = []
|
309
|
-
for child in node
|
360
|
+
for child in node.nodes:
|
310
361
|
collected.append(self.visit(child, value))
|
311
362
|
|
312
363
|
return collected
|
313
364
|
|
314
|
-
def visit_or_expression(self, node, value):
|
315
|
-
matched = self.visit(node
|
365
|
+
def visit_or_expression(self, node: OrExpression, value: ta.Any) -> ta.Any:
|
366
|
+
matched = self.visit(node.left, value)
|
316
367
|
|
317
368
|
if self._is_false(matched):
|
318
|
-
matched = self.visit(node
|
369
|
+
matched = self.visit(node.right, value)
|
319
370
|
|
320
371
|
return matched
|
321
372
|
|
322
|
-
def visit_and_expression(self, node, value):
|
323
|
-
matched = self.visit(node
|
373
|
+
def visit_and_expression(self, node: AndExpression, value: ta.Any) -> ta.Any:
|
374
|
+
matched = self.visit(node.left, value)
|
324
375
|
|
325
376
|
if self._is_false(matched):
|
326
377
|
return matched
|
327
378
|
|
328
|
-
return self.visit(node
|
379
|
+
return self.visit(node.right, value)
|
329
380
|
|
330
|
-
def visit_not_expression(self, node, value):
|
331
|
-
original_result = self.visit(node
|
381
|
+
def visit_not_expression(self, node: NotExpression, value: ta.Any) -> ta.Any:
|
382
|
+
original_result = self.visit(node.expr, value)
|
332
383
|
|
333
384
|
if _is_actual_number(original_result) and original_result == 0:
|
334
385
|
# Special case for 0, !0 should be false, not true. 0 is not a special cased integer in jmespath.
|
@@ -336,59 +387,59 @@ class TreeInterpreter(Visitor):
|
|
336
387
|
|
337
388
|
return not original_result
|
338
389
|
|
339
|
-
def visit_pipe(self, node, value):
|
390
|
+
def visit_pipe(self, node: Pipe, value: ta.Any) -> ta.Any:
|
340
391
|
result = value
|
341
|
-
for child in node
|
392
|
+
for child in [node.left, node.right]:
|
342
393
|
result = self.visit(child, result)
|
343
394
|
return result
|
344
395
|
|
345
|
-
def visit_projection(self, node, value):
|
346
|
-
base = self.visit(node
|
396
|
+
def visit_projection(self, node: Projection, value: ta.Any) -> ta.Any:
|
397
|
+
base = self.visit(node.left, value)
|
347
398
|
|
348
399
|
allow_string = False
|
349
|
-
first_child = node
|
350
|
-
if first_child
|
351
|
-
nested_children = first_child
|
352
|
-
if len(nested_children) > 1 and nested_children[1]
|
400
|
+
first_child = node.left
|
401
|
+
if isinstance(first_child, IndexExpression):
|
402
|
+
nested_children = first_child.nodes
|
403
|
+
if len(nested_children) > 1 and isinstance(nested_children[1], Slice):
|
353
404
|
allow_string = True
|
354
405
|
|
355
406
|
if isinstance(base, str) and allow_string:
|
356
407
|
# projections are really sub-expressions in disguise evaluate the rhs when lhs is a sliced string
|
357
|
-
return self.visit(node
|
408
|
+
return self.visit(node.right, base)
|
358
409
|
|
359
410
|
if not isinstance(base, list):
|
360
411
|
return None
|
412
|
+
|
361
413
|
collected = []
|
362
414
|
for element in base:
|
363
|
-
current = self.visit(node
|
415
|
+
current = self.visit(node.right, element)
|
364
416
|
if current is not None:
|
365
417
|
collected.append(current)
|
366
418
|
|
367
419
|
return collected
|
368
420
|
|
369
|
-
def visit_let_expression(self, node, value):
|
370
|
-
*bindings, expr = node['children']
|
421
|
+
def visit_let_expression(self, node: LetExpression, value: ta.Any) -> ta.Any:
|
371
422
|
scope = {}
|
372
|
-
for assign in bindings:
|
423
|
+
for assign in node.bindings:
|
373
424
|
scope.update(self.visit(assign, value))
|
374
425
|
self._scope.push_scope(scope)
|
375
|
-
result = self.visit(expr, value)
|
426
|
+
result = self.visit(node.expr, value)
|
376
427
|
self._scope.pop_scope()
|
377
428
|
return result
|
378
429
|
|
379
|
-
def visit_assign(self, node, value):
|
380
|
-
name = node
|
381
|
-
value = self.visit(node
|
430
|
+
def visit_assign(self, node: Assign, value: ta.Any) -> ta.Any:
|
431
|
+
name = node.name
|
432
|
+
value = self.visit(node.expr, value)
|
382
433
|
return {name: value}
|
383
434
|
|
384
|
-
def visit_variable_ref(self, node, value):
|
435
|
+
def visit_variable_ref(self, node: VariableRef, value: ta.Any) -> ta.Any:
|
385
436
|
try:
|
386
|
-
return self._scope[node
|
437
|
+
return self._scope[node.name]
|
387
438
|
except KeyError:
|
388
|
-
raise
|
439
|
+
raise UndefinedVariableError(node.name) # noqa
|
389
440
|
|
390
|
-
def visit_value_projection(self, node, value):
|
391
|
-
base = self.visit(node
|
441
|
+
def visit_value_projection(self, node: ValueProjection, value: ta.Any) -> ta.Any:
|
442
|
+
base = self.visit(node.left, value)
|
392
443
|
try:
|
393
444
|
base = base.values()
|
394
445
|
except AttributeError:
|
@@ -396,39 +447,62 @@ class TreeInterpreter(Visitor):
|
|
396
447
|
|
397
448
|
collected = []
|
398
449
|
for element in base:
|
399
|
-
current = self.visit(node
|
450
|
+
current = self.visit(node.right, element)
|
400
451
|
if current is not None:
|
401
452
|
collected.append(current)
|
402
453
|
|
403
454
|
return collected
|
404
455
|
|
405
|
-
def _is_false(self, value):
|
456
|
+
def _is_false(self, value: ta.Any) -> bool:
|
406
457
|
# This looks weird, but we're explicitly using equality checks because the truth/false values are different
|
407
458
|
# between python and jmespath.
|
408
|
-
return (
|
459
|
+
return (
|
460
|
+
value == '' or # noqa
|
461
|
+
value == [] or
|
462
|
+
value == {} or
|
463
|
+
value is None or
|
464
|
+
value is False
|
465
|
+
)
|
409
466
|
|
410
|
-
def _is_true(self, value):
|
467
|
+
def _is_true(self, value: ta.Any) -> bool:
|
411
468
|
return not self._is_false(value)
|
412
469
|
|
413
470
|
|
414
|
-
class GraphvizVisitor
|
415
|
-
def __init__(self):
|
471
|
+
class GraphvizVisitor:
|
472
|
+
def __init__(self) -> None:
|
416
473
|
super().__init__()
|
417
|
-
self._lines = []
|
474
|
+
self._lines: list[str] = []
|
418
475
|
self._count = 1
|
419
476
|
|
420
|
-
def visit(self, node
|
477
|
+
def visit(self, node: Node) -> str:
|
421
478
|
self._lines.append('digraph AST {')
|
422
|
-
current = f
|
479
|
+
current = f'{node_type(node)}{self._count}'
|
423
480
|
self._count += 1
|
424
481
|
self._visit(node, current)
|
425
482
|
self._lines.append('}')
|
426
483
|
return '\n'.join(self._lines)
|
427
484
|
|
428
|
-
def
|
429
|
-
|
430
|
-
|
431
|
-
|
485
|
+
def _node_value(self, node: Node) -> lang.Maybe[ta.Any]:
|
486
|
+
if isinstance(node, (ArithmeticUnary, Arithmetic)):
|
487
|
+
return lang.just(node.operator)
|
488
|
+
elif isinstance(node, (Assign, Comparator, FunctionExpression, Field, VariableRef)):
|
489
|
+
return lang.just(node.name)
|
490
|
+
elif isinstance(node, Index):
|
491
|
+
return lang.just(node.index)
|
492
|
+
elif isinstance(node, KeyValPair):
|
493
|
+
return lang.just(node.key_name)
|
494
|
+
elif isinstance(node, Literal):
|
495
|
+
return lang.just(node.literal_value)
|
496
|
+
else:
|
497
|
+
return lang.empty()
|
498
|
+
|
499
|
+
def _visit(self, node: Node, current: str) -> None:
|
500
|
+
self._lines.append(
|
501
|
+
f'{current} '
|
502
|
+
f'[label="{node_type(node)}({self._node_value(node).map(json.dumps).map(html.escape).or_else("")})"]',
|
503
|
+
)
|
504
|
+
for child in node.children:
|
505
|
+
child_name = f'{node_type(child)}{self._count}'
|
432
506
|
self._count += 1
|
433
507
|
self._lines.append(f' {current} -> {child_name}')
|
434
508
|
self._visit(child, child_name)
|