Skip to content

Commit c7a10af

Browse files
committed
Fix canonical paths, compound paths and list literals
1 parent 33fe76d commit c7a10af

File tree

8 files changed

+52
-57
lines changed

8 files changed

+52
-57
lines changed

jsonpath/env.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ def compile(self, path: str) -> Union[JSONPath, CompoundJSONPath]: # noqa: A003
185185
env=self, segments=self.parser.parse(stream), pseudo_root=pseudo_root
186186
)
187187

188+
# TODO: Optionally raise for trailing whitespace
189+
stream.skip_whitespace()
190+
188191
# TODO: better!
189192
if stream.current().kind != TOKEN_EOF:
190193
_path = CompoundJSONPath(env=self, path=_path)
@@ -198,6 +201,7 @@ def compile(self, path: str) -> Union[JSONPath, CompoundJSONPath]: # noqa: A003
198201

199202
if stream.current().kind == TOKEN_UNION:
200203
stream.next()
204+
stream.skip_whitespace()
201205
pseudo_root = stream.current().kind == TOKEN_PSEUDO_ROOT
202206
_path = _path.union(
203207
JSONPath(
@@ -208,6 +212,7 @@ def compile(self, path: str) -> Union[JSONPath, CompoundJSONPath]: # noqa: A003
208212
)
209213
elif stream.current().kind == TOKEN_INTERSECTION:
210214
stream.next()
215+
stream.skip_whitespace()
211216
pseudo_root = stream.current().kind == TOKEN_PSEUDO_ROOT
212217
_path = _path.intersection(
213218
JSONPath(

jsonpath/parse.py

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ def __init__(self, *, env: JSONPathEnvironment) -> None:
241241
TOKEN_FUNCTION: self.parse_function_extension,
242242
TOKEN_INT: self.parse_integer_literal,
243243
TOKEN_KEY: self.parse_current_key,
244+
TOKEN_LBRACKET: self.parse_list_literal,
244245
TOKEN_LPAREN: self.parse_grouped_expression,
245246
TOKEN_MISSING: self.parse_undefined,
246247
TOKEN_NIL: self.parse_nil,
@@ -293,9 +294,6 @@ def parse(self, stream: TokenStream) -> Iterator[JSONPathSegment]:
293294
if stream.current().kind in {TOKEN_ROOT, TOKEN_PSEUDO_ROOT}:
294295
stream.next()
295296

296-
# TODO: Support "bare" paths. Those without a leading dot for shorthand
297-
# selectors
298-
299297
yield from self.parse_path(stream)
300298

301299
if stream.current().kind not in (TOKEN_EOF, TOKEN_INTERSECTION, TOKEN_UNION):
@@ -312,18 +310,18 @@ def parse_path(self, stream: TokenStream) -> Iterable[JSONPathSegment]:
312310
"""
313311
while True:
314312
stream.skip_whitespace()
315-
if stream.current().kind == TOKEN_DOT:
316-
# Consume the dot.
317-
stream.next()
313+
_token = stream.current()
314+
if _token.kind == TOKEN_DOT:
315+
stream.eat(TOKEN_DOT)
318316
# Assert that dot is followed by shorthand selector without whitespace.
319317
stream.expect(TOKEN_NAME, TOKEN_WILD, TOKEN_KEYS)
320318
token = stream.current()
321319
selectors = self.parse_selectors(stream)
322320
yield JSONPathChildSegment(
323321
env=self.env, token=token, selectors=selectors
324322
)
325-
elif stream.current().kind == TOKEN_DDOT:
326-
token = stream.next()
323+
elif _token.kind == TOKEN_DDOT:
324+
token = stream.eat(TOKEN_DDOT)
327325
selectors = self.parse_selectors(stream)
328326
if not selectors:
329327
raise JSONPathSyntaxError(
@@ -333,7 +331,14 @@ def parse_path(self, stream: TokenStream) -> Iterable[JSONPathSegment]:
333331
yield JSONPathRecursiveDescentSegment(
334332
env=self.env, token=token, selectors=selectors
335333
)
336-
elif stream.current().kind == TOKEN_LBRACKET:
334+
elif _token.kind == TOKEN_LBRACKET:
335+
selectors = self.parse_selectors(stream)
336+
yield JSONPathChildSegment(
337+
env=self.env, token=_token, selectors=selectors
338+
)
339+
elif _token.kind in {TOKEN_NAME, TOKEN_WILD, TOKEN_KEYS}:
340+
# A non-standard "bare" path. One without a leading identifier (`$`,
341+
# `@`, `^` or `_`).
337342
token = stream.current()
338343
selectors = self.parse_selectors(stream)
339344
yield JSONPathChildSegment(
@@ -377,6 +382,7 @@ def parse_selectors(self, stream: TokenStream) -> tuple[JSONPathSelector, ...]:
377382
stream.pos -= 1
378383
return tuple(self.parse_bracketed_selection(stream))
379384

385+
stream.pos -= 1
380386
return ()
381387

382388
def parse_bracketed_selection(self, stream: TokenStream) -> List[JSONPathSelector]: # noqa: PLR0912
@@ -446,15 +452,14 @@ def parse_bracketed_selection(self, stream: TokenStream) -> List[JSONPathSelecto
446452
token=token,
447453
)
448454

449-
# XXX:
450-
# if stream.peek().kind == TOKEN_EOF:
451-
# raise JSONPathSyntaxError(
452-
# "unexpected end of segment",
453-
# token=stream.current(),
454-
# )
455-
456455
stream.skip_whitespace()
457456

457+
if stream.current().kind == TOKEN_EOF:
458+
raise JSONPathSyntaxError(
459+
"unexpected end of segment",
460+
token=stream.current(),
461+
)
462+
458463
if stream.current().kind != TOKEN_RBRACKET:
459464
stream.eat(TOKEN_COMMA)
460465
stream.skip_whitespace()
@@ -665,7 +670,12 @@ def parse_list_literal(self, stream: TokenStream) -> FilterExpression:
665670
stream.eat(TOKEN_LBRACKET)
666671
list_items: List[FilterExpression] = []
667672

668-
while stream.current().kind != TOKEN_RBRACKET:
673+
while True:
674+
stream.skip_whitespace()
675+
676+
if stream.current().kind == TOKEN_RBRACKET:
677+
break
678+
669679
try:
670680
list_items.append(self.list_item_map[stream.current().kind](stream))
671681
except KeyError as err:
@@ -674,11 +684,10 @@ def parse_list_literal(self, stream: TokenStream) -> FilterExpression:
674684
token=stream.current(),
675685
) from err
676686

677-
if stream.peek().kind != TOKEN_RBRACKET:
678-
stream.expect_peek(TOKEN_COMMA)
679-
stream.next()
680-
681-
stream.next()
687+
stream.skip_whitespace()
688+
if stream.current().kind != TOKEN_RBRACKET:
689+
stream.eat(TOKEN_COMMA)
690+
stream.skip_whitespace()
682691

683692
stream.eat(TOKEN_RBRACKET)
684693
return ListLiteral(list_items)

jsonpath/selectors.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,7 @@ def __init__(
7171
self.shorthand = shorthand
7272

7373
def __str__(self) -> str:
74-
return (
75-
f"[{canonical_string(self.name)}]"
76-
if self.shorthand
77-
else f"{canonical_string(self.name)}"
78-
)
74+
return canonical_string(self.name)
7975

8076
def __eq__(self, __value: object) -> bool:
8177
return (
@@ -203,11 +199,7 @@ def __init__(
203199
self.shorthand = shorthand
204200

205201
def __str__(self) -> str:
206-
return (
207-
f"[{self.env.keys_selector_token}]"
208-
if self.shorthand
209-
else self.env.keys_selector_token
210-
)
202+
return self.env.keys_selector_token
211203

212204
def __eq__(self, __value: object) -> bool:
213205
return isinstance(__value, KeysSelector) and self.token == __value.token
@@ -315,7 +307,7 @@ def __init__(
315307
self.shorthand = shorthand
316308

317309
def __str__(self) -> str:
318-
return "[*]" if self.shorthand else "*"
310+
return "*"
319311

320312
def __eq__(self, __value: object) -> bool:
321313
return isinstance(__value, WildSelector) and self.token == __value.token

jsonpath/stream.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class TokenStream:
1616
def __init__(self, token_iter: Iterable[Token]):
1717
self.tokens = list(token_iter)
1818
self.pos = 0
19-
self.eof = Token(TOKEN_EOF, "", -1, self.tokens[0].path)
19+
path = self.tokens[0].path if self.tokens else ""
20+
self.eof = Token(TOKEN_EOF, "", -1, path)
2021

2122
def __str__(self) -> str: # pragma: no cover
2223
return f"current: {self.current}\nnext: {self.peek}"

tests/test_env.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""JSONPathEnvironment API test cases."""
2+
23
import asyncio
34
from typing import List
45

@@ -178,7 +179,7 @@ def test_custom_fake_root_identifier_token() -> None:
178179
"""Test that we can change the non-standard fake root identifier."""
179180

180181
class MyJSONPathEnvironment(JSONPathEnvironment):
181-
fake_root_token = "$$"
182+
pseudo_root_token = "$$"
182183

183184
env = MyJSONPathEnvironment()
184185
data = {"foo": {"a": 1, "b": 2, "c": 3}}
@@ -191,7 +192,7 @@ def test_disable_fake_root_identifier() -> None:
191192
"""Test that we can disable the non-standard fake root identifier."""
192193

193194
class MyJSONPathEnvironment(JSONPathEnvironment):
194-
fake_root_token = ""
195+
pseudo_root_token = ""
195196

196197
env = MyJSONPathEnvironment()
197198
with pytest.raises(JSONPathSyntaxError):

tests/test_errors.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def env() -> JSONPathEnvironment:
1515

1616

1717
def test_unclosed_selection_list(env: JSONPathEnvironment) -> None:
18-
with pytest.raises(JSONPathSyntaxError, match=r"unexpected end of selector list"):
18+
with pytest.raises(JSONPathSyntaxError, match=r"unexpected end of segment"):
1919
env.compile("$[1,2")
2020

2121

@@ -39,6 +39,11 @@ def test_unbalanced_parens(env: JSONPathEnvironment) -> None:
3939
env.compile("$[?((@.foo)]")
4040

4141

42+
def test_root_dot(env: JSONPathEnvironment) -> None:
43+
with pytest.raises(JSONPathSyntaxError):
44+
env.compile("$.")
45+
46+
4247
class FilterLiteralTestCase(NamedTuple):
4348
description: str
4449
query: str

tests/test_filter_expression_caching.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_root_path_cache() -> None:
5050
env = JSONPathEnvironment(filter_caching=True)
5151
data = {"some": [{"a": 1}, {"a": 99}, {"a": 2}, {"a": 3}]}
5252
with mock.patch(
53-
"jsonpath.filter.RootPath.evaluate", return_value=10
53+
"jsonpath.filter.RootFilterQuery.evaluate", return_value=10
5454
) as mock_root_path:
5555
path = env.compile("$.some[?@.a < $.thing].a")
5656
rv = path.findall(data)
@@ -63,7 +63,7 @@ def test_root_path_no_cache() -> None:
6363
env = JSONPathEnvironment(filter_caching=False)
6464
data = {"some": [{"a": 1}, {"a": 99}, {"a": 2}, {"a": 3}]}
6565
with mock.patch(
66-
"jsonpath.filter.RootPath.evaluate", return_value=10
66+
"jsonpath.filter.RootFilterQuery.evaluate", return_value=10
6767
) as mock_root_path:
6868
path = env.compile("$.some[?@.a < $.thing].a")
6969
rv = path.findall(data)

tests/test_parse.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@ class Case:
1616
TEST_CASES = [
1717
Case(description="empty", path="", want="$"),
1818
Case(description="just root", path="$", want="$"),
19-
Case(description="root dot", path="$.", want="$"),
2019
Case(description="implicit root dot property", path=".thing", want="$['thing']"),
2120
Case(description="root dot property", path="$.thing", want="$['thing']"),
22-
Case(description="root bracket property", path="$[thing]", want="$['thing']"),
2321
Case(
2422
description="root double quoted property", path='$["thing"]', want="$['thing']"
2523
),
@@ -31,40 +29,24 @@ class Case:
3129
path="$['anything{!%']",
3230
want="$['anything{!%']",
3331
),
34-
Case(description="root dot bracket property", path="$.[thing]", want="$['thing']"),
3532
Case(description="root bracket index", path="$[1]", want="$[1]"),
3633
Case(description="root slice", path="$[1:-1]", want="$[1:-1:1]"),
37-
Case(description="root dot slice", path="$.[1:-1]", want="$[1:-1:1]"),
3834
Case(description="root slice with step", path="$[1:-1:2]", want="$[1:-1:2]"),
3935
Case(description="root slice with empty start", path="$[:-1]", want="$[:-1:1]"),
4036
Case(description="root slice with empty stop", path="$[1:]", want="$[1::1]"),
4137
Case(description="root dot wild", path="$.*", want="$[*]"),
4238
Case(description="root bracket wild", path="$[*]", want="$[*]"),
43-
Case(description="root dot bracket wild", path="$.[*]", want="$[*]"),
44-
Case(description="root descend", path="$..", want="$.."),
45-
Case(description="root dot descend", path="$...", want="$.."),
4639
Case(description="root selector list", path="$[1,2]", want="$[1, 2]"),
47-
Case(description="root dot selector list", path="$.[1,2]", want="$[1, 2]"),
4840
Case(
4941
description="root selector list with slice",
5042
path="$[1,5:-1:1]",
5143
want="$[1, 5:-1:1]",
5244
),
53-
Case(
54-
description="root selector list with properties",
55-
path="$[some,thing]",
56-
want="$['some', 'thing']",
57-
),
5845
Case(
5946
description="root selector list with quoted properties",
6047
path="$[\"some\",'thing']",
6148
want="$['some', 'thing']",
6249
),
63-
Case(
64-
description="implicit root selector list with mixed selectors",
65-
path='$["some",thing, 1, 2:-2:2]',
66-
want="$['some', 'thing', 1, 2:-2:2]",
67-
),
6850
Case(
6951
description="filter self dot property",
7052
path="[?(@.thing)]",

0 commit comments

Comments
 (0)