Skip to content

Commit 634f968

Browse files
committed
feat(jsonpath): add filter capability
1 parent fd8ceba commit 634f968

File tree

1 file changed

+91
-6
lines changed

1 file changed

+91
-6
lines changed

jsonpath_ng/jsonpath.py

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
121134
class 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+
197214
class 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+
217238
class 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+
252279
class 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+
424484
class 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+
515588
class 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

Comments
 (0)