Skip to content

Commit 8cada95

Browse files
authored
Merge pull request #63 from jg-rp/membership
Fix membership operators and filter expression literals
2 parents c28196a + 8c9f0cf commit 8cada95

File tree

4 files changed

+64
-6
lines changed

4 files changed

+64
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
**Fixes**
66

7-
- 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).
7+
- 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).
88

99
**Features**
1010

11+
- 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).
1112
- 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.
1213
- 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.
1314

jsonpath/env.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Core JSONPath configuration object."""
2+
23
from __future__ import annotations
34

45
import re
@@ -548,9 +549,9 @@ def compare( # noqa: PLR0911
548549
return self._lt(right, left) or self._eq(left, right)
549550
if operator == "<=":
550551
return self._lt(left, right) or self._eq(left, right)
551-
if operator == "in" and isinstance(right, Sequence):
552+
if operator == "in" and isinstance(right, (Mapping, Sequence)):
552553
return left in right
553-
if operator == "contains" and isinstance(left, Sequence):
554+
if operator == "contains" and isinstance(left, (Mapping, Sequence)):
554555
return right in left
555556
if operator == "=~" and isinstance(right, re.Pattern) and isinstance(left, str):
556557
return bool(right.fullmatch(left))

jsonpath/parse.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,22 @@ class Parser:
197197
]
198198
)
199199

200+
# Infix operators that accept filter expression literals.
201+
INFIX_LITERAL_OPERATORS = frozenset(
202+
[
203+
"==",
204+
">=",
205+
">",
206+
"<=",
207+
"<",
208+
"!=",
209+
"<>",
210+
"=~",
211+
"in",
212+
"contains",
213+
]
214+
)
215+
200216
PREFIX_OPERATORS = frozenset(
201217
[
202218
TOKEN_NOT,
@@ -530,14 +546,14 @@ def parse_infix_expression(
530546
self._raise_for_non_comparable_function(left, tok)
531547
self._raise_for_non_comparable_function(right, tok)
532548

533-
if operator not in self.COMPARISON_OPERATORS:
534-
if isinstance(left, Literal):
549+
if operator not in self.INFIX_LITERAL_OPERATORS:
550+
if isinstance(left, (Literal, Nil)):
535551
raise JSONPathSyntaxError(
536552
"filter expression literals outside of "
537553
"function expressions must be compared",
538554
token=tok,
539555
)
540-
if isinstance(right, Literal):
556+
if isinstance(right, (Literal, Nil)):
541557
raise JSONPathSyntaxError(
542558
"filter expression literals outside of "
543559
"function expressions must be compared",

tests/test_find.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,46 @@ class Case:
8787
data=[{"a": True, "b": False}],
8888
want=[{"a": True, "b": False}],
8989
),
90+
Case(
91+
description="array contains literal",
92+
path="$[[email protected] contains 'foo']",
93+
data=[{"a": ["foo", "bar"]}, {"a": ["bar"]}],
94+
want=[
95+
{
96+
"a": ["foo", "bar"],
97+
}
98+
],
99+
),
100+
Case(
101+
description="object contains literal",
102+
path="$[[email protected] contains 'foo']",
103+
data=[{"a": {"foo": "bar"}}, {"a": {"bar": "baz"}}],
104+
want=[
105+
{
106+
"a": {"foo": "bar"},
107+
}
108+
],
109+
),
110+
Case(
111+
description="literal in array",
112+
path="$[?'foo' in @.a]",
113+
data=[{"a": ["foo", "bar"]}, {"a": ["bar"]}],
114+
want=[
115+
{
116+
"a": ["foo", "bar"],
117+
}
118+
],
119+
),
120+
Case(
121+
description="literal in object",
122+
path="$[?'foo' in @.a]",
123+
data=[{"a": {"foo": "bar"}}, {"a": {"bar": "baz"}}],
124+
want=[
125+
{
126+
"a": {"foo": "bar"},
127+
}
128+
],
129+
),
90130
]
91131

92132

0 commit comments

Comments
 (0)