Skip to content

Commit 84ebe96

Browse files
authored
Respect the recursion limit in nondeterministic mode (#1)
* Respect the recursion limit in nondeterministic mode * Fix depth for children of children
1 parent b1772df commit 84ebe96

File tree

3 files changed

+64
-8
lines changed

3 files changed

+64
-8
lines changed

jsonpath_rfc9535/segments.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def resolve(self, nodes: Iterable[JSONPathNode]) -> Iterable[JSONPathNode]:
7676
# XXX: This feels like a bit of a hack.
7777
visitor = (
7878
self._nondeterministic_visit
79-
if self.env.nondeterministic and random.random() < 0.2 # noqa: S311, PLR2004
79+
if self.env.nondeterministic and random.random() < 0.8 # noqa: S311, PLR2004
8080
else self._visit
8181
)
8282

@@ -86,7 +86,7 @@ def resolve(self, nodes: Iterable[JSONPathNode]) -> Iterable[JSONPathNode]:
8686
yield from selector.resolve(_node)
8787

8888
def _visit(self, node: JSONPathNode, depth: int = 1) -> Iterable[JSONPathNode]:
89-
"""Pre order node traversal."""
89+
"""Depth-first, pre-order node traversal."""
9090
if depth > self.env.max_recursion_depth:
9191
raise JSONPathRecursionError("recursion limit exceeded", token=self.token)
9292

@@ -136,20 +136,31 @@ def _children(node: JSONPathNode) -> Iterable[JSONPathNode]:
136136
root=node.root,
137137
)
138138

139-
queue: Deque[JSONPathNode] = deque(_children(root))
140-
yield root
139+
# (node, depth) tuples
140+
queue: Deque[Tuple[JSONPathNode, int]] = deque()
141+
142+
yield root # visit the root node
143+
queue.extend([(child, 1) for child in _children(root)]) # queue root's children
141144

142145
while queue:
143-
_node = queue.popleft()
146+
_node, depth = queue.popleft()
147+
148+
if depth >= self.env.max_recursion_depth:
149+
raise JSONPathRecursionError(
150+
"recursion limit exceeded", token=self.token
151+
)
152+
144153
yield _node
154+
145155
# Visit child nodes now or queue them for later?
146156
visit_children = random.choice([True, False]) # noqa: S311
157+
147158
for child in _children(_node):
148159
if visit_children:
149160
yield child
150-
queue.extend(_children(child))
161+
queue.extend([(child, depth + 2) for child in _children(child)])
151162
else:
152-
queue.append(child)
163+
queue.append((child, depth + 1))
153164

154165
def __str__(self) -> str:
155166
return f"..[{', '.join(str(itm) for itm in self.selectors)}]"

tests/test_errors.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,26 @@ class MockEnv(JSONPathEnvironment):
6464
env.find(query, data)
6565

6666

67+
def test_recursive_data_nondeterministic() -> None:
68+
class MockEnv(JSONPathEnvironment):
69+
nondeterministic = True
70+
71+
env = MockEnv()
72+
query = "$..a"
73+
arr: List[Any] = []
74+
data: Any = {"foo": arr}
75+
arr.append(data)
76+
77+
with pytest.raises(JSONPathRecursionError):
78+
env.find(query, data)
79+
80+
6781
class FilterLiteralTestCase(NamedTuple):
6882
description: str
6983
query: str
7084

7185

86+
# TODO: add these to the CTS?
7287
BAD_FILTER_LITERAL_TEST_CASES: List[FilterLiteralTestCase] = [
7388
FilterLiteralTestCase("just true", "$[?true]"),
7489
FilterLiteralTestCase("just string", "$[?'foo']"),

tests/test_nondeterminism.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import Any
1111
from typing import List
1212
from typing import Optional
13+
from typing import Tuple
1314

1415
import pytest
1516

@@ -37,12 +38,16 @@ def valid_cases() -> List[Case]:
3738
return [case for case in cases() if not case.invalid_selector]
3839

3940

41+
def nondeterministic_cases() -> List[Case]:
42+
return [case for case in valid_cases() if isinstance(case.results, list)]
43+
44+
4045
class MockEnv(JSONPathEnvironment):
4146
nondeterministic = True
4247

4348

4449
@pytest.mark.parametrize("case", valid_cases(), ids=operator.attrgetter("name"))
45-
def test_nondeterminism(case: Case) -> None:
50+
def test_nondeterminism_valid_cases(case: Case) -> None:
4651
assert case.document is not None
4752
env = MockEnv()
4853
rv = env.find(case.selector, case.document).values()
@@ -51,3 +56,28 @@ def test_nondeterminism(case: Case) -> None:
5156
assert rv in case.results
5257
else:
5358
assert rv == case.result
59+
60+
61+
@pytest.mark.parametrize(
62+
"case", nondeterministic_cases(), ids=operator.attrgetter("name")
63+
)
64+
def test_nondeterminism(case: Case) -> None:
65+
"""Test that we agree with CTS when it comes to nondeterministic results."""
66+
assert case.document is not None
67+
assert case.results is not None
68+
69+
def _result_repr(rv: List[object]) -> Tuple[str, ...]:
70+
"""Return a hashable representation of a result list."""
71+
return tuple([str(value) for value in rv])
72+
73+
env = MockEnv()
74+
75+
# Repeat enough times to has high probability that we've covered all
76+
# valid permutations.
77+
results = {
78+
_result_repr(env.find(case.selector, case.document).values())
79+
for _ in range(1000)
80+
}
81+
82+
assert len(results) == len(case.results)
83+
assert results == {_result_repr(result) for result in case.results}

0 commit comments

Comments
 (0)