Coverage for jsonfind/jsonfind.py: 71%
261 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 14:23 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 14:23 +0000
1"""
2>>> obj = {"a":"b","c":{"d":"e"}}
3>>> tgt = obj["c"]
4>>> JsonFind.to_jsonpointer(JsonFind.find_eq(obj, tgt))
5'/c'
6>>> JsonFind.to_jsonpath(JsonFind.find_eq(obj, tgt))
7'c'
8"""
10import fnmatch
11import re
12from logging import getLogger
14import jsonpath
15import jsonpointer
17try:
18 import pyjq
19except (ModuleNotFoundError, ImportError):
20 pyjq = None
21try:
22 import jsonselect
23except (ModuleNotFoundError, ImportError):
24 jsonselect = None
26log = getLogger(__name__)
29def EQ(a, b):
30 """
31 >>> EQ(1, 2)
32 False
33 >>> EQ(1, 1)
34 True
35 >>> EQ("abc", "abc")
36 True
37 >>> EQ("abc", "def")
38 False
39 >>> EQ({"a":"b"}, {"a":"b"})
40 True
41 >>> EQ({"a":"b"}, {"a":"c"})
42 False
43 """
44 return a == b
47def IS(a, b):
48 """
49 >>> IS(1, 2)
50 False
51 >>> IS(1, 1)
52 True
53 >>> IS("abc", "abc")
54 True
55 >>> IS("abc", "def")
56 False
57 >>> IS({"a":"b"}, {"a":"b"})
58 False
59 >>> IS({"a":"b"}, {"a":"c"})
60 False
61 """
62 return a is b
65def IN1(a, b):
66 """
67 >>> IN1(1, [1,2,3])
68 True
69 >>> IN1([1,2,3], 1)
70 False
71 >>> IN1(1, [2,3])
72 False
73 >>> IN1("abc", "hello abc world")
74 True
75 >>> IN1("xyz", "hello abc world")
76 False
77 """
78 try:
79 return a in b
80 except TypeError:
81 return False
84def IN2(a, b):
85 """
86 >>> IN2(1, [1,2,3])
87 False
88 >>> IN2([1,2,3], 1)
89 True
90 >>> IN2([2,3], 1)
91 False
92 >>> IN2("hello abc world", "abc")
93 True
94 >>> IN2("hello abc world", "xyz")
95 False
96 """
97 try:
98 return b in a
99 except TypeError:
100 return False
103def compare_regexp(a, b):
104 """
105 >>> compare_regexp("abcde", "bcd")
106 False
107 >>> compare_regexp("abcde", "a[bcd]{3}e")
108 True
109 >>> compare_regexp("a,bcde", "[abcde,]*")
110 True
111 >>> compare_regexp("abcde", re.compile("bcd[ef]"))
112 False
113 >>> compare_regexp({"b":"abcde"}, "bcd[ef]")
114 False
115 """
116 log.debug("compare(regexp) %s %s", a, b)
117 if isinstance(a, str):
118 if hasattr(b, "fullmatch"):
119 return b.fullmatch(a) is not None
120 elif isinstance(b, str): 120 ↛ 122line 120 didn't jump to line 122 because the condition on line 120 was always true
121 return re.fullmatch(b, a) is not None
122 return EQ(a, b)
125def compare_regexp_substr(a, b):
126 """
127 >>> compare_regexp_substr("abcde", "bcd")
128 True
129 >>> compare_regexp_substr("abcde", "bcdf")
130 False
131 >>> compare_regexp_substr("abcde", "bcd[ef]")
132 True
133 >>> compare_regexp_substr("abcde", re.compile("bcd[ef]"))
134 True
135 >>> compare_regexp_substr({"b":"abcde"}, "bcd[ef]")
136 False
137 """
138 if isinstance(a, str):
139 if hasattr(b, "search"):
140 return b.search(a) is not None
141 elif isinstance(b, str): 141 ↛ 143line 141 didn't jump to line 143 because the condition on line 141 was always true
142 return re.search(b, a) is not None
143 return EQ(a, b)
146def compare_fnmatch(a, b):
147 """
148 >>> compare_fnmatch("world", "worl?")
149 True
150 >>> compare_fnmatch("world", "word*")
151 False
152 >>> compare_fnmatch("world", "wor*d")
153 True
154 >>> compare_fnmatch("world", "?or??")
155 True
156 >>> compare_fnmatch("world", "?and?")
157 False
158 >>> compare_fnmatch(1, 2)
159 False
160 """
161 if isinstance(a, str) and isinstance(b, str):
162 return fnmatch.fnmatch(a, b)
163 return EQ(a, b)
166def compare_range(a, b):
167 """
168 >>> compare_range(1, "-10")
169 True
170 >>> compare_range(1, "10-")
171 False
172 >>> compare_range(20, "-10")
173 False
174 >>> compare_range(1, "10-20")
175 False
176 >>> compare_range(1.0, "0-1.0")
177 True
178 >>> compare_range(100, "-")
179 True
180 >>> compare_range("b", "a-z")
181 True
182 >>> compare_range("b", "b")
183 True
184 >>> compare_range("b", "a")
185 False
186 """
187 if "-" not in b:
188 return a == type(a)(b)
189 bmin, bmax = b.split("-", 1)
190 if bmin not in (None, "") and type(a)(bmin) > a:
191 return False
192 return not (bmax not in (None, "") and type(a)(bmax) < a)
195def compare_eval(a, b):
196 """
197 >>> compare_eval(1, "x%2==1")
198 True
199 >>> compare_eval(1, "0<x/3 and x/3 <=1.0")
200 True
201 >>> compare_eval("xyz", "len(x)==3")
202 True
203 >>> compare_eval("xyz", 'hash(x)!=hash("xyz")')
204 False
205 >>> compare_eval("xyz", 'x[2]=="z"')
206 True
207 >>> compare_eval("xyzxyz", 'x[:int(len(x)/2)]==x[int(len(x)/2):]')
208 True
209 >>> compare_eval("xyzxyz123123", 'x[:int(len(x)/2)]==x[int(len(x)/2):]')
210 False
211 """
212 return eval(b, {}, {"x": a})
215def compare_subset(a, b, key_fn=EQ, val_fn=EQ):
216 """
217 >>> compare_subset({"a":"b", "c":{"d":"e"}}, {"a":"b"})
218 True
219 >>> compare_subset({"a":"b", "c":{"d":"e"}}, {"a":"c"})
220 False
221 >>> compare_subset({"a":"b", "c":{"d":"e"}}, {"a":"b", "c":"d"})
222 False
223 >>> compare_subset({"a":"b", "c":{"d":"e"}}, {})
224 True
225 >>> compare_subset([1,2,3], [1,3,5,2,4])
226 False
227 >>> compare_subset([1,2,3], [1,3])
228 True
229 """
230 if isinstance(b, (list, tuple)) and isinstance(a, (list, tuple)):
231 for ib in b:
232 if not any(compare_subset(ia, ib, key_fn, val_fn) for ia in a):
233 return False
234 return True
235 elif isinstance(b, dict) and isinstance(a, dict):
236 for kb, vb in b.items():
237 flag = False
238 for ka, va in filter(lambda f: key_fn(f[0], kb), a.items()):
239 if compare_subset(va, vb, key_fn, val_fn):
240 flag = True
241 break
242 if not flag:
243 return False
244 return True
245 return val_fn(a, b)
248def compare_superset(a, b, key_fn=EQ, val_fn=EQ):
249 """
250 >>> compare_superset({"a":"b", "c":{"d":"e"}}, {"a":"b", "c":{"d":"e", "f":"g"}})
251 True
252 >>> compare_superset({"a":"b", "c":{"d":"e"}}, {"a":"b"})
253 False
254 >>> compare_superset({"a":"b", "c":{"d":"e"}}, {"a":"b", "c":"d"})
255 False
256 >>> compare_superset({"a":"b", "c":{"d":"e"}}, {})
257 False
258 >>> compare_superset({"a":"b"}, {"a":"b"})
259 True
260 >>> compare_superset({"a":"b"}, {"a":"b", "c":"d"})
261 True
262 >>> compare_superset([1,2,3], [1,3,5,2,4])
263 True
264 >>> compare_superset([1,2,3], [1,3])
265 False
266 """
267 if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
268 for ia in a:
269 if not any(compare_superset(ia, ib, key_fn, val_fn) for ib in b):
270 return False
271 return True
272 elif isinstance(a, dict) and isinstance(b, dict):
273 for ka, va in a.items():
274 flag = False
275 for kb, vb in filter(lambda f: key_fn(ka, f[0]), b.items()):
276 if compare_superset(va, vb, key_fn, val_fn):
277 flag = True
278 break
279 if not flag:
280 return False
281 return True
282 return val_fn(a, b)
285def compare_set(a, b, key_fn=EQ, val_fn=EQ):
286 """
287 >>> compare_set({"a":"b", "c":"d"}, {"a":"b", "c":"d"})
288 True
289 >>> compare_set({"a":"b", "c":"d"}, {"a":"b"})
290 False
291 >>> compare_set({"a":"b"}, {"a":"b", "c":"d"})
292 False
293 """
294 return compare_subset(a, b, key_fn, val_fn) and compare_superset(
295 a, b, key_fn, val_fn
296 )
299class JsonFind:
300 @classmethod
301 def get_children(cls, obj):
302 if isinstance(obj, dict):
303 return obj.items()
304 elif isinstance(obj, (tuple, list)):
305 return enumerate(obj)
306 return []
308 @classmethod
309 def get_children_attr(cls, obj):
310 """
311 >>> class A: hello="world"
312 >>> list(filter(lambda f: not f[0].startswith("_"), JsonFind.get_children_attr(A)))
313 [('hello', 'world')]
314 """
315 return obj.__dict__.items()
317 @classmethod
318 def issubset(cls, obj, target):
319 if isinstance(target, dict) and isinstance(obj, dict):
320 if obj.items() >= target.items():
321 return True
322 elif (
323 isinstance(target, (list, tuple))
324 and isinstance(obj, (list, tuple))
325 and all(x in obj for x in target)
326 ):
327 return True
328 return False
330 @classmethod
331 def filter_subset(cls, obj, target):
332 if cls.issubset(obj, target):
333 yield []
334 return
335 for k, v in cls.get_children(obj):
336 for chld in cls.filter_subset(v, target):
337 yield [k, *chld]
338 return
340 @classmethod
341 def filter_eq(cls, obj, target):
342 if obj == target:
343 yield []
344 return
345 for k, v in cls.get_children(obj):
346 for chld in cls.filter_eq(v, target):
347 yield [k, *chld]
348 return
350 @classmethod
351 def filter_is(cls, obj, target):
352 if obj is target:
353 yield []
354 return
355 for k, v in cls.get_children(obj):
356 for chld in cls.filter_is(v, target):
357 yield [k, *chld]
358 return
360 @classmethod
361 def filter_compare(cls, obj, target, key_fn=IS, val_fn=IS):
362 if compare_set(obj, target, key_fn, val_fn):
363 log.debug("found %s %s", obj, target)
364 yield []
365 return
366 log.debug("not-found %s %s", obj, target)
367 for k, v in cls.get_children(obj):
368 for chld in cls.filter_compare(v, target, key_fn, val_fn):
369 yield [k, *chld]
370 return
372 @classmethod
373 def filter_compare_subset(cls, obj, target, key_fn=IS, val_fn=IS):
374 if compare_subset(obj, target, key_fn, val_fn):
375 log.debug("found %s %s", obj, target)
376 yield []
377 return
378 log.debug("not-found %s %s", obj, target)
379 for k, v in cls.get_children(obj):
380 for chld in cls.filter_compare_subset(v, target, key_fn, val_fn):
381 yield [k, *chld]
382 return
384 @classmethod
385 def filter_compare_superset(cls, obj, target, key_fn=IS, val_fn=IS):
386 if compare_superset(obj, target, key_fn, val_fn):
387 log.debug("found %s %s", obj, target)
388 yield []
389 return
390 log.debug("not-found %s %s", obj, target)
391 for k, v in cls.get_children(obj):
392 for chld in cls.filter_compare_superset(v, target, key_fn, val_fn):
393 yield [k, *chld]
394 return
396 @classmethod
397 def filter_attr_eq(cls, obj, target):
398 if obj == target:
399 yield []
400 return
401 for k, v in cls.get_children_attr(obj):
402 for chld in cls.filter_attr_eq(v, target):
403 yield [k, *chld]
404 return
406 @classmethod
407 def filter_attr_is(cls, obj, target):
408 if obj is target:
409 yield []
410 return
411 for k, v in cls.get_children_attr(obj):
412 for chld in cls.filter_attr_is(v, target):
413 yield [k, *chld]
414 return
416 @classmethod
417 def filter_key(cls, obj, target, prev=None):
418 if prev is None:
419 prev = []
420 if prev[-len(target) :] == target:
421 yield []
422 return
423 for k, v in cls.get_children(obj):
424 for chld in cls.filter_key(v, target, [*prev, k]):
425 yield [k, *chld]
426 return
428 @classmethod
429 def find_eq(cls, obj, target):
430 return next(cls.filter_eq(obj, target), None)
432 @classmethod
433 def find_is(cls, obj, target):
434 return next(cls.filter_is(obj, target), None)
436 @classmethod
437 def find_attr_eq(cls, obj, target):
438 return next(cls.filter_attr_eq(obj, target), None)
440 @classmethod
441 def find_attr_is(cls, obj, target):
442 return next(cls.filter_attr_is(obj, target), None)
444 @classmethod
445 def find_subset(cls, obj, target):
446 return next(cls.filter_subset(obj, target), None)
448 @classmethod
449 def find_superset(cls, obj, target):
450 return next(cls.filter_superset(obj, target), None)
452 @classmethod
453 def find_key(cls, obj, target, prev=None):
454 return next(cls.filter_key(obj, target, prev), None)
456 @classmethod
457 def to_jsonpath(cls, val):
458 res = ""
459 for i in val:
460 if isinstance(i, str):
461 res += "." + i
462 elif isinstance(i, int): 462 ↛ 465line 462 didn't jump to line 465 because the condition on line 462 was always true
463 res += f"[{i}]"
464 else:
465 raise TypeError(f"invalid type: {i} ({val})")
466 return res.lstrip(".")
468 @classmethod
469 def escape_jsonptr(cls, s):
470 if not isinstance(s, str):
471 return str(s)
472 s = s.replace("~", "~0")
473 return s.replace("/", "~1")
475 @classmethod
476 def to_jsonpointer(cls, val):
477 return "/" + "/".join([cls.escape_jsonptr(x) for x in val])
479 @classmethod
480 def format_to(cls, mode, val):
481 fn = getattr(cls, f"to_{mode}")
482 return fn(val)
484 @classmethod
485 def find_by(cls, mode, obj, path):
486 if mode == "jsonpointer":
487 return jsonpointer.resolve_pointer(obj, path)
488 elif mode == "jsonpath":
489 return jsonpath.jsonpath(obj, path)
490 elif pyjq is not None and mode == "jq":
491 return pyjq.all(path, obj)
492 elif jsonselect is not None and mode == "jsonselect":
493 return list(jsonselect.match(path, obj))
494 return None
497format_list = [
498 x.split("_", 1)[-1] for x in filter(lambda f: f.startswith("to_"), dir(JsonFind))
499]
500find_format_list = [*format_list]
501if pyjq is not None and "jq" not in find_format_list: 501 ↛ 502line 501 didn't jump to line 502 because the condition on line 501 was never true
502 find_format_list.append("jq")
503if jsonselect is not None and "jsonselect" not in find_format_list: 503 ↛ 504line 503 didn't jump to line 504 because the condition on line 503 was never true
504 find_format_list.append("jsonselect")