Skip to content

Commit

Permalink
Add wherenot
Browse files Browse the repository at this point in the history
  • Loading branch information
kaapstorm committed Sep 30, 2024
1 parent d5a766f commit 855df05
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 14 deletions.
28 changes: 15 additions & 13 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,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 @@ -369,6 +369,36 @@ def __eq__(self, other):
def __hash__(self):
return hash((self.left, 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((self.left, self.right))


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 @@ -48,7 +48,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 @@ -69,6 +69,7 @@ def parse_token_stream(self, token_iterator):
('left', '|'),
('left', '&'),
('left', 'WHERE'),
('left', 'WHERENOT'),
]

def p_error(self, t):
Expand All @@ -81,6 +82,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 @@ -91,6 +93,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
10 changes: 10 additions & 0 deletions tests/test_jsonpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ def test_datumincontext_in_context_nested():
{"foo": {"bar": 3, "flag": 1}, "baz": {"bar": 2}},
),
#
# WhereNot
# --------
#
(
'(* wherenot flag) .. bar',
{'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 2}},
4,
{'foo': {'bar': 1, 'flag': 1}, 'baz': {'bar': 4}},
),
#
# Lambdas
# -------
#
Expand Down
1 change: 1 addition & 0 deletions tests/test_lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
("`this`", (("this", "NAMED_OPERATOR"),)),
("|", (("|", "|"),)),
("where", (("where", "WHERE"),)),
("wherenot", (("wherenot", "WHERENOT"),)),
)


Expand Down
1 change: 1 addition & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
("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")))),
)
Expand Down

0 comments on commit 855df05

Please sign in to comment.