diff --git a/README.rst b/README.rst index e87eb35..9ffa19e 100644 --- a/README.rst +++ b/README.rst @@ -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* ): diff --git a/jsonpath_ng/jsonpath.py b/jsonpath_ng/jsonpath.py index e6e287c..c2e392b 100644 --- a/jsonpath_ng/jsonpath.py +++ b/jsonpath_ng/jsonpath.py @@ -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 diff --git a/jsonpath_ng/lexer.py b/jsonpath_ng/lexer.py index b2ad5ee..bc86488 100644 --- a/jsonpath_ng/lexer.py +++ b/jsonpath_ng/lexer.py @@ -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()) diff --git a/jsonpath_ng/parser.py b/jsonpath_ng/parser.py index 3c6f37b..0a7aa2a 100644 --- a/jsonpath_ng/parser.py +++ b/jsonpath_ng/parser.py @@ -69,6 +69,7 @@ def parse_token_stream(self, token_iterator): ('left', '|'), ('left', '&'), ('left', 'WHERE'), + ('left', 'WHERENOT'), ] def p_error(self, t): @@ -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] @@ -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 == '&': diff --git a/tests/test_jsonpath.py b/tests/test_jsonpath.py index 20d4f11..e215304 100644 --- a/tests/test_jsonpath.py +++ b/tests/test_jsonpath.py @@ -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 # ------- # diff --git a/tests/test_lexer.py b/tests/test_lexer.py index 85533f5..00a6a17 100644 --- a/tests/test_lexer.py +++ b/tests/test_lexer.py @@ -24,6 +24,7 @@ ("`this`", (("this", "NAMED_OPERATOR"),)), ("|", (("|", "|"),)), ("where", (("where", "WHERE"),)), + ("wherenot", (("wherenot", "WHERENOT"),)), ) diff --git a/tests/test_parser.py b/tests/test_parser.py index c54b489..b247270 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -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")))), )