Skip to content

Commit 48ad155

Browse files
authored
Merge pull request #12 from jg-rp/update-cts
Prepare for CTS schema changes and fix normalized slice indexes
2 parents 17ac4d5 + ed2dfec commit 48ad155

10 files changed

+39
-21
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ ENV/
8383

8484
# Dev utils
8585
dev.py
86+
_dev.py
8687
profile_.py
8788
tests/test_dev.py
8889

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Fixed normalized paths produced by `JSONPathNode.path()`. Previously we were not handling some escape sequences correctly in name selectors.
88
- Fixed serialization of `JSONPathQuery` instances. `JSONPathQuery.__str__()` now serialized name selectors and string literals to the canonical format, similar to normalized paths. We're also now minimizing the use of parentheses when serializing logical expressions.
9+
- Fixed parsing of filter queries with multiple bracketed segments.
910

1011
## Version 0.1.3
1112

jsonpath_rfc9535/lex.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def ignore_whitespace(self) -> bool:
120120
if self.pos != self.start:
121121
msg = (
122122
"must emit or ignore before consuming whitespace "
123-
f"({self.query[self.start: self.pos]})"
123+
f"({self.query[self.start : self.pos]})"
124124
)
125125
raise JSONPathLexerError(
126126
msg, token=Token(TokenType.ERROR, msg, self.pos, self.query)
@@ -245,8 +245,6 @@ def lex_inside_bracketed_segment(l: Lexer) -> Optional[StateFn]: # noqa: PLR091
245245

246246
if c == "]":
247247
l.emit(TokenType.RBRACKET)
248-
if l.filter_depth:
249-
return lex_inside_filter
250248
return lex_segment
251249

252250
if c == "":

jsonpath_rfc9535/node.py

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ def values(self) -> List[object]:
6767
"""Return the values from this node list."""
6868
return [node.value for node in self]
6969

70+
def paths(self) -> List[str]:
71+
"""Return normalized paths from this node list."""
72+
return [node.path() for node in self]
73+
7074
def items(self) -> List[Tuple[str, object]]:
7175
"""Return a list of (path, value) pairs, one for each node in the list."""
7276
return [(node.path(), node.value) for node in self]

jsonpath_rfc9535/selectors.py

+4-10
Original file line numberDiff line numberDiff line change
@@ -166,19 +166,13 @@ def _check_range(self, *indices: Optional[int]) -> None:
166166
):
167167
raise JSONPathIndexError("index out of range", token=self.token)
168168

169-
def _normalized_index(self, obj: Sequence[object], index: int) -> int:
170-
if index < 0 and len(obj) >= abs(index):
171-
return len(obj) + index
172-
return index
173-
174169
def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]:
175170
"""Select a range of values from an array/list."""
176171
if isinstance(node.value, list) and self.slice.step != 0:
177-
idx = self.slice.start or 0
178-
step = self.slice.step or 1
179-
for element in node.value[self.slice]:
180-
yield node.new_child(element, self._normalized_index(node.value, idx))
181-
idx += step
172+
for idx, element in zip( # noqa: B905
173+
range(*self.slice.indices(len(node.value))), node.value[self.slice]
174+
):
175+
yield node.new_child(element, idx)
182176

183177

184178
class WildcardSelector(JSONPathSelector):

jsonpath_rfc9535/serialize.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@
55

66
def canonical_string(value: str) -> str:
77
"""Return _value_ as a canonically formatted string literal."""
8-
single_quoted = json.dumps(value)[1:-1].replace('\\"', '"').replace("'", "\\'")
8+
single_quoted = (
9+
json.dumps(value, ensure_ascii=False)[1:-1]
10+
.replace('\\"', '"')
11+
.replace("'", "\\'")
12+
)
913
return f"'{single_quoted}'"

tests/test_compliance.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ class Case:
2525
selector: str
2626
document: JSONValue = None
2727
result: Any = None
28+
result_paths: Optional[List[Any]] = None
2829
results: Optional[List[Any]] = None
30+
results_paths: Optional[List[Any]] = None
2931
invalid_selector: Optional[bool] = None
3032
tags: List[str] = field(default_factory=list)
3133

@@ -53,12 +55,15 @@ def test_compliance(case: Case) -> None:
5355
pytest.skip(reason=SKIP[case.name]) # no cov
5456

5557
assert case.document is not None
56-
rv = jsonpath.JSONPathNodeList(jsonpath.find(case.selector, case.document)).values()
58+
nodes = jsonpath.JSONPathNodeList(jsonpath.find(case.selector, case.document))
5759

5860
if case.results is not None:
59-
assert rv in case.results
61+
assert isinstance(case.results_paths, list)
62+
assert nodes.values() in case.results
63+
assert nodes.paths() in case.results_paths
6064
else:
61-
assert rv == case.result
65+
assert nodes.values() == case.result
66+
assert nodes.paths() == case.result_paths
6267

6368

6469
@pytest.mark.parametrize("case", invalid_cases(), ids=operator.attrgetter("name"))

tests/test_cts_nondeterminism.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import pytest
1717

1818
from jsonpath_rfc9535 import JSONPathEnvironment
19+
from jsonpath_rfc9535 import JSONPathNodeList
1920
from jsonpath_rfc9535 import JSONValue
2021

2122

@@ -25,7 +26,9 @@ class Case:
2526
selector: str
2627
document: JSONValue = None
2728
result: Any = None
29+
result_paths: Optional[List[Any]] = None
2830
results: Optional[List[Any]] = None
31+
results_paths: Optional[List[Any]] = None
2932
invalid_selector: Optional[bool] = None
3033
tags: List[str] = field(default_factory=list)
3134

@@ -52,12 +55,15 @@ class MockEnv(JSONPathEnvironment):
5255
def test_nondeterminism_valid_cases(case: Case) -> None:
5356
assert case.document is not None
5457
env = MockEnv()
55-
rv = env.find(case.selector, case.document).values()
58+
nodes = JSONPathNodeList(env.find(case.selector, case.document))
5659

5760
if case.results is not None:
58-
assert rv in case.results
61+
assert isinstance(case.results_paths, list)
62+
assert nodes.values() in case.results
63+
assert nodes.paths() in case.results_paths
5964
else:
60-
assert rv == case.result
65+
assert nodes.values() == case.result
66+
assert nodes.paths() == case.result_paths
6167

6268

6369
@pytest.mark.parametrize(

tests/test_parse.py

+5
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ class Case:
136136
query="$[?!(@.a && [email protected])]",
137137
want="$[?!(@['a'] && !@['b'])]",
138138
),
139+
Case(
140+
description="filter query, multiple bracketed segments",
141+
query="$[?@[0][1]]",
142+
want="$[?@[0][1]]",
143+
),
139144
]
140145

141146

0 commit comments

Comments
 (0)