Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ ENV/

# Dev utils
dev.py
_dev.py
profile_.py
tests/test_dev.py

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Fixed normalized paths produced by `JSONPathNode.path()`. Previously we were not handling some escape sequences correctly in name selectors.
- 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.
- Fixed parsing of filter queries with multiple bracketed segments.

## Version 0.1.3

Expand Down
4 changes: 1 addition & 3 deletions jsonpath_rfc9535/lex.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def ignore_whitespace(self) -> bool:
if self.pos != self.start:
msg = (
"must emit or ignore before consuming whitespace "
f"({self.query[self.start: self.pos]})"
f"({self.query[self.start : self.pos]})"
)
raise JSONPathLexerError(
msg, token=Token(TokenType.ERROR, msg, self.pos, self.query)
Expand Down Expand Up @@ -245,8 +245,6 @@ def lex_inside_bracketed_segment(l: Lexer) -> Optional[StateFn]: # noqa: PLR091

if c == "]":
l.emit(TokenType.RBRACKET)
if l.filter_depth:
return lex_inside_filter
return lex_segment

if c == "":
Expand Down
4 changes: 4 additions & 0 deletions jsonpath_rfc9535/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def values(self) -> List[object]:
"""Return the values from this node list."""
return [node.value for node in self]

def paths(self) -> List[str]:
"""Return normalized paths from this node list."""
return [node.path() for node in self]

def items(self) -> List[Tuple[str, object]]:
"""Return a list of (path, value) pairs, one for each node in the list."""
return [(node.path(), node.value) for node in self]
Expand Down
14 changes: 4 additions & 10 deletions jsonpath_rfc9535/selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,19 +166,13 @@ def _check_range(self, *indices: Optional[int]) -> None:
):
raise JSONPathIndexError("index out of range", token=self.token)

def _normalized_index(self, obj: Sequence[object], index: int) -> int:
if index < 0 and len(obj) >= abs(index):
return len(obj) + index
return index

def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]:
"""Select a range of values from an array/list."""
if isinstance(node.value, list) and self.slice.step != 0:
idx = self.slice.start or 0
step = self.slice.step or 1
for element in node.value[self.slice]:
yield node.new_child(element, self._normalized_index(node.value, idx))
idx += step
for idx, element in zip( # noqa: B905
range(*self.slice.indices(len(node.value))), node.value[self.slice]
):
yield node.new_child(element, idx)


class WildcardSelector(JSONPathSelector):
Expand Down
6 changes: 5 additions & 1 deletion jsonpath_rfc9535/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@

def canonical_string(value: str) -> str:
"""Return _value_ as a canonically formatted string literal."""
single_quoted = json.dumps(value)[1:-1].replace('\\"', '"').replace("'", "\\'")
single_quoted = (
json.dumps(value, ensure_ascii=False)[1:-1]
.replace('\\"', '"')
.replace("'", "\\'")
)
return f"'{single_quoted}'"
11 changes: 8 additions & 3 deletions tests/test_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ class Case:
selector: str
document: JSONValue = None
result: Any = None
result_paths: Optional[List[Any]] = None
results: Optional[List[Any]] = None
results_paths: Optional[List[Any]] = None
invalid_selector: Optional[bool] = None
tags: List[str] = field(default_factory=list)

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

assert case.document is not None
rv = jsonpath.JSONPathNodeList(jsonpath.find(case.selector, case.document)).values()
nodes = jsonpath.JSONPathNodeList(jsonpath.find(case.selector, case.document))

if case.results is not None:
assert rv in case.results
assert isinstance(case.results_paths, list)
assert nodes.values() in case.results
assert nodes.paths() in case.results_paths
else:
assert rv == case.result
assert nodes.values() == case.result
assert nodes.paths() == case.result_paths


@pytest.mark.parametrize("case", invalid_cases(), ids=operator.attrgetter("name"))
Expand Down
12 changes: 9 additions & 3 deletions tests/test_cts_nondeterminism.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import pytest

from jsonpath_rfc9535 import JSONPathEnvironment
from jsonpath_rfc9535 import JSONPathNodeList
from jsonpath_rfc9535 import JSONValue


Expand All @@ -25,7 +26,9 @@ class Case:
selector: str
document: JSONValue = None
result: Any = None
result_paths: Optional[List[Any]] = None
results: Optional[List[Any]] = None
results_paths: Optional[List[Any]] = None
invalid_selector: Optional[bool] = None
tags: List[str] = field(default_factory=list)

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

if case.results is not None:
assert rv in case.results
assert isinstance(case.results_paths, list)
assert nodes.values() in case.results
assert nodes.paths() in case.results_paths
else:
assert rv == case.result
assert nodes.values() == case.result
assert nodes.paths() == case.result_paths


@pytest.mark.parametrize(
Expand Down
5 changes: 5 additions & 0 deletions tests/test_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ class Case:
query="$[?!(@.a && [email protected])]",
want="$[?!(@['a'] && !@['b'])]",
),
Case(
description="filter query, multiple bracketed segments",
query="$[?@[0][1]]",
want="$[?@[0][1]]",
),
]


Expand Down
Loading