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

1import operator 

2 

3from jmespath import functions 

4from jmespath.compat import string_type 

5from numbers import Number 

6 

7 

8def _equals(x, y): 

9 if _is_special_number_case(x, y): 

10 return False 

11 else: 

12 return x == y 

13 

14 

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) 

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 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) 

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 # True 

52 # >>> isinstance(True, int) 

53 # True 

54 if isinstance(x, bool): 

55 return False 

56 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 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 

72 

73 

74class _Expression(object): 

75 def __init__(self, expression, interpreter): 

76 self.expression = expression 

77 self.interpreter = interpreter 

78 

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] = method 

94 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.ge 

108 } 

109 _EQUALITY_OPS = ['eq', 'ne'] 

110 MAP_TYPE = dict 

111 

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() 

124 

125 def default_visit(self, node, *args, **kwargs): 

126 raise NotImplementedError(node['type']) 

127 

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 

133 

134 def visit_field(self, node, value): 

135 try: 

136 return value.get(node['value']) 

137 except AttributeError: 

138 return None 

139 

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) 

159 

160 def visit_current(self, node, value): 

161 return value 

162 

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 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 

185 

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 

198 

199 def visit_identity(self, node, value): 

200 return value 

201 

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 

211 

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 

217 

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] 

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 None 

233 collected = self._dict_cls() 

234 for child in node['children']: 

235 collected[child['value']] = self.visit(child, value) 

236 return collected 

237 

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 

245 

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 

251 

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) 

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 False 

264 return not original_result 

265 

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 

271 

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 

282 

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 

295 

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) 

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 = 1 

312 

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) 

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 += 1 

327 self._lines.append(' %s -> %s' % (current, child_name)) 

328 self._visit(child, child_name)