Skip to content

Commit 225f686

Browse files
committed
Assert that non-standard syntax fails in strict mode
1 parent 6e1f3b7 commit 225f686

13 files changed

Lines changed: 167 additions & 81 deletions

jsonpath/lex.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ def compile_strict_rules(self) -> Pattern[str]:
203203
rules = [
204204
(TOKEN_DOUBLE_QUOTE_STRING, self.double_quote_pattern),
205205
(TOKEN_SINGLE_QUOTE_STRING, self.single_quote_pattern),
206-
(TOKEN_DOT_KEY_PROPERTY, self.dot_key_pattern),
207206
(TOKEN_DOT_PROPERTY, self.dot_property_pattern),
208207
(
209208
TOKEN_FLOAT,

jsonpath/parse.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,16 @@ def __init__(self, *, env: JSONPathEnvironment) -> None:
298298

299299
def parse(self, stream: TokenStream) -> Iterator[JSONPathSegment]:
300300
"""Parse a JSONPath query from a stream of tokens."""
301+
# Leading whitespace is not allowed in strict mode.
301302
if stream.skip_whitespace() and self.env.strict:
302303
raise JSONPathSyntaxError(
303304
"unexpected leading whitespace", token=stream.current()
304305
)
305306

307+
# Trailing whitespace is not allowed in strict mode.
306308
if (
307309
self.env.strict
308-
and len(stream.tokens)
310+
and stream.tokens
309311
and stream.tokens[-1].kind == TOKEN_WHITESPACE
310312
):
311313
raise JSONPathSyntaxError(
@@ -319,6 +321,7 @@ def parse(self, stream: TokenStream) -> Iterator[JSONPathSegment]:
319321
):
320322
stream.next()
321323
elif self.env.strict:
324+
# Raises a syntax error because the current token is not TOKEN_ROOT.
322325
stream.expect(TOKEN_ROOT)
323326

324327
yield from self.parse_query(stream)
@@ -853,6 +856,9 @@ def parse_filter_expression(
853856

854857
def _decode_string_literal(self, token: Token) -> str:
855858
if self.env.strict:
859+
# For strict compliance with RC 9535, we must unescape string literals
860+
# ourself. RFC 9535 is more strict than json.loads when it comes to
861+
# parsing \uXXXX escape sequences.
856862
return unescape_string(
857863
token.value,
858864
token,

jsonpath/selectors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from typing import Union
1616

1717
from .exceptions import JSONPathIndexError
18+
from .exceptions import JSONPathSyntaxError
1819
from .exceptions import JSONPathTypeError
1920
from .match import NodeList
2021
from .serialize import canonical_string
@@ -384,6 +385,9 @@ def __init__(
384385
super().__init__(env=env, token=token)
385386
self.query = query
386387

388+
if env.strict:
389+
raise JSONPathSyntaxError("unexpected query selector", token=token)
390+
387391
def __str__(self) -> str:
388392
return str(self.query)
389393

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ theme:
77
palette:
88
- scheme: "default"
99
media: "(prefers-color-scheme: light)"
10+
primary: "blue"
1011
toggle:
1112
icon: "material/weather-sunny"
1213
name: "Switch to dark mode"
1314
- scheme: "slate"
1415
media: "(prefers-color-scheme: dark)"
1516
primary: "blue"
17+
accent: blue
1618
toggle:
1719
icon: "material/weather-night"
1820
name: "Switch to light mode"

tests/keys_selector.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,23 @@
4646
"tags": ["extra"]
4747
},
4848
{
49-
"name": "keys selector, object key",
49+
"name": "object key",
5050
"selector": "$.a[0].~",
5151
"document": { "a": [{ "b": "x", "c": "z" }, { "b": "y" }] },
5252
"result": ["b", "c"],
5353
"result_paths": ["$['a'][0][~'b']", "$['a'][0][~'c']"],
5454
"tags": ["extra"]
5555
},
5656
{
57-
"name": "keys selector, array key",
57+
"name": "array key",
5858
"selector": "$.a.~",
5959
"document": { "a": [{ "b": "x", "c": "z" }, { "b": "y" }] },
6060
"result": [],
6161
"result_paths": [],
6262
"tags": ["extra"]
6363
},
6464
{
65-
"name": "keys selector, descendant keys",
65+
"name": "descendant keys",
6666
"selector": "$..[~]",
6767
"document": { "a": [{ "b": "x", "c": "z" }, { "b": "y" }] },
6868
"result": ["a", "b", "c", "b"],

tests/membership_operators.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"tests": [
3+
{
4+
"name": "array contains literal string",
5+
"selector": "$[?@.a contains 'foo']",
6+
"document": [{ "a": ["foo", "bar"] }, { "a": ["bar"] }],
7+
"result": [
8+
{
9+
"a": ["foo", "bar"]
10+
}
11+
],
12+
"result_paths": ["$[0]"],
13+
"tags": ["extra"]
14+
},
15+
{
16+
"name": "object contains literal string",
17+
"selector": "$[?@.a contains 'foo']",
18+
"document": [{ "a": { "foo": "bar" } }, { "a": { "bar": "baz" } }],
19+
"result": [
20+
{
21+
"a": { "foo": "bar" }
22+
}
23+
],
24+
"result_paths": ["$[0]"],
25+
"tags": ["extra"]
26+
},
27+
{
28+
"name": "string literal in array",
29+
"selector": "$[?'foo' in @.a]",
30+
"document": [{ "a": ["foo", "bar"] }, { "a": ["bar"] }],
31+
"result": [
32+
{
33+
"a": ["foo", "bar"]
34+
}
35+
],
36+
"result_paths": ["$[0]"],
37+
"tags": ["extra"]
38+
},
39+
{
40+
"name": "string literal in object",
41+
"selector": "$[?'foo' in @.a]",
42+
"document": [{ "a": { "foo": "bar" } }, { "a": { "bar": "baz" } }],
43+
"result": [
44+
{
45+
"a": { "foo": "bar" }
46+
}
47+
],
48+
"result_paths": ["$[0]"],
49+
"tags": ["extra"]
50+
},
51+
{
52+
"name": "string from embedded query in object",
53+
"selector": "$[?$[-1] in @.a]",
54+
"document": [{ "a": { "foo": "bar" } }, { "a": { "bar": "baz" } }, "foo"],
55+
"result": [
56+
{
57+
"a": { "foo": "bar" }
58+
}
59+
],
60+
"result_paths": ["$[0]"],
61+
"tags": ["extra"]
62+
}
63+
]
64+
}

tests/test_current_key_identifier.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55

66
from jsonpath import JSONPathEnvironment
7+
from jsonpath import JSONPathSyntaxError
78
from jsonpath import NodeList
89

910
from ._cts_case import Case
@@ -31,3 +32,11 @@ def test_current_key_identifier(env: JSONPathEnvironment, case: Case) -> None:
3132
assert case.result_paths is not None
3233
assert nodes.values() == case.result
3334
assert nodes.paths() == case.result_paths
35+
36+
37+
@pytest.mark.parametrize("case", data, ids=operator.attrgetter("name"))
38+
def test_current_key_identifier_fails_in_strict_mode(case: Case) -> None:
39+
env = JSONPathEnvironment(strict=True)
40+
41+
with pytest.raises(JSONPathSyntaxError):
42+
env.compile(case.selector)

tests/test_find.py

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -29,36 +29,6 @@ class Case:
2929
data={"some": {"thing": "else"}},
3030
want=[],
3131
),
32-
Case(
33-
description="keys from a mapping",
34-
path="$.some[~]",
35-
data={"some": {"thing": "else"}},
36-
want=["thing"],
37-
),
38-
Case(
39-
description="keys from a sequence",
40-
path="$.some.~",
41-
data={"some": ["thing", "else"]},
42-
want=[],
43-
),
44-
Case(
45-
description="match key pattern",
46-
path="$.some[?match(#, 'thing[0-9]+')]",
47-
data={
48-
"some": {
49-
"thing1": {"foo": 1},
50-
"thing2": {"foo": 2},
51-
"other": {"foo": 3},
52-
}
53-
},
54-
want=[{"foo": 1}, {"foo": 2}],
55-
),
56-
Case(
57-
description="filter current key, array data",
58-
path="$.abc[?(# >= 1)]",
59-
data={"abc": [1, 2, 3], "def": [4, 5], "abx": [6], "aby": []},
60-
want=[2, 3],
61-
),
6232
Case(
6333
description="select root value using pseudo root",
6434
path="^[?@.some.thing > 7]",
@@ -71,12 +41,6 @@ class Case:
7141
data={"some": {"thing": 42}, "num": 7},
7242
want=[{"some": {"thing": 42}, "num": 7}],
7343
),
74-
Case(
75-
description="recurse object keys",
76-
path="$..~",
77-
data={"some": {"thing": "else", "foo": {"bar": "baz"}}},
78-
want=["some", "thing", "foo", "bar"],
79-
),
8044
Case(
8145
description="logical expr existence tests",
8246
path="$[?@.a && @.b]",
@@ -89,46 +53,6 @@ class Case:
8953
data=[{"a": True, "b": False}],
9054
want=[{"a": True, "b": False}],
9155
),
92-
Case(
93-
description="array contains literal",
94-
path="$[?@.a contains 'foo']",
95-
data=[{"a": ["foo", "bar"]}, {"a": ["bar"]}],
96-
want=[
97-
{
98-
"a": ["foo", "bar"],
99-
}
100-
],
101-
),
102-
Case(
103-
description="object contains literal",
104-
path="$[?@.a contains 'foo']",
105-
data=[{"a": {"foo": "bar"}}, {"a": {"bar": "baz"}}],
106-
want=[
107-
{
108-
"a": {"foo": "bar"},
109-
}
110-
],
111-
),
112-
Case(
113-
description="literal in array",
114-
path="$[?'foo' in @.a]",
115-
data=[{"a": ["foo", "bar"]}, {"a": ["bar"]}],
116-
want=[
117-
{
118-
"a": ["foo", "bar"],
119-
}
120-
],
121-
),
122-
Case(
123-
description="literal in object",
124-
path="$[?'foo' in @.a]",
125-
data=[{"a": {"foo": "bar"}}, {"a": {"bar": "baz"}}],
126-
want=[
127-
{
128-
"a": {"foo": "bar"},
129-
}
130-
],
131-
),
13256
Case(
13357
description="quoted reserved word, and",
13458
path="$['and']",

tests/test_key_selector.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55

66
from jsonpath import JSONPathEnvironment
7+
from jsonpath import JSONPathSyntaxError
78
from jsonpath import NodeList
89

910
from ._cts_case import Case
@@ -31,3 +32,11 @@ def test_key_selector(env: JSONPathEnvironment, case: Case) -> None:
3132
assert case.result_paths is not None
3233
assert nodes.values() == case.result
3334
assert nodes.paths() == case.result_paths
35+
36+
37+
@pytest.mark.parametrize("case", data, ids=operator.attrgetter("name"))
38+
def test_key_selector_fails_in_strict_mode(case: Case) -> None:
39+
env = JSONPathEnvironment(strict=True)
40+
41+
with pytest.raises(JSONPathSyntaxError):
42+
env.compile(case.selector)

tests/test_keys_filter_selector.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55

66
from jsonpath import JSONPathEnvironment
7+
from jsonpath import JSONPathSyntaxError
78
from jsonpath import NodeList
89

910
from ._cts_case import Case
@@ -31,3 +32,11 @@ def test_keys_filter_selector(env: JSONPathEnvironment, case: Case) -> None:
3132
assert case.result_paths is not None
3233
assert nodes.values() == case.result
3334
assert nodes.paths() == case.result_paths
35+
36+
37+
@pytest.mark.parametrize("case", data, ids=operator.attrgetter("name"))
38+
def test_keys_filter_selector_fails_in_strict_mode(case: Case) -> None:
39+
env = JSONPathEnvironment(strict=True)
40+
41+
with pytest.raises(JSONPathSyntaxError):
42+
env.compile(case.selector)

0 commit comments

Comments
 (0)