@@ -35,6 +35,19 @@ def update(self, data, val):
3535
3636 raise NotImplementedError ()
3737
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+
3851 def child (self , child ):
3952 """
4053 Equivalent to Child(self, next) but with some canonicalization
@@ -72,7 +85,6 @@ class DatumInContext(object):
7285 context within that passed in, so an object can be built from the inside
7386 out.
7487 """
75-
7688 @classmethod
7789 def wrap (cls , data ):
7890 if isinstance (data , cls ):
@@ -118,6 +130,7 @@ def __repr__(self):
118130 def __eq__ (self , other ):
119131 return isinstance (other , DatumInContext ) and other .value == self .value and other .path == self .path and self .context == other .context
120132
133+
121134class AutoIdForDatum (DatumInContext ):
122135 """
123136 This behaves like a DatumInContext, but the value is
@@ -185,6 +198,9 @@ def find(self, data):
185198 def update (self , data , val ):
186199 return val
187200
201+ def filter (self , fn , data ):
202+ return data if fn (data ) else None
203+
188204 def __str__ (self ):
189205 return '$'
190206
@@ -194,6 +210,7 @@ def __repr__(self):
194210 def __eq__ (self , other ):
195211 return isinstance (other , Root )
196212
213+
197214class This (JSONPath ):
198215 """
199216 The JSONPath referring to the current datum. Concrete syntax is '@'.
@@ -205,6 +222,9 @@ def find(self, datum):
205222 def update (self , data , val ):
206223 return val
207224
225+ def filter (self , fn , data ):
226+ return data if fn (data ) else None
227+
208228 def __str__ (self ):
209229 return '`this`'
210230
@@ -214,6 +234,7 @@ def __repr__(self):
214234 def __eq__ (self , other ):
215235 return isinstance (other , This )
216236
237+
217238class Child (JSONPath ):
218239 """
219240 JSONPath that first matches the left, then the right.
@@ -240,6 +261,11 @@ def update(self, data, val):
240261 self .right .update (datum .value , val )
241262 return data
242263
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+
243269 def __eq__ (self , other ):
244270 return isinstance (other , Child ) and self .left == other .left and self .right == other .right
245271
@@ -249,6 +275,7 @@ def __str__(self):
249275 def __repr__ (self ):
250276 return '%s(%r, %r)' % (self .__class__ .__name__ , self .left , self .right )
251277
278+
252279class Parent (JSONPath ):
253280 """
254281 JSONPath that matches the parent node of the current match.
@@ -292,6 +319,11 @@ def update(self, data, val):
292319 datum .path .update (data , val )
293320 return data
294321
322+ def filter (self , fn , data ):
323+ for datum in self .find (data ):
324+ datum .path .filter (fn , datum .value )
325+ return data
326+
295327 def __str__ (self ):
296328 return '%s where %s' % (self .left , self .right )
297329
@@ -374,6 +406,33 @@ def update_recursively(data):
374406
375407 return data
376408
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+
377436 def __str__ (self ):
378437 return '%s..%s' % (self .left , self .right )
379438
@@ -421,6 +480,7 @@ def is_singular(self):
421480 def find (self , data ):
422481 raise NotImplementedError ()
423482
483+
424484class Fields (JSONPath ):
425485 """
426486 JSONPath referring to some field of the current object.
@@ -438,7 +498,7 @@ def get_field_datum(self, datum, field):
438498 return AutoIdForDatum (datum )
439499 else :
440500 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`
442502 return DatumInContext (value = field_value , path = Fields (field ), context = datum )
443503 except (TypeError , KeyError , AttributeError ):
444504 return None
@@ -454,11 +514,11 @@ def reified_fields(self, datum):
454514 return ()
455515
456516 def find (self , datum ):
457- datum = DatumInContext .wrap (datum )
517+ datum = DatumInContext .wrap (datum )
458518
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 ]
462522
463523 def update (self , data , val ):
464524 for field in self .reified_fields (DatumInContext .wrap (data )):
@@ -469,6 +529,13 @@ def update(self, data, val):
469529 data [field ] = val
470530 return data
471531
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+
472539 def __str__ (self ):
473540 return ',' .join (map (str , self .fields ))
474541
@@ -506,12 +573,18 @@ def update(self, data, val):
506573 data [self .index ] = val
507574 return data
508575
576+ def filter (self , fn , data ):
577+ if fn (data [self .index ]):
578+ data .pop (self .index ) # relies on mutation :(
579+ return data
580+
509581 def __eq__ (self , other ):
510582 return isinstance (other , Index ) and self .index == other .index
511583
512584 def __str__ (self ):
513585 return '[%i]' % self .index
514586
587+
515588class Slice (JSONPath ):
516589 """
517590 JSONPath matching a slice of an array.
@@ -561,6 +634,18 @@ def update(self, data, val):
561634 datum .path .update (data , val )
562635 return data
563636
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+
564649 def __str__ (self ):
565650 if self .start == None and self .end == None and self .step == None :
566651 return '[*]'
0 commit comments