Skip to content

Commit ee76178

Browse files
committed
Add wherenot
1 parent e05fbc2 commit ee76178

File tree

7 files changed

+62
-14
lines changed

7 files changed

+62
-14
lines changed

README.rst

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,21 @@ Atomic expressions:
109109

110110
Jsonpath operators:
111111

112-
+-------------------------------------+------------------------------------------------------------------------------------+
113-
| Syntax | Meaning |
114-
+=====================================+====================================================================================+
115-
| *jsonpath1* ``.`` *jsonpath2* | All nodes matched by *jsonpath2* starting at any node matching *jsonpath1* |
116-
+-------------------------------------+------------------------------------------------------------------------------------+
117-
| *jsonpath* ``[`` *whatever* ``]`` | Same as *jsonpath*\ ``.``\ *whatever* |
118-
+-------------------------------------+------------------------------------------------------------------------------------+
119-
| *jsonpath1* ``..`` *jsonpath2* | All nodes matched by *jsonpath2* that descend from any node matching *jsonpath1* |
120-
+-------------------------------------+------------------------------------------------------------------------------------+
121-
| *jsonpath1* ``where`` *jsonpath2* | Any nodes matching *jsonpath1* with a child matching *jsonpath2* |
122-
+-------------------------------------+------------------------------------------------------------------------------------+
123-
| *jsonpath1* ``|`` *jsonpath2* | Any nodes matching the union of *jsonpath1* and *jsonpath2* |
124-
+-------------------------------------+------------------------------------------------------------------------------------+
112+
+--------------------------------------+-----------------------------------------------------------------------------------+
113+
| Syntax | Meaning |
114+
+======================================+===================================================================================+
115+
| *jsonpath1* ``.`` *jsonpath2* | All nodes matched by *jsonpath2* starting at any node matching *jsonpath1* |
116+
+--------------------------------------+-----------------------------------------------------------------------------------+
117+
| *jsonpath* ``[`` *whatever* ``]`` | Same as *jsonpath*\ ``.``\ *whatever* |
118+
+--------------------------------------+-----------------------------------------------------------------------------------+
119+
| *jsonpath1* ``..`` *jsonpath2* | All nodes matched by *jsonpath2* that descend from any node matching *jsonpath1* |
120+
+--------------------------------------+-----------------------------------------------------------------------------------+
121+
| *jsonpath1* ``where`` *jsonpath2* | Any nodes matching *jsonpath1* with a child matching *jsonpath2* |
122+
+--------------------------------------+-----------------------------------------------------------------------------------+
123+
| *jsonpath1* ``wherenot`` *jsonpath2* | Any nodes matching *jsonpath1* with a child not matching *jsonpath2* |
124+
+--------------------------------------+-----------------------------------------------------------------------------------+
125+
| *jsonpath1* ``|`` *jsonpath2* | Any nodes matching the union of *jsonpath1* and *jsonpath2* |
126+
+--------------------------------------+-----------------------------------------------------------------------------------+
125127

126128
Field specifiers ( *field* ):
127129

jsonpath_ng/jsonpath.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,36 @@ def __str__(self):
330330
def __eq__(self, other):
331331
return isinstance(other, Where) and other.left == self.left and other.right == self.right
332332

333+
334+
class WhereNot(Where):
335+
"""
336+
Identical to ``Where``, but filters for only those nodes that
337+
do *not* have a match on the right.
338+
339+
>>> jsonpath = WhereNot(Fields('spam'), Fields('spam'))
340+
>>> jsonpath.find({"spam": {"spam": 1}})
341+
[]
342+
>>> matches = jsonpath.find({"spam": 1})
343+
>>> matches[0].value
344+
1
345+
346+
"""
347+
def find(self, data):
348+
return [subdata for subdata in self.left.find(data)
349+
if not self.right.find(subdata)]
350+
351+
def __str__(self):
352+
return '%s wherenot %s' % (self.left, self.right)
353+
354+
def __eq__(self, other):
355+
return (isinstance(other, WhereNot)
356+
and other.left == self.left
357+
and other.right == self.right)
358+
359+
def __hash__(self):
360+
return hash(str(self))
361+
362+
333363
class Descendants(JSONPath):
334364
"""
335365
JSONPath that matches first the left expression then any descendant

jsonpath_ng/lexer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ def tokenize(self, string):
5050

5151
literals = ['*', '.', '[', ']', '(', ')', '$', ',', ':', '|', '&', '~']
5252

53-
reserved_words = { 'where': 'WHERE' }
53+
reserved_words = {
54+
'where': 'WHERE',
55+
'wherenot': 'WHERENOT',
56+
}
5457

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

jsonpath_ng/parser.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def parse_token_stream(self, token_iterator, start_symbol='jsonpath'):
6363
('left', '|'),
6464
('left', '&'),
6565
('left', 'WHERE'),
66+
('left', 'WHERENOT'),
6667
]
6768

6869
def p_error(self, t):
@@ -72,6 +73,7 @@ def p_jsonpath_binop(self, p):
7273
"""jsonpath : jsonpath '.' jsonpath
7374
| jsonpath DOUBLEDOT jsonpath
7475
| jsonpath WHERE jsonpath
76+
| jsonpath WHERENOT jsonpath
7577
| jsonpath '|' jsonpath
7678
| jsonpath '&' jsonpath"""
7779
op = p[2]
@@ -82,6 +84,8 @@ def p_jsonpath_binop(self, p):
8284
p[0] = Descendants(p[1], p[3])
8385
elif op == 'where':
8486
p[0] = Where(p[1], p[3])
87+
elif op == 'wherenot':
88+
p[0] = WhereNot(p[1], p[3])
8589
elif op == '|':
8690
p[0] = Union(p[1], p[3])
8791
elif op == '&':

tests/test_jsonpath.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,13 @@ def test_update_descendants_where(self):
335335
{'foo': {'bar': 3, 'flag': 1}, 'baz': {'bar': 2}})
336336
])
337337

338+
def test_update_descendants_wherenot(self):
339+
self.check_update_cases([
340+
({'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 2}},
341+
'(* wherenot flag) .. bar', 4,
342+
{'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 4}})
343+
])
344+
338345
def test_update_descendants(self):
339346
self.check_update_cases([
340347
({'somefield': 1}, '$..somefield', 42, {'somefield': 42}),

tests/test_lexer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def test_simple_inputs(self):
5353
self.assert_lex_equiv('`this`', [self.token('this', 'NAMED_OPERATOR')])
5454
self.assert_lex_equiv('|', [self.token('|', '|')])
5555
self.assert_lex_equiv('where', [self.token('where', 'WHERE')])
56+
self.assert_lex_equiv('wherenot', [self.token('wherenot', 'WHERENOT')])
5657

5758
def test_basic_errors(self):
5859
def tokenize(s):

tests/test_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def test_atomic(string, parsed):
5454
('foo.baz', Child(Fields('foo'), Fields('baz'))),
5555
('foo.baz,bizzle', Child(Fields('foo'), Fields('baz', 'bizzle'))),
5656
('foo where baz', Where(Fields('foo'), Fields('baz'))),
57+
('foo wherenot baz', WhereNot(Fields('foo'), Fields('baz'))),
5758
('foo..baz', Descendants(Fields('foo'), Fields('baz'))),
5859
('foo..baz.bing', Descendants(Fields('foo'), Child(Fields('baz'), Fields('bing'))))
5960
])

0 commit comments

Comments
 (0)