Coverage for jsonfind/jsonfind.py: 71%

261 statements  

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

9 

10import fnmatch 

11import re 

12from logging import getLogger 

13 

14import jsonpath 

15import jsonpointer 

16 

17try: 

18 import pyjq 

19except (ModuleNotFoundError, ImportError): 

20 pyjq = None 

21try: 

22 import jsonselect 

23except (ModuleNotFoundError, ImportError): 

24 jsonselect = None 

25 

26log = getLogger(__name__) 

27 

28 

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 

45 

46 

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 

63 

64 

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 

82 

83 

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 

101 

102 

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) 

123 

124 

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) 

144 

145 

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) 

164 

165 

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) 

193 

194 

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

213 

214 

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) 

246 

247 

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) 

283 

284 

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 ) 

297 

298 

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

307 

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

316 

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 

329 

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 

339 

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 

349 

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 

359 

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 

371 

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 

383 

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 

395 

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 

405 

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 

415 

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 

427 

428 @classmethod 

429 def find_eq(cls, obj, target): 

430 return next(cls.filter_eq(obj, target), None) 

431 

432 @classmethod 

433 def find_is(cls, obj, target): 

434 return next(cls.filter_is(obj, target), None) 

435 

436 @classmethod 

437 def find_attr_eq(cls, obj, target): 

438 return next(cls.filter_attr_eq(obj, target), None) 

439 

440 @classmethod 

441 def find_attr_is(cls, obj, target): 

442 return next(cls.filter_attr_is(obj, target), None) 

443 

444 @classmethod 

445 def find_subset(cls, obj, target): 

446 return next(cls.filter_subset(obj, target), None) 

447 

448 @classmethod 

449 def find_superset(cls, obj, target): 

450 return next(cls.filter_superset(obj, target), None) 

451 

452 @classmethod 

453 def find_key(cls, obj, target, prev=None): 

454 return next(cls.filter_key(obj, target, prev), None) 

455 

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

467 

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

474 

475 @classmethod 

476 def to_jsonpointer(cls, val): 

477 return "/" + "/".join([cls.escape_jsonptr(x) for x in val]) 

478 

479 @classmethod 

480 def format_to(cls, mode, val): 

481 fn = getattr(cls, f"to_{mode}") 

482 return fn(val) 

483 

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 

495 

496 

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