Skip to content

Commit 93bbe2b

Browse files
committed
Support filters without parentheses.
1 parent 573523c commit 93bbe2b

File tree

8 files changed

+38
-33
lines changed

8 files changed

+38
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Added the built-in `length()` function.
99
- Added the built-in `count()` function. `count()` is an alias for `length()`
1010
- Added the built-in `keys()` function.
11+
- Support filters without parentheses.
1112

1213
## Version 0.2.0
1314

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,16 @@ $...title
227227

228228
### Filters (`[?(EXPRESSION)]`)
229229

230-
Filters allow you to remove nodes from a selection using a Boolean expression. Within a filter, `@` refers to the current node and `$` refers to the root node in the target document. `@` and `$` can be used to select nodes as part of the expression.
230+
Filters allow you to remove nodes from a selection using a Boolean expression. Within a filter, `@` refers to the current node and `$` refers to the root node in the target document. `@` and `$` can be used to select nodes as part of the expression. Since version 0.3.0, the parentheses are optional, as per the IETF JSONPath draft. These two examples are equivalent.
231231

232232
```text
233233
$..products.*[?(@.price < $.price_cap)]
234234
```
235235

236+
```text
237+
$..products.*[[email protected] < $.price_cap]
238+
```
239+
236240
Comparison operators include `==`, `!=`, `<`, `>`, `<=` and `>=`. Plus `<>` as an alias for `!=`.
237241

238242
`in` and `contains` are membership operators. `left in right` is equivalent to `right contains left`.
@@ -286,8 +290,7 @@ And this is a list of areas where we deviate from the [IETF JSONPath draft](http
286290
- The root token (default `$`) is optional.
287291
- Paths starting with a dot (`.`) are OK. `.thing` is the same as `$.thing`, as is `thing`, `$[thing]` and `$["thing"]`.
288292
- Nested filters are not supported.
289-
- When a filter is applied to an object (mapping) value, we do not silently apply that filter to the object's values. See the "Existence of non-singular queries" example in the IETF JSONPath draft.
290-
- Parentheses are required when writing filter selectors, as is common in existing JSONPath implementations. `$.some[?(@.thing)]` is OK, `$.some[[email protected]]` is not.
293+
- We don't treat filter expressions without a comparison as existence test, but an "is truthy" test. See the "Existence of non-singular queries" example in the IETF JSONPath draft.
291294

292295
And this is a list of features that are uncommon or unique to Python JSONPath.
293296

jsonpath/lex.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
from .token import TOKEN_INTERSECTION
3434
from .token import TOKEN_LE
3535
from .token import TOKEN_LG
36-
from .token import TOKEN_LIST_END
3736
from .token import TOKEN_LIST_SLICE
3837
from .token import TOKEN_LIST_START
3938
from .token import TOKEN_LPAREN
@@ -46,6 +45,7 @@
4645
from .token import TOKEN_NULL
4746
from .token import TOKEN_OR
4847
from .token import TOKEN_PROPERTY
48+
from .token import TOKEN_RBRACKET
4949
from .token import TOKEN_RE
5050
from .token import TOKEN_RE_FLAGS
5151
from .token import TOKEN_RE_PATTERN
@@ -137,7 +137,7 @@ def compile_rules(self) -> Pattern[str]:
137137
(TOKEN_SLICE, self.slice_pattern),
138138
(TOKEN_WILD, self.wild_pattern),
139139
(TOKEN_LIST_SLICE, self.slice_list_pattern),
140-
(TOKEN_FILTER_START, r"\[\s*\?\s*\("),
140+
(TOKEN_FILTER_START, r"\[\s*\?\s*\(?"),
141141
(TOKEN_FILTER_END, r"\)\s*]"),
142142
(TOKEN_FUNCTION, self.function_pattern),
143143
(TOKEN_BRACKET_PROPERTY, self.bracketed_property_pattern),
@@ -162,7 +162,7 @@ def compile_rules(self) -> Pattern[str]:
162162
(TOKEN_UNDEFINED, r"undefined"),
163163
(TOKEN_MISSING, r"missing"),
164164
(TOKEN_LIST_START, r"\["),
165-
(TOKEN_LIST_END, r"]"),
165+
(TOKEN_RBRACKET, r"]"),
166166
(TOKEN_COMMA, r","),
167167
(TOKEN_EQ, r"=="),
168168
(TOKEN_NE, r"!="),

jsonpath/parse.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
from .token import TOKEN_INTERSECTION
5959
from .token import TOKEN_LE
6060
from .token import TOKEN_LG
61-
from .token import TOKEN_LIST_END
6261
from .token import TOKEN_LIST_START
6362
from .token import TOKEN_LPAREN
6463
from .token import TOKEN_LT
@@ -70,6 +69,7 @@
7069
from .token import TOKEN_NULL
7170
from .token import TOKEN_OR
7271
from .token import TOKEN_PROPERTY
72+
from .token import TOKEN_RBRACKET
7373
from .token import TOKEN_RE
7474
from .token import TOKEN_RE_FLAGS
7575
from .token import TOKEN_RE_PATTERN
@@ -288,7 +288,7 @@ def parse_selector_list(self, stream: TokenStream) -> ListSelector:
288288
tok = stream.next_token()
289289
list_items: List[Union[IndexSelector, PropertySelector, SliceSelector]] = []
290290

291-
while stream.current.kind != TOKEN_LIST_END:
291+
while stream.current.kind != TOKEN_RBRACKET:
292292
if stream.current.kind == TOKEN_INT:
293293
list_items.append(
294294
IndexSelector(
@@ -313,7 +313,7 @@ def parse_selector_list(self, stream: TokenStream) -> ListSelector:
313313
token=stream.current,
314314
)
315315

316-
if stream.peek.kind != TOKEN_LIST_END:
316+
if stream.peek.kind != TOKEN_RBRACKET:
317317
stream.next_token()
318318

319319
stream.next_token()
@@ -329,7 +329,7 @@ def parse_filter(self, stream: TokenStream) -> Filter:
329329
raise JSONPathSyntaxError("unbalanced ')'", token=stream.current)
330330

331331
stream.next_token()
332-
stream.expect(TOKEN_FILTER_END)
332+
stream.expect(TOKEN_FILTER_END, TOKEN_RBRACKET)
333333
return Filter(env=self.env, token=tok, expression=expr)
334334

335335
def parse_boolean(self, stream: TokenStream) -> FilterExpression:
@@ -379,13 +379,18 @@ def parse_grouped_expression(self, stream: TokenStream) -> FilterExpression:
379379
stream.next_token()
380380

381381
while stream.current.kind != TOKEN_RPAREN:
382-
if stream.current.kind in (TOKEN_EOF, TOKEN_FILTER_END):
382+
if stream.current.kind == TOKEN_EOF:
383383
raise JSONPathSyntaxError(
384384
"unbalanced parentheses", token=stream.current
385385
)
386+
if stream.current.kind == TOKEN_FILTER_END:
387+
# In some cases, an RPAREN followed by an RBRACKET can
388+
# look like the long form "end of filter" token.
389+
stream.push(stream.current)
390+
break
386391
expr = self.parse_infix_expression(stream, expr)
387392

388-
stream.expect(TOKEN_RPAREN)
393+
stream.expect(TOKEN_RPAREN, TOKEN_FILTER_END)
389394
return expr
390395

391396
def parse_root_path(self, stream: TokenStream) -> FilterExpression:
@@ -419,7 +424,7 @@ def parse_list_literal(self, stream: TokenStream) -> FilterExpression:
419424
stream.next_token()
420425
list_items: List[FilterExpression] = []
421426

422-
while stream.current.kind != TOKEN_LIST_END:
427+
while stream.current.kind != TOKEN_RBRACKET:
423428
try:
424429
list_items.append(self.list_item_map[stream.current.kind](stream))
425430
except KeyError as err:
@@ -428,7 +433,7 @@ def parse_list_literal(self, stream: TokenStream) -> FilterExpression:
428433
token=stream.current,
429434
) from err
430435

431-
if stream.peek.kind != TOKEN_LIST_END:
436+
if stream.peek.kind != TOKEN_RBRACKET:
432437
stream.expect_peek(TOKEN_COMMA)
433438
stream.next_token()
434439

@@ -466,7 +471,7 @@ def parse_filter_selector(
466471
try:
467472
left = self.token_map[stream.current.kind](stream)
468473
except KeyError as err:
469-
if stream.current.kind in (TOKEN_EOF, TOKEN_FILTER_END):
474+
if stream.current.kind in (TOKEN_EOF, TOKEN_FILTER_END, TOKEN_RBRACKET):
470475
msg = "end of expression"
471476
else:
472477
msg = repr(stream.current.value)
@@ -477,7 +482,7 @@ def parse_filter_selector(
477482
while True:
478483
peek_kind = stream.peek.kind
479484
if (
480-
peek_kind in (TOKEN_EOF, TOKEN_FILTER_END)
485+
peek_kind in (TOKEN_EOF, TOKEN_FILTER_END, TOKEN_RBRACKET)
481486
or self.PRECEDENCES.get(peek_kind, self.PRECEDENCE_LOWEST) < precedence
482487
):
483488
break

jsonpath/stream.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,17 @@ def close(self) -> None:
7474
"""Close the stream."""
7575
self.current = Token(TOKEN_EOF, "", -1, "")
7676

77-
def expect(self, typ: str) -> None:
77+
def expect(self, *typ: str) -> None:
7878
""""""
79-
if self.current.kind != typ:
79+
if self.current.kind not in typ:
8080
raise JSONPathSyntaxError(
8181
f"expected {typ!r}, found {self.current.kind!r}",
8282
token=self.current,
8383
)
8484

85-
def expect_peek(self, typ: str) -> None:
85+
def expect_peek(self, *typ: str) -> None:
8686
""""""
87-
if self.peek.kind != typ:
87+
if self.peek.kind not in typ:
8888
raise JSONPathSyntaxError(
8989
f"expected {typ!r}, found {self.peek.kind!r}",
9090
token=self.peek,

jsonpath/token.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
TOKEN_FILTER_START = sys.intern("FILTER_START")
2020
TOKEN_IDENT = sys.intern("IDENT")
2121
TOKEN_INDEX = sys.intern("IDX")
22-
TOKEN_LIST_END = sys.intern("RBRACKET")
22+
TOKEN_RBRACKET = sys.intern("RBRACKET")
2323
TOKEN_BARE_PROPERTY = sys.intern("BARE_PROPERTY")
2424
TOKEN_LIST_SLICE = sys.intern("LSLICE")
2525
TOKEN_LIST_START = sys.intern("LBRACKET")

tests/compliance.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ def cases() -> List[Case]:
4949

5050
def valid_cases() -> List[Case]:
5151
def mangle_filter(case: Case) -> Case:
52-
# XXX: Insert parentheses around filter expression :(
53-
if case.name.startswith("filter") and case.selector.count("]") == 1:
54-
case.selector = case.selector.replace("[?", "[?(").replace("]", ")]")
55-
5652
# XXX: Insert wildcard in front of root :(
5753
if (
5854
case.name.startswith("filter")

tests/test_lex.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
from jsonpath.token import TOKEN_INDEX
2222
from jsonpath.token import TOKEN_INT
2323
from jsonpath.token import TOKEN_INTERSECTION
24-
from jsonpath.token import TOKEN_LIST_END
2524
from jsonpath.token import TOKEN_LIST_START
2625
from jsonpath.token import TOKEN_LT
2726
from jsonpath.token import TOKEN_NIL
2827
from jsonpath.token import TOKEN_NOT
2928
from jsonpath.token import TOKEN_OR
3029
from jsonpath.token import TOKEN_PROPERTY
30+
from jsonpath.token import TOKEN_RBRACKET
3131
from jsonpath.token import TOKEN_RE
3232
from jsonpath.token import TOKEN_RE_FLAGS
3333
from jsonpath.token import TOKEN_RE_PATTERN
@@ -84,7 +84,7 @@ class Case:
8484
Token(kind=TOKEN_ROOT, value="$", index=0, path='$["some"]'),
8585
Token(kind=TOKEN_LIST_START, value="[", index=1, path='$["some"]'),
8686
Token(kind=TOKEN_STRING, value="some", index=3, path='$["some"]'),
87-
Token(kind=TOKEN_LIST_END, value="]", index=8, path='$["some"]'),
87+
Token(kind=TOKEN_RBRACKET, value="]", index=8, path='$["some"]'),
8888
],
8989
),
9090
Case(
@@ -94,7 +94,7 @@ class Case:
9494
Token(kind=TOKEN_ROOT, value="$", index=0, path="$['some']"),
9595
Token(kind=TOKEN_LIST_START, value="[", index=1, path="$['some']"),
9696
Token(kind=TOKEN_STRING, value="some", index=3, path="$['some']"),
97-
Token(kind=TOKEN_LIST_END, value="]", index=8, path="$['some']"),
97+
Token(kind=TOKEN_RBRACKET, value="]", index=8, path="$['some']"),
9898
],
9999
),
100100
Case(
@@ -245,7 +245,7 @@ class Case:
245245
Token(kind=TOKEN_INT, value="4", index=4, path="$[1,4,5]"),
246246
Token(kind=TOKEN_COMMA, value=",", index=5, path="$[1,4,5]"),
247247
Token(kind=TOKEN_INT, value="5", index=6, path="$[1,4,5]"),
248-
Token(kind=TOKEN_LIST_END, value="]", index=7, path="$[1,4,5]"),
248+
Token(kind=TOKEN_RBRACKET, value="]", index=7, path="$[1,4,5]"),
249249
],
250250
),
251251
Case(
@@ -259,7 +259,7 @@ class Case:
259259
Token(kind=TOKEN_SLICE_START, value="4", index=4, path="$[1,4:9]"),
260260
Token(kind=TOKEN_SLICE_STOP, value="9", index=6, path="$[1,4:9]"),
261261
Token(kind=TOKEN_SLICE_STEP, value="", index=-1, path="$[1,4:9]"),
262-
Token(kind=TOKEN_LIST_END, value="]", index=7, path="$[1,4:9]"),
262+
Token(kind=TOKEN_RBRACKET, value="]", index=7, path="$[1,4:9]"),
263263
],
264264
),
265265
Case(
@@ -275,7 +275,7 @@ class Case:
275275
Token(
276276
kind=TOKEN_BARE_PROPERTY, value="thing", index=7, path="$[some,thing]"
277277
),
278-
Token(kind=TOKEN_LIST_END, value="]", index=12, path="$[some,thing]"),
278+
Token(kind=TOKEN_RBRACKET, value="]", index=12, path="$[some,thing]"),
279279
],
280280
),
281281
Case(
@@ -781,7 +781,7 @@ class Case:
781781
kind=TOKEN_STRING, value="1", index=19, path="[?(@.thing in [1, '1'])]"
782782
),
783783
Token(
784-
kind=TOKEN_LIST_END,
784+
kind=TOKEN_RBRACKET,
785785
value="]",
786786
index=21,
787787
path="[?(@.thing in [1, '1'])]",
@@ -1039,7 +1039,7 @@ class Case:
10391039
Token(
10401040
kind=TOKEN_STRING, value="thing", index=11, path="$['some', 'thing']"
10411041
),
1042-
Token(kind=TOKEN_LIST_END, value="]", index=17, path="$['some', 'thing']"),
1042+
Token(kind=TOKEN_RBRACKET, value="]", index=17, path="$['some', 'thing']"),
10431043
],
10441044
),
10451045
Case(

0 commit comments

Comments
 (0)