Skip to content

Commit

Permalink
Add wherenot
Browse files Browse the repository at this point in the history
  • Loading branch information
kaapstorm committed Feb 5, 2021
1 parent de4df60 commit 5de5076
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 14 deletions.
28 changes: 15 additions & 13 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,21 @@ Atomic expressions:

Jsonpath operators:

+-------------------------------------+------------------------------------------------------------------------------------+
| Syntax | Meaning |
+=====================================+====================================================================================+
| *jsonpath1* ``.`` *jsonpath2* | All nodes matched by *jsonpath2* starting at any node matching *jsonpath1* |
+-------------------------------------+------------------------------------------------------------------------------------+
| *jsonpath* ``[`` *whatever* ``]`` | Same as *jsonpath*\ ``.``\ *whatever* |
+-------------------------------------+------------------------------------------------------------------------------------+
| *jsonpath1* ``..`` *jsonpath2* | All nodes matched by *jsonpath2* that descend from any node matching *jsonpath1* |
+-------------------------------------+------------------------------------------------------------------------------------+
| *jsonpath1* ``where`` *jsonpath2* | Any nodes matching *jsonpath1* with a child matching *jsonpath2* |
+-------------------------------------+------------------------------------------------------------------------------------+
| *jsonpath1* ``|`` *jsonpath2* | Any nodes matching the union of *jsonpath1* and *jsonpath2* |
+-------------------------------------+------------------------------------------------------------------------------------+
+--------------------------------------+-----------------------------------------------------------------------------------+
| Syntax | Meaning |
+======================================+===================================================================================+
| *jsonpath1* ``.`` *jsonpath2* | All nodes matched by *jsonpath2* starting at any node matching *jsonpath1* |
+--------------------------------------+-----------------------------------------------------------------------------------+
| *jsonpath* ``[`` *whatever* ``]`` | Same as *jsonpath*\ ``.``\ *whatever* |
+--------------------------------------+-----------------------------------------------------------------------------------+
| *jsonpath1* ``..`` *jsonpath2* | All nodes matched by *jsonpath2* that descend from any node matching *jsonpath1* |
+--------------------------------------+-----------------------------------------------------------------------------------+
| *jsonpath1* ``where`` *jsonpath2* | Any nodes matching *jsonpath1* with a child matching *jsonpath2* |
+--------------------------------------+-----------------------------------------------------------------------------------+
| *jsonpath1* ``wherenot`` *jsonpath2* | Any nodes matching *jsonpath1* with a child not matching *jsonpath2* |
+--------------------------------------+-----------------------------------------------------------------------------------+
| *jsonpath1* ``|`` *jsonpath2* | Any nodes matching the union of *jsonpath1* and *jsonpath2* |
+--------------------------------------+-----------------------------------------------------------------------------------+

Field specifiers ( *field* ):

Expand Down
30 changes: 30 additions & 0 deletions jsonpath_ng/jsonpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,36 @@ def __str__(self):
def __eq__(self, other):
return isinstance(other, Where) and other.left == self.left and other.right == self.right


class WhereNot(Where):
"""
Identical to ``Where``, but filters for only those nodes that
do *not* have a match on the right.
>>> jsonpath = WhereNot(Fields('spam'), Fields('spam'))
>>> jsonpath.find({"spam": {"spam": 1}})
[]
>>> matches = jsonpath.find({"spam": 1})
>>> matches[0].value
1
"""
def find(self, data):
return [subdata for subdata in self.left.find(data)
if not self.right.find(subdata)]

def __str__(self):
return '%s wherenot %s' % (self.left, self.right)

def __eq__(self, other):
return (isinstance(other, WhereNot)
and other.left == self.left
and other.right == self.right)

def __hash__(self):
return hash(str(self))


class Descendants(JSONPath):
"""
JSONPath that matches first the left expression then any descendant
Expand Down
5 changes: 4 additions & 1 deletion jsonpath_ng/lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ def tokenize(self, string):

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

reserved_words = { 'where': 'WHERE' }
reserved_words = {
'where': 'WHERE',
'wherenot': 'WHERENOT',
}

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

Expand Down
4 changes: 4 additions & 0 deletions jsonpath_ng/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def parse_token_stream(self, token_iterator, start_symbol='jsonpath'):
('left', '|'),
('left', '&'),
('left', 'WHERE'),
('left', 'WHERENOT'),
]

def p_error(self, t):
Expand All @@ -72,6 +73,7 @@ def p_jsonpath_binop(self, p):
"""jsonpath : jsonpath '.' jsonpath
| jsonpath DOUBLEDOT jsonpath
| jsonpath WHERE jsonpath
| jsonpath WHERENOT jsonpath
| jsonpath '|' jsonpath
| jsonpath '&' jsonpath"""
op = p[2]
Expand All @@ -82,6 +84,8 @@ def p_jsonpath_binop(self, p):
p[0] = Descendants(p[1], p[3])
elif op == 'where':
p[0] = Where(p[1], p[3])
elif op == 'wherenot':
p[0] = WhereNot(p[1], p[3])
elif op == '|':
p[0] = Union(p[1], p[3])
elif op == '&':
Expand Down
7 changes: 7 additions & 0 deletions tests/test_jsonpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,13 @@ def test_update_descendants_where(self):
{'foo': {'bar': 3, 'flag': 1}, 'baz': {'bar': 2}})
])

def test_update_descendants_wherenot(self):
self.check_update_cases([
({'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 2}},
'(* wherenot flag) .. bar', 4,
{'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 4}})
])

def test_update_descendants(self):
self.check_update_cases([
({'somefield': 1}, '$..somefield', 42, {'somefield': 42}),
Expand Down
1 change: 1 addition & 0 deletions tests/test_lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def test_simple_inputs(self):
self.assert_lex_equiv('`this`', [self.token('this', 'NAMED_OPERATOR')])
self.assert_lex_equiv('|', [self.token('|', '|')])
self.assert_lex_equiv('where', [self.token('where', 'WHERE')])
self.assert_lex_equiv('wherenot', [self.token('wherenot', 'WHERENOT')])

def test_basic_errors(self):
def tokenize(s):
Expand Down
1 change: 1 addition & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ def test_nested(self):
self.check_parse_cases([('foo.baz', Child(Fields('foo'), Fields('baz'))),
('foo.baz,bizzle', Child(Fields('foo'), Fields('baz', 'bizzle'))),
('foo where baz', Where(Fields('foo'), Fields('baz'))),
('foo wherenot baz', WhereNot(Fields('foo'), Fields('baz'))),
('foo..baz', Descendants(Fields('foo'), Fields('baz'))),
('foo..baz.bing', Descendants(Fields('foo'), Child(Fields('baz'), Fields('bing'))))])

0 comments on commit 5de5076

Please sign in to comment.