Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jmespath/visitor.py: 24%
212 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:03 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:03 +0000
1import operator
3from jmespath import functions
4from jmespath.compat import string_type
5from numbers import Number
8def _equals(x, y):
9 if _is_special_number_case(x, y):
10 return False
11 else:
12 return x == y
15def _is_special_number_case(x, y):
16 # We need to special case comparing 0 or 1 to
17 # True/False. While normally comparing any
18 # integer other than 0/1 to True/False will always
19 # return False. However 0/1 have this:
20 # >>> 0 == True
21 # False
22 # >>> 0 == False
23 # True
24 # >>> 1 == True
25 # True
26 # >>> 1 == False
27 # False
28 #
29 # Also need to consider that:
30 # >>> 0 in [True, False]
31 # True
32 if _is_actual_number(x) and x in (0, 1):
33 return isinstance(y, bool)
34 elif _is_actual_number(y) and y in (0, 1):
35 return isinstance(x, bool)
38def _is_comparable(x):
39 # The spec doesn't officially support string types yet,
40 # but enough people are relying on this behavior that
41 # it's been added back. This should eventually become
42 # part of the official spec.
43 return _is_actual_number(x) or isinstance(x, string_type)
46def _is_actual_number(x):
47 # We need to handle python's quirkiness with booleans,
48 # specifically:
49 #
50 # >>> isinstance(False, int)
51 # True
52 # >>> isinstance(True, int)
53 # True
54 if isinstance(x, bool):
55 return False
56 return isinstance(x, Number)
59class Options(object):
60 """Options to control how a JMESPath function is evaluated."""
61 def __init__(self, dict_cls=None, custom_functions=None):
62 #: The class to use when creating a dict. The interpreter
63 # may create dictionaries during the evaluation of a JMESPath
64 # expression. For example, a multi-select hash will
65 # create a dictionary. By default we use a dict() type.
66 # You can set this value to change what dict type is used.
67 # The most common reason you would change this is if you
68 # want to set a collections.OrderedDict so that you can
69 # have predictable key ordering.
70 self.dict_cls = dict_cls
71 self.custom_functions = custom_functions
74class _Expression(object):
75 def __init__(self, expression, interpreter):
76 self.expression = expression
77 self.interpreter = interpreter
79 def visit(self, node, *args, **kwargs):
80 return self.interpreter.visit(node, *args, **kwargs)
83class Visitor(object):
84 def __init__(self):
85 self._method_cache = {}
87 def visit(self, node, *args, **kwargs):
88 node_type = node['type']
89 method = self._method_cache.get(node_type)
90 if method is None:
91 method = getattr(
92 self, 'visit_%s' % node['type'], self.default_visit)
93 self._method_cache[node_type] = method
94 return method(node, *args, **kwargs)
96 def default_visit(self, node, *args, **kwargs):
97 raise NotImplementedError("default_visit")
100class TreeInterpreter(Visitor):
101 COMPARATOR_FUNC = {
102 'eq': _equals,
103 'ne': lambda x, y: not _equals(x, y),
104 'lt': operator.lt,
105 'gt': operator.gt,
106 'lte': operator.le,
107 'gte': operator.ge
108 }
109 _EQUALITY_OPS = ['eq', 'ne']
110 MAP_TYPE = dict
112 def __init__(self, options=None):
113 super(TreeInterpreter, self).__init__()
114 self._dict_cls = self.MAP_TYPE
115 if options is None:
116 options = Options()
117 self._options = options
118 if options.dict_cls is not None:
119 self._dict_cls = self._options.dict_cls
120 if options.custom_functions is not None:
121 self._functions = self._options.custom_functions
122 else:
123 self._functions = functions.Functions()
125 def default_visit(self, node, *args, **kwargs):
126 raise NotImplementedError(node['type'])
128 def visit_subexpression(self, node, value):
129 result = value
130 for node in node['children']:
131 result = self.visit(node, result)
132 return result
134 def visit_field(self, node, value):
135 try:
136 return value.get(node['value'])
137 except AttributeError:
138 return None
140 def visit_comparator(self, node, value):
141 # Common case: comparator is == or !=
142 comparator_func = self.COMPARATOR_FUNC[node['value']]
143 if node['value'] in self._EQUALITY_OPS:
144 return comparator_func(
145 self.visit(node['children'][0], value),
146 self.visit(node['children'][1], value)
147 )
148 else:
149 # Ordering operators are only valid for numbers.
150 # Evaluating any other type with a comparison operator
151 # will yield a None value.
152 left = self.visit(node['children'][0], value)
153 right = self.visit(node['children'][1], value)
154 num_types = (int, float)
155 if not (_is_comparable(left) and
156 _is_comparable(right)):
157 return None
158 return comparator_func(left, right)
160 def visit_current(self, node, value):
161 return value
163 def visit_expref(self, node, value):
164 return _Expression(node['children'][0], self)
166 def visit_function_expression(self, node, value):
167 resolved_args = []
168 for child in node['children']:
169 current = self.visit(child, value)
170 resolved_args.append(current)
171 return self._functions.call_function(node['value'], resolved_args)
173 def visit_filter_projection(self, node, value):
174 base = self.visit(node['children'][0], value)
175 if not isinstance(base, list):
176 return None
177 comparator_node = node['children'][2]
178 collected = []
179 for element in base:
180 if self._is_true(self.visit(comparator_node, element)):
181 current = self.visit(node['children'][1], element)
182 if current is not None:
183 collected.append(current)
184 return collected
186 def visit_flatten(self, node, value):
187 base = self.visit(node['children'][0], value)
188 if not isinstance(base, list):
189 # Can't flatten the object if it's not a list.
190 return None
191 merged_list = []
192 for element in base:
193 if isinstance(element, list):
194 merged_list.extend(element)
195 else:
196 merged_list.append(element)
197 return merged_list
199 def visit_identity(self, node, value):
200 return value
202 def visit_index(self, node, value):
203 # Even though we can index strings, we don't
204 # want to support that.
205 if not isinstance(value, list):
206 return None
207 try:
208 return value[node['value']]
209 except IndexError:
210 return None
212 def visit_index_expression(self, node, value):
213 result = value
214 for node in node['children']:
215 result = self.visit(node, result)
216 return result
218 def visit_slice(self, node, value):
219 if not isinstance(value, list):
220 return None
221 s = slice(*node['children'])
222 return value[s]
224 def visit_key_val_pair(self, node, value):
225 return self.visit(node['children'][0], value)
227 def visit_literal(self, node, value):
228 return node['value']
230 def visit_multi_select_dict(self, node, value):
231 if value is None:
232 return None
233 collected = self._dict_cls()
234 for child in node['children']:
235 collected[child['value']] = self.visit(child, value)
236 return collected
238 def visit_multi_select_list(self, node, value):
239 if value is None:
240 return None
241 collected = []
242 for child in node['children']:
243 collected.append(self.visit(child, value))
244 return collected
246 def visit_or_expression(self, node, value):
247 matched = self.visit(node['children'][0], value)
248 if self._is_false(matched):
249 matched = self.visit(node['children'][1], value)
250 return matched
252 def visit_and_expression(self, node, value):
253 matched = self.visit(node['children'][0], value)
254 if self._is_false(matched):
255 return matched
256 return self.visit(node['children'][1], value)
258 def visit_not_expression(self, node, value):
259 original_result = self.visit(node['children'][0], value)
260 if _is_actual_number(original_result) and original_result == 0:
261 # Special case for 0, !0 should be false, not true.
262 # 0 is not a special cased integer in jmespath.
263 return False
264 return not original_result
266 def visit_pipe(self, node, value):
267 result = value
268 for node in node['children']:
269 result = self.visit(node, result)
270 return result
272 def visit_projection(self, node, value):
273 base = self.visit(node['children'][0], value)
274 if not isinstance(base, list):
275 return None
276 collected = []
277 for element in base:
278 current = self.visit(node['children'][1], element)
279 if current is not None:
280 collected.append(current)
281 return collected
283 def visit_value_projection(self, node, value):
284 base = self.visit(node['children'][0], value)
285 try:
286 base = base.values()
287 except AttributeError:
288 return None
289 collected = []
290 for element in base:
291 current = self.visit(node['children'][1], element)
292 if current is not None:
293 collected.append(current)
294 return collected
296 def _is_false(self, value):
297 # This looks weird, but we're explicitly using equality checks
298 # because the truth/false values are different between
299 # python and jmespath.
300 return (value == '' or value == [] or value == {} or value is None or
301 value is False)
303 def _is_true(self, value):
304 return not self._is_false(value)
307class GraphvizVisitor(Visitor):
308 def __init__(self):
309 super(GraphvizVisitor, self).__init__()
310 self._lines = []
311 self._count = 1
313 def visit(self, node, *args, **kwargs):
314 self._lines.append('digraph AST {')
315 current = '%s%s' % (node['type'], self._count)
316 self._count += 1
317 self._visit(node, current)
318 self._lines.append('}')
319 return '\n'.join(self._lines)
321 def _visit(self, node, current):
322 self._lines.append('%s [label="%s(%s)"]' % (
323 current, node['type'], node.get('value', '')))
324 for child in node.get('children', []):
325 child_name = '%s%s' % (child['type'], self._count)
326 self._count += 1
327 self._lines.append(' %s -> %s' % (current, child_name))
328 self._visit(child, child_name)