From 23c0f8410f9ca594e13f190170ea5a1e25a1bb29 Mon Sep 17 00:00:00 2001 From: James Prior Date: Sat, 6 Jul 2024 07:29:08 +0100 Subject: [PATCH 1/2] Fix infix operators and filter expression literals --- CHANGELOG.md | 2 +- jsonpath/parse.py | 22 +++++++++++++++++++--- tests/test_find.py | 20 ++++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3819e3..7ec0433 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Fixes** -- Fixed handling of JSONPath literals in filter expressions. We now raise a `JSONPathSyntaxError` if a filter expression literal is not part of a comparison or function expression. See [jsonpath-compliance-test-suite#81](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite/pull/81). +- Fixed handling of JSONPath literals in filter expressions. We now raise a `JSONPathSyntaxError` if a filter expression literal is not part of a comparison, membership or function expression. See [jsonpath-compliance-test-suite#81](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite/pull/81). **Features** diff --git a/jsonpath/parse.py b/jsonpath/parse.py index 39ac74b..c055d56 100644 --- a/jsonpath/parse.py +++ b/jsonpath/parse.py @@ -197,6 +197,22 @@ class Parser: ] ) + # Infix operators that accept filter expression literals. + INFIX_LITERAL_OPERATORS = frozenset( + [ + "==", + ">=", + ">", + "<=", + "<", + "!=", + "<>", + "=~", + "in", + "contains", + ] + ) + PREFIX_OPERATORS = frozenset( [ TOKEN_NOT, @@ -530,14 +546,14 @@ def parse_infix_expression( self._raise_for_non_comparable_function(left, tok) self._raise_for_non_comparable_function(right, tok) - if operator not in self.COMPARISON_OPERATORS: - if isinstance(left, Literal): + if operator not in self.INFIX_LITERAL_OPERATORS: + if isinstance(left, (Literal, Nil)): raise JSONPathSyntaxError( "filter expression literals outside of " "function expressions must be compared", token=tok, ) - if isinstance(right, Literal): + if isinstance(right, (Literal, Nil)): raise JSONPathSyntaxError( "filter expression literals outside of " "function expressions must be compared", diff --git a/tests/test_find.py b/tests/test_find.py index 989104f..f3d4122 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -87,6 +87,26 @@ class Case: data=[{"a": True, "b": False}], want=[{"a": True, "b": False}], ), + Case( + description="array contains literal", + path="$[?@.a contains 'foo']", + data=[{"a": ["foo", "bar"]}, {"a": ["bar"]}], + want=[ + { + "a": ["foo", "bar"], + } + ], + ), + Case( + description="literal in array", + path="$[?'foo' in @.a]", + data=[{"a": ["foo", "bar"]}, {"a": ["bar"]}], + want=[ + { + "a": ["foo", "bar"], + } + ], + ), ] From 8c9f0cfeeece26f591ed24abe476a65c35f369c6 Mon Sep 17 00:00:00 2001 From: James Prior Date: Sat, 6 Jul 2024 07:42:25 +0100 Subject: [PATCH 2/2] Add membership of object data --- CHANGELOG.md | 1 + jsonpath/env.py | 5 +++-- tests/test_find.py | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec0433..2c851ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ **Features** +- Allow JSONPath filter expression membership operators (`contains` and `in`) to operate on object/mapping data as well as arrays/sequences. See [#55](https://github.com/jg-rp/python-jsonpath/issues/55). - Added a `select` method to the JSONPath [query iterator interface](https://jg-rp.github.io/python-jsonpath/query/), generating a projection of each JSONPath match by selecting a subset of its values. - Added the `addne` and `addap` operations to [JSONPatch](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPatch). `addne` (add if not exists) is like the standard `add` operation, but only adds object keys/values if the key does not exist. `addap` (add or append) is like the standard `add` operation, but assumes an index of `-` if the target index can not be resolved. diff --git a/jsonpath/env.py b/jsonpath/env.py index 17c1add..ef49d4b 100644 --- a/jsonpath/env.py +++ b/jsonpath/env.py @@ -1,4 +1,5 @@ """Core JSONPath configuration object.""" + from __future__ import annotations import re @@ -548,9 +549,9 @@ def compare( # noqa: PLR0911 return self._lt(right, left) or self._eq(left, right) if operator == "<=": return self._lt(left, right) or self._eq(left, right) - if operator == "in" and isinstance(right, Sequence): + if operator == "in" and isinstance(right, (Mapping, Sequence)): return left in right - if operator == "contains" and isinstance(left, Sequence): + if operator == "contains" and isinstance(left, (Mapping, Sequence)): return right in left if operator == "=~" and isinstance(right, re.Pattern) and isinstance(left, str): return bool(right.fullmatch(left)) diff --git a/tests/test_find.py b/tests/test_find.py index f3d4122..b2175e2 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -97,6 +97,16 @@ class Case: } ], ), + Case( + description="object contains literal", + path="$[?@.a contains 'foo']", + data=[{"a": {"foo": "bar"}}, {"a": {"bar": "baz"}}], + want=[ + { + "a": {"foo": "bar"}, + } + ], + ), Case( description="literal in array", path="$[?'foo' in @.a]", @@ -107,6 +117,16 @@ class Case: } ], ), + Case( + description="literal in object", + path="$[?'foo' in @.a]", + data=[{"a": {"foo": "bar"}}, {"a": {"bar": "baz"}}], + want=[ + { + "a": {"foo": "bar"}, + } + ], + ), ]