Skip to content

Commit 58378a6

Browse files
kaapstormmichaelmior
authored andcommitted
Add wherenot
1 parent d5a766f commit 58378a6

File tree

7 files changed

+66
-15
lines changed

7 files changed

+66
-15
lines changed

README.rst

+15-13
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,21 @@ Atomic expressions:
126126

127127
Jsonpath operators:
128128

129-
+-------------------------------------+------------------------------------------------------------------------------------+
130-
| Syntax | Meaning |
131-
+=====================================+====================================================================================+
132-
| *jsonpath1* ``.`` *jsonpath2* | All nodes matched by *jsonpath2* starting at any node matching *jsonpath1* |
133-
+-------------------------------------+------------------------------------------------------------------------------------+
134-
| *jsonpath* ``[`` *whatever* ``]`` | Same as *jsonpath*\ ``.``\ *whatever* |
135-
+-------------------------------------+------------------------------------------------------------------------------------+
136-
| *jsonpath1* ``..`` *jsonpath2* | All nodes matched by *jsonpath2* that descend from any node matching *jsonpath1* |
137-
+-------------------------------------+------------------------------------------------------------------------------------+
138-
| *jsonpath1* ``where`` *jsonpath2* | Any nodes matching *jsonpath1* with a child matching *jsonpath2* |
139-
+-------------------------------------+------------------------------------------------------------------------------------+
140-
| *jsonpath1* ``|`` *jsonpath2* | Any nodes matching the union of *jsonpath1* and *jsonpath2* |
141-
+-------------------------------------+------------------------------------------------------------------------------------+
129+
+--------------------------------------+-----------------------------------------------------------------------------------+
130+
| Syntax | Meaning |
131+
+======================================+===================================================================================+
132+
| *jsonpath1* ``.`` *jsonpath2* | All nodes matched by *jsonpath2* starting at any node matching *jsonpath1* |
133+
+--------------------------------------+-----------------------------------------------------------------------------------+
134+
| *jsonpath* ``[`` *whatever* ``]`` | Same as *jsonpath*\ ``.``\ *whatever* |
135+
+--------------------------------------+-----------------------------------------------------------------------------------+
136+
| *jsonpath1* ``..`` *jsonpath2* | All nodes matched by *jsonpath2* that descend from any node matching *jsonpath1* |
137+
+--------------------------------------+-----------------------------------------------------------------------------------+
138+
| *jsonpath1* ``where`` *jsonpath2* | Any nodes matching *jsonpath1* with a child matching *jsonpath2* |
139+
+--------------------------------------+-----------------------------------------------------------------------------------+
140+
| *jsonpath1* ``wherenot`` *jsonpath2* | Any nodes matching *jsonpath1* with a child not matching *jsonpath2* |
141+
+--------------------------------------+-----------------------------------------------------------------------------------+
142+
| *jsonpath1* ``|`` *jsonpath2* | Any nodes matching the union of *jsonpath1* and *jsonpath2* |
143+
+--------------------------------------+-----------------------------------------------------------------------------------+
142144

143145
Field specifiers ( *field* ):
144146

jsonpath_ng/jsonpath.py

+30
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,36 @@ def __eq__(self, other):
369369
def __hash__(self):
370370
return hash((self.left, self.right))
371371

372+
373+
class WhereNot(Where):
374+
"""
375+
Identical to ``Where``, but filters for only those nodes that
376+
do *not* have a match on the right.
377+
378+
>>> jsonpath = WhereNot(Fields('spam'), Fields('spam'))
379+
>>> jsonpath.find({"spam": {"spam": 1}})
380+
[]
381+
>>> matches = jsonpath.find({"spam": 1})
382+
>>> matches[0].value
383+
1
384+
385+
"""
386+
def find(self, data):
387+
return [subdata for subdata in self.left.find(data)
388+
if not self.right.find(subdata)]
389+
390+
def __str__(self):
391+
return '%s wherenot %s' % (self.left, self.right)
392+
393+
def __eq__(self, other):
394+
return (isinstance(other, WhereNot)
395+
and other.left == self.left
396+
and other.right == self.right)
397+
398+
def __hash__(self):
399+
return hash((self.left, self.right))
400+
401+
372402
class Descendants(JSONPath):
373403
"""
374404
JSONPath that matches first the left expression then any descendant

jsonpath_ng/lexer.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ def tokenize(self, string):
4848

4949
literals = ['*', '.', '[', ']', '(', ')', '$', ',', ':', '|', '&', '~']
5050

51-
reserved_words = { 'where': 'WHERE' }
51+
reserved_words = {
52+
'where': 'WHERE',
53+
'wherenot': 'WHERENOT',
54+
}
5255

5356
tokens = ['DOUBLEDOT', 'NUMBER', 'ID', 'NAMED_OPERATOR'] + list(reserved_words.values())
5457

jsonpath_ng/parser.py

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def parse_token_stream(self, token_iterator):
6969
('left', '|'),
7070
('left', '&'),
7171
('left', 'WHERE'),
72+
('left', 'WHERENOT'),
7273
]
7374

7475
def p_error(self, t):
@@ -81,6 +82,7 @@ def p_jsonpath_binop(self, p):
8182
"""jsonpath : jsonpath '.' jsonpath
8283
| jsonpath DOUBLEDOT jsonpath
8384
| jsonpath WHERE jsonpath
85+
| jsonpath WHERENOT jsonpath
8486
| jsonpath '|' jsonpath
8587
| jsonpath '&' jsonpath"""
8688
op = p[2]
@@ -91,6 +93,8 @@ def p_jsonpath_binop(self, p):
9193
p[0] = Descendants(p[1], p[3])
9294
elif op == 'where':
9395
p[0] = Where(p[1], p[3])
96+
elif op == 'wherenot':
97+
p[0] = WhereNot(p[1], p[3])
9498
elif op == '|':
9599
p[0] = Union(p[1], p[3])
96100
elif op == '&':

tests/test_jsonpath.py

+10
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ def test_datumincontext_in_context_nested():
127127
{"foo": {"bar": 3, "flag": 1}, "baz": {"bar": 2}},
128128
),
129129
#
130+
# WhereNot
131+
# --------
132+
#
133+
(
134+
'(* wherenot flag) .. bar',
135+
{'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 2}},
136+
4,
137+
{'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 4}},
138+
),
139+
#
130140
# Lambdas
131141
# -------
132142
#

tests/test_lexer.py

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
("`this`", (("this", "NAMED_OPERATOR"),)),
2525
("|", (("|", "|"),)),
2626
("where", (("where", "WHERE"),)),
27+
("wherenot", (("wherenot", "WHERENOT"),)),
2728
)
2829

2930

tests/test_parser.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from jsonpath_ng.jsonpath import Child, Descendants, Fields, Index, Slice, Where
3+
from jsonpath_ng.jsonpath import Child, Descendants, Fields, Index, Slice, Where, WhereNot
44
from jsonpath_ng.lexer import JsonPathLexer
55
from jsonpath_ng.parser import JsonPathParser
66

@@ -27,6 +27,7 @@
2727
("foo.baz", Child(Fields("foo"), Fields("baz"))),
2828
("foo.baz,bizzle", Child(Fields("foo"), Fields("baz", "bizzle"))),
2929
("foo where baz", Where(Fields("foo"), Fields("baz"))),
30+
("foo wherenot baz", WhereNot(Fields("foo"), Fields("baz"))),
3031
("foo..baz", Descendants(Fields("foo"), Fields("baz"))),
3132
("foo..baz.bing", Descendants(Fields("foo"), Child(Fields("baz"), Fields("bing")))),
3233
)

0 commit comments

Comments
 (0)