codekingpro/portable-devtools
114k
1import operator2 3from jmespath import functions4from jmespath.compat import string_type5from numbers import Number6 7 8def _equals(x, y):9 if _is_special_number_case(x, y):10 return False11 else:12 return x == y13 14 15def _is_special_number_case(x, y):16 # We need to special case comparing 0 or 1 to17 # True/False. While normally comparing any18 # integer other than 0/1 to True/False will always19 # return False. However 0/1 have this:20 # >>> 0 == True21 # False22 # >>> 0 == False23 # True24 # >>> 1 == True25 # True26 # >>> 1 == False27 # False28 #29 # Also need to consider that:30 # >>> 0 in [True, False]31 # True32 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)36 37 38def _is_comparable(x):39 # The spec doesn't officially support string types yet,40 # but enough people are relying on this behavior that41 # it's been added back. This should eventually become42 # part of the official spec.43 return _is_actual_number(x) or isinstance(x, string_type)44 45 46def _is_actual_number(x):47 # We need to handle python's quirkiness with booleans,48 # specifically:49 #50 # >>> isinstance(False, int)51 # True52 # >>> isinstance(True, int)53 # True54 if isinstance(x, bool):55 return False56 return isinstance(x, Number)57 58 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 interpreter63 # may create dictionaries during the evaluation of a JMESPath64 # expression. For example, a multi-select hash will65 # 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 you68 # want to set a collections.OrderedDict so that you can69 # have predictable key ordering.70 self.dict_cls = dict_cls71 self.custom_functions = custom_functions72 73 74class _Expression(object):75 def __init__(self, expression, interpreter):76 self.expression = expression77 self.interpreter = interpreter78 79 def visit(self, node, *args, **kwargs):80 return self.interpreter.visit(node, *args, **kwargs)81 82 83class Visitor(object):84 def __init__(self):85 self._method_cache = {}86 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] = method94 return method(node, *args, **kwargs)95 96 def default_visit(self, node, *args, **kwargs):97 raise NotImplementedError("default_visit")98 99 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.ge108 }109 _EQUALITY_OPS = ['eq', 'ne']110 MAP_TYPE = dict111 112 def __init__(self, options=None):113 super(TreeInterpreter, self).__init__()114 self._dict_cls = self.MAP_TYPE115 if options is None:116 options = Options()117 self._options = options118 if options.dict_cls is not None:119 self._dict_cls = self._options.dict_cls120 if options.custom_functions is not None:121 self._functions = self._options.custom_functions122 else:123 self._functions = functions.Functions()124 125 def default_visit(self, node, *args, **kwargs):126 raise NotImplementedError(node['type'])127 128 def visit_subexpression(self, node, value):129 result = value130 for node in node['children']:131 result = self.visit(node, result)132 return result133 134 def visit_field(self, node, value):135 try:136 return value.get(node['value'])137 except AttributeError:138 return None139 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 operator151 # 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) and156 _is_comparable(right)):157 return None158 return comparator_func(left, right)159 160 def visit_current(self, node, value):161 return value162 163 def visit_expref(self, node, value):164 return _Expression(node['children'][0], self)165 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)172 173 def visit_filter_projection(self, node, value):174 base = self.visit(node['children'][0], value)175 if not isinstance(base, list):176 return None177 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 collected185 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 None191 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_list198 199 def visit_identity(self, node, value):200 return value201 202 def visit_index(self, node, value):203 # Even though we can index strings, we don't204 # want to support that.205 if not isinstance(value, list):206 return None207 try:208 return value[node['value']]209 except IndexError:210 return None211 212 def visit_index_expression(self, node, value):213 result = value214 for node in node['children']:215 result = self.visit(node, result)216 return result217 218 def visit_slice(self, node, value):219 if not isinstance(value, list):220 return None221 s = slice(*node['children'])222 return value[s]223 224 def visit_key_val_pair(self, node, value):225 return self.visit(node['children'][0], value)226 227 def visit_literal(self, node, value):228 return node['value']229 230 def visit_multi_select_dict(self, node, value):231 if value is None:232 return None233 collected = self._dict_cls()234 for child in node['children']:235 collected[child['value']] = self.visit(child, value)236 return collected237 238 def visit_multi_select_list(self, node, value):239 if value is None:240 return None241 collected = []242 for child in node['children']:243 collected.append(self.visit(child, value))244 return collected245 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 matched251 252 def visit_and_expression(self, node, value):253 matched = self.visit(node['children'][0], value)254 if self._is_false(matched):255 return matched256 return self.visit(node['children'][1], value)257 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 False264 return not original_result265 266 def visit_pipe(self, node, value):267 result = value268 for node in node['children']:269 result = self.visit(node, result)270 return result271 272 def visit_projection(self, node, value):273 base = self.visit(node['children'][0], value)274 if not isinstance(base, list):275 return None276 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 collected282 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 None289 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 collected295 296 def _is_false(self, value):297 # This looks weird, but we're explicitly using equality checks298 # because the truth/false values are different between299 # python and jmespath.300 return (value == '' or value == [] or value == {} or value is None or301 value is False)302 303 def _is_true(self, value):304 return not self._is_false(value)305 306 307class GraphvizVisitor(Visitor):308 def __init__(self):309 super(GraphvizVisitor, self).__init__()310 self._lines = []311 self._count = 1312 313 def visit(self, node, *args, **kwargs):314 self._lines.append('digraph AST {')315 current = '%s%s' % (node['type'], self._count)316 self._count += 1317 self._visit(node, current)318 self._lines.append('}')319 return '\n'.join(self._lines)320 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 += 1327 self._lines.append(' %s -> %s' % (current, child_name))328 self._visit(child, child_name)329 