@@ -35,6 +35,19 @@ def update(self, data, val):
35
35
36
36
raise NotImplementedError ()
37
37
38
+ def filter (self , fn , data ):
39
+ """
40
+ Returns `data` with the specified path filtering nodes according
41
+ the filter evaluation result returned by the filter function.
42
+
43
+ Arguments:
44
+ fn (function): unary function that accepts one argument
45
+ and returns bool.
46
+ data (dict|list|tuple): JSON object to filter.
47
+ """
48
+
49
+ raise NotImplementedError ()
50
+
38
51
def child (self , child ):
39
52
"""
40
53
Equivalent to Child(self, next) but with some canonicalization
@@ -72,7 +85,6 @@ class DatumInContext(object):
72
85
context within that passed in, so an object can be built from the inside
73
86
out.
74
87
"""
75
-
76
88
@classmethod
77
89
def wrap (cls , data ):
78
90
if isinstance (data , cls ):
@@ -118,6 +130,7 @@ def __repr__(self):
118
130
def __eq__ (self , other ):
119
131
return isinstance (other , DatumInContext ) and other .value == self .value and other .path == self .path and self .context == other .context
120
132
133
+
121
134
class AutoIdForDatum (DatumInContext ):
122
135
"""
123
136
This behaves like a DatumInContext, but the value is
@@ -185,6 +198,9 @@ def find(self, data):
185
198
def update (self , data , val ):
186
199
return val
187
200
201
+ def filter (self , fn , data ):
202
+ return data if fn (data ) else None
203
+
188
204
def __str__ (self ):
189
205
return '$'
190
206
@@ -194,6 +210,7 @@ def __repr__(self):
194
210
def __eq__ (self , other ):
195
211
return isinstance (other , Root )
196
212
213
+
197
214
class This (JSONPath ):
198
215
"""
199
216
The JSONPath referring to the current datum. Concrete syntax is '@'.
@@ -205,6 +222,9 @@ def find(self, datum):
205
222
def update (self , data , val ):
206
223
return val
207
224
225
+ def filter (self , fn , data ):
226
+ return data if fn (data ) else None
227
+
208
228
def __str__ (self ):
209
229
return '`this`'
210
230
@@ -214,6 +234,7 @@ def __repr__(self):
214
234
def __eq__ (self , other ):
215
235
return isinstance (other , This )
216
236
237
+
217
238
class Child (JSONPath ):
218
239
"""
219
240
JSONPath that first matches the left, then the right.
@@ -240,6 +261,11 @@ def update(self, data, val):
240
261
self .right .update (datum .value , val )
241
262
return data
242
263
264
+ def filter (self , fn , data ):
265
+ for datum in self .left .find (data ):
266
+ self .right .filter (fn , datum .value )
267
+ return data
268
+
243
269
def __eq__ (self , other ):
244
270
return isinstance (other , Child ) and self .left == other .left and self .right == other .right
245
271
@@ -249,6 +275,7 @@ def __str__(self):
249
275
def __repr__ (self ):
250
276
return '%s(%r, %r)' % (self .__class__ .__name__ , self .left , self .right )
251
277
278
+
252
279
class Parent (JSONPath ):
253
280
"""
254
281
JSONPath that matches the parent node of the current match.
@@ -292,6 +319,11 @@ def update(self, data, val):
292
319
datum .path .update (data , val )
293
320
return data
294
321
322
+ def filter (self , fn , data ):
323
+ for datum in self .find (data ):
324
+ datum .path .filter (fn , datum .value )
325
+ return data
326
+
295
327
def __str__ (self ):
296
328
return '%s where %s' % (self .left , self .right )
297
329
@@ -374,6 +406,33 @@ def update_recursively(data):
374
406
375
407
return data
376
408
409
+ def filter (self , fn , data ):
410
+ # Get all left matches into a list
411
+ left_matches = self .left .find (data )
412
+ if not isinstance (left_matches , list ):
413
+ left_matches = [left_matches ]
414
+
415
+ def filter_recursively (data ):
416
+ # Update only mutable values corresponding to JSON types
417
+ if not (isinstance (data , list ) or isinstance (data , dict )):
418
+ return
419
+
420
+ self .right .filter (fn , data )
421
+
422
+ # Manually do the * or [*] to avoid coercion and recurse just the right-hand pattern
423
+ if isinstance (data , list ):
424
+ for i in range (0 , len (data )):
425
+ filter_recursively (data [i ])
426
+
427
+ elif isinstance (data , dict ):
428
+ for field in data .keys ():
429
+ filter_recursively (data [field ])
430
+
431
+ for submatch in left_matches :
432
+ filter_recursively (submatch .value )
433
+
434
+ return data
435
+
377
436
def __str__ (self ):
378
437
return '%s..%s' % (self .left , self .right )
379
438
@@ -421,6 +480,7 @@ def is_singular(self):
421
480
def find (self , data ):
422
481
raise NotImplementedError ()
423
482
483
+
424
484
class Fields (JSONPath ):
425
485
"""
426
486
JSONPath referring to some field of the current object.
@@ -438,7 +498,7 @@ def get_field_datum(self, datum, field):
438
498
return AutoIdForDatum (datum )
439
499
else :
440
500
try :
441
- field_value = datum .value [field ] # Do NOT use `val.get(field)` since that confuses None as a value and None due to `get`
501
+ field_value = datum .value [field ] # Do NOT use `val.get(field)` since that confuses None as a value and None due to `get`
442
502
return DatumInContext (value = field_value , path = Fields (field ), context = datum )
443
503
except (TypeError , KeyError , AttributeError ):
444
504
return None
@@ -454,11 +514,11 @@ def reified_fields(self, datum):
454
514
return ()
455
515
456
516
def find (self , datum ):
457
- datum = DatumInContext .wrap (datum )
517
+ datum = DatumInContext .wrap (datum )
458
518
459
- return [field_datum
460
- for field_datum in [self .get_field_datum (datum , field ) for field in self .reified_fields (datum )]
461
- if field_datum is not None ]
519
+ return [field_datum
520
+ for field_datum in [self .get_field_datum (datum , field ) for field in self .reified_fields (datum )]
521
+ if field_datum is not None ]
462
522
463
523
def update (self , data , val ):
464
524
for field in self .reified_fields (DatumInContext .wrap (data )):
@@ -469,6 +529,13 @@ def update(self, data, val):
469
529
data [field ] = val
470
530
return data
471
531
532
+ def filter (self , fn , data ):
533
+ for field in self .reified_fields (DatumInContext .wrap (data )):
534
+ if field in data :
535
+ if fn (data [field ]):
536
+ data .pop (field )
537
+ return data
538
+
472
539
def __str__ (self ):
473
540
return ',' .join (map (str , self .fields ))
474
541
@@ -506,12 +573,18 @@ def update(self, data, val):
506
573
data [self .index ] = val
507
574
return data
508
575
576
+ def filter (self , fn , data ):
577
+ if fn (data [self .index ]):
578
+ data .pop (self .index ) # relies on mutation :(
579
+ return data
580
+
509
581
def __eq__ (self , other ):
510
582
return isinstance (other , Index ) and self .index == other .index
511
583
512
584
def __str__ (self ):
513
585
return '[%i]' % self .index
514
586
587
+
515
588
class Slice (JSONPath ):
516
589
"""
517
590
JSONPath matching a slice of an array.
@@ -561,6 +634,18 @@ def update(self, data, val):
561
634
datum .path .update (data , val )
562
635
return data
563
636
637
+ def filter (self , fn , data ):
638
+ while True :
639
+ length = len (data )
640
+ for datum in self .find (data ):
641
+ data = datum .path .filter (fn , data )
642
+ if len (data ) < length :
643
+ break
644
+
645
+ if length == len (data ):
646
+ break
647
+ return data
648
+
564
649
def __str__ (self ):
565
650
if self .start == None and self .end == None and self .step == None :
566
651
return '[*]'
0 commit comments