Skip to content

Commit 0c329db

Browse files
committed
Make ondeterministic descent helpers importable
1 parent ee6404e commit 0c329db

File tree

4 files changed

+173
-154
lines changed

4 files changed

+173
-154
lines changed

jsonpath_rfc9535/utils/__init__.py

Whitespace-only changes.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""Utilities for exploring nondeterminism in the recursive descent segment."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
import random
7+
import sys
8+
from collections import deque
9+
from typing import TYPE_CHECKING
10+
from typing import Deque
11+
from typing import Iterable
12+
from typing import List
13+
from typing import Optional
14+
from typing import TextIO
15+
from typing import Tuple
16+
17+
if TYPE_CHECKING:
18+
from jsonpath_rfc9535.environment import JSONLikeData
19+
20+
21+
HORIZONTAL_SEP = "\N{BOX DRAWINGS LIGHT HORIZONTAL}" * 2
22+
VERTICAL_SEP = "\N{BOX DRAWINGS LIGHT VERTICAL}"
23+
BRANCH = "\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}" + HORIZONTAL_SEP + " "
24+
TERMINAL_BRANCH = "\N{BOX DRAWINGS LIGHT UP AND RIGHT}" + HORIZONTAL_SEP + " "
25+
INDENT = VERTICAL_SEP + " " * 3
26+
TERMINAL_INDENT = " " * 4
27+
28+
COLOR_CODES = [
29+
("\033[92m", "\033[0m"),
30+
("\033[93m", "\033[0m"),
31+
("\033[94m", "\033[0m"),
32+
("\033[95m", "\033[0m"),
33+
("\033[96m", "\033[0m"),
34+
("\033[91m", "\033[0m"),
35+
]
36+
37+
38+
class AuxNode:
39+
"""A auxiliary tree node, including a list of child nodes and depth.
40+
41+
Use the `from_(data)` static method to build an auxiliary tree instead of
42+
instantiating this class directly.
43+
"""
44+
45+
def __init__(
46+
self,
47+
depth: int,
48+
value: object,
49+
children: Optional[List[AuxNode]] = None,
50+
) -> None:
51+
self.value = value
52+
self.children = children or []
53+
self.depth = depth
54+
55+
def __str__(self) -> str:
56+
c_start, c_stop = COLOR_CODES[self.depth % len(COLOR_CODES)]
57+
return f"{c_start}{self.value}{c_stop}"
58+
59+
@staticmethod
60+
def from_(data: JSONLikeData, *, collections_only: bool = False) -> AuxNode:
61+
"""Build a tree from JSON-like _data_."""
62+
63+
def _visit(node: AuxNode, depth: int = 0) -> None:
64+
if isinstance(node.value, dict):
65+
for val in node.value.values():
66+
if not collections_only or isinstance(val, (list, dict)):
67+
_node = AuxNode(depth + 1, val)
68+
_visit(_node, depth + 1)
69+
node.children.append(_node)
70+
71+
elif isinstance(node.value, list):
72+
for val in node.value:
73+
if not collections_only or isinstance(val, (list, dict)):
74+
_node = AuxNode(depth + 1, val)
75+
_visit(_node, depth + 1)
76+
node.children.append(_node)
77+
78+
root = AuxNode(0, data)
79+
_visit(root)
80+
return root
81+
82+
83+
def pp_tree(
84+
node: AuxNode,
85+
indent: str = "",
86+
buf: TextIO = sys.stdout,
87+
) -> None:
88+
"""Pretty print the tree rooted at `node`."""
89+
# Pre-order tree traversal
90+
buf.write(str(node) + os.linesep)
91+
92+
if node.children:
93+
# Recursively call pptree for all but the last child of `node`.
94+
for child in node.children[:-1]:
95+
buf.write(indent + BRANCH)
96+
pp_tree(child, indent=indent + INDENT, buf=buf)
97+
98+
# Terminal branch case for last, possibly only, child of `node`.
99+
buf.write(indent + TERMINAL_BRANCH)
100+
pp_tree(node.children[-1], indent=indent + TERMINAL_INDENT, buf=buf)
101+
102+
# Base case. No children.
103+
104+
105+
def pre_order_visit(node: AuxNode) -> Iterable[AuxNode]:
106+
"""Generate nodes rooted at _node_ from a pre-order traversal."""
107+
yield node
108+
109+
for child in node.children:
110+
yield from pre_order_visit(child)
111+
112+
113+
def breadth_first_visit(node: AuxNode) -> Iterable[AuxNode]:
114+
"""Generate nodes rooted at _node_ from a level-order traversal."""
115+
queue: Deque[AuxNode] = deque([node])
116+
117+
while queue:
118+
_node = queue.popleft()
119+
yield _node
120+
queue.extend(_node.children)
121+
122+
123+
def nondeterministic_visit(root: AuxNode) -> Iterable[AuxNode]:
124+
"""Generate nodes rooted at _node_ from a nondeterministic traversal.
125+
126+
This tree visitor will never produce nodes in depth-first pre-order, so
127+
use `pre_order_visit` in addition to `nondeterministic_visit` to get all
128+
permutations. Or use `all_perms()`.
129+
"""
130+
queue: Deque[AuxNode] = deque(root.children)
131+
yield root
132+
133+
while queue:
134+
_node = queue.popleft()
135+
yield _node
136+
# Visit child nodes now or queue them for later?
137+
visit_children = random.choice([True, False])
138+
for child in _node.children:
139+
if visit_children:
140+
yield child
141+
queue.extend(child.children)
142+
else:
143+
queue.append(child)
144+
145+
146+
def all_perms(root: AuxNode) -> List[Tuple[AuxNode, ...]]:
147+
"""Return a list of valid permutations for the auxiliary tree _root_."""
148+
perms = {tuple(nondeterministic_visit(root)) for _ in range(1000)}
149+
perms.add(tuple(pre_order_visit(root)))
150+
return sorted(perms, key=lambda t: str(t))

pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,10 @@ convention = "google"
172172
"scripts/__init__.py" = ["D104"]
173173
"tests/*" = ["D100", "D101", "D104", "D103"]
174174
"jsonpath_rfc9535/lex.py" = ["E741"]
175-
"scripts/nondeterministic_descent.py" = ["D103", "T201", "D101", "D102", "S311"]
175+
"jsonpath_rfc9535/utils/nondeterministic_descent.py" = [
176+
"D103",
177+
"D101",
178+
"D102",
179+
"S311",
180+
]
181+
"scripts/nondeterministic_descent.py" = ["T201"]

scripts/nondeterministic_descent.py

Lines changed: 16 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,164 +1,27 @@
1-
"""Utilities for exploring nondeterminism in the recursive descent segment."""
1+
"""Command line utility for inspecting nondeterministic recursive descent orderings."""
22

33
from __future__ import annotations
44

55
import json
6-
import os
7-
import random
86
import sys
9-
from collections import deque
107
from typing import TYPE_CHECKING
11-
from typing import Deque
12-
from typing import Iterable
13-
from typing import List
14-
from typing import Optional
15-
from typing import TextIO
16-
from typing import Tuple
8+
9+
from jsonpath_rfc9535.utils.nondeterministic_descent import AuxNode
10+
from jsonpath_rfc9535.utils.nondeterministic_descent import all_perms
11+
from jsonpath_rfc9535.utils.nondeterministic_descent import breadth_first_visit
12+
from jsonpath_rfc9535.utils.nondeterministic_descent import pp_tree
13+
from jsonpath_rfc9535.utils.nondeterministic_descent import pre_order_visit
1714

1815
if TYPE_CHECKING:
19-
from jsonpath_rfc9535.environment import JSONLikeData
20-
21-
22-
HORIZONTAL_SEP = "\N{BOX DRAWINGS LIGHT HORIZONTAL}" * 2
23-
VERTICAL_SEP = "\N{BOX DRAWINGS LIGHT VERTICAL}"
24-
BRANCH = "\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}" + HORIZONTAL_SEP + " "
25-
TERMINAL_BRANCH = "\N{BOX DRAWINGS LIGHT UP AND RIGHT}" + HORIZONTAL_SEP + " "
26-
INDENT = VERTICAL_SEP + " " * 3
27-
TERMINAL_INDENT = " " * 4
28-
29-
COLOR_CODES = [
30-
("\033[92m", "\033[0m"),
31-
("\033[93m", "\033[0m"),
32-
("\033[94m", "\033[0m"),
33-
("\033[95m", "\033[0m"),
34-
("\033[96m", "\033[0m"),
35-
("\033[91m", "\033[0m"),
36-
]
37-
38-
39-
class AuxNode:
40-
def __init__(
41-
self,
42-
depth: int,
43-
value: object,
44-
children: Optional[List[AuxNode]] = None,
45-
) -> None:
46-
self.value = value
47-
self.children = children or []
48-
self.depth = depth
49-
50-
def __str__(self) -> str:
51-
c_start, c_stop = COLOR_CODES[self.depth % len(COLOR_CODES)]
52-
return f"{c_start}{self.value}{c_stop}"
53-
54-
@staticmethod
55-
def from_(data: JSONLikeData) -> AuxNode:
56-
def _visit(node: AuxNode, depth: int = 0) -> None:
57-
if isinstance(node.value, dict):
58-
for val in node.value.values():
59-
_node = AuxNode(depth + 1, val)
60-
_visit(_node, depth + 1)
61-
node.children.append(_node)
62-
63-
elif isinstance(node.value, list):
64-
for val in node.value:
65-
_node = AuxNode(depth + 1, val)
66-
_visit(_node, depth + 1)
67-
node.children.append(_node)
68-
69-
root = AuxNode(0, data)
70-
_visit(root)
71-
return root
72-
73-
@staticmethod
74-
def collections(data: JSONLikeData) -> AuxNode:
75-
def _visit(node: AuxNode, depth: int = 0) -> None:
76-
if isinstance(node.value, dict):
77-
for val in node.value.values():
78-
if isinstance(val, (list, dict)):
79-
_node = AuxNode(depth + 1, val)
80-
_visit(_node, depth + 1)
81-
node.children.append(_node)
82-
83-
elif isinstance(node.value, list):
84-
for val in node.value:
85-
if isinstance(val, (list, dict)):
86-
_node = AuxNode(depth + 1, val)
87-
_visit(_node, depth + 1)
88-
node.children.append(_node)
89-
90-
root = AuxNode(0, data)
91-
_visit(root)
92-
return root
93-
94-
95-
def pptree(
96-
node: AuxNode,
97-
indent: str = "",
98-
buf: TextIO = sys.stdout,
99-
) -> None:
100-
"""Pretty print the tree rooted at `node`."""
101-
# Pre-order tree traversal
102-
buf.write(str(node) + os.linesep)
103-
104-
if node.children:
105-
# Recursively call pptree for all but the last child of `node`.
106-
for child in node.children[:-1]:
107-
buf.write(indent + BRANCH)
108-
pptree(child, indent=indent + INDENT, buf=buf)
109-
110-
# Terminal branch case for last, possibly only, child of `node`.
111-
buf.write(indent + TERMINAL_BRANCH)
112-
pptree(node.children[-1], indent=indent + TERMINAL_INDENT, buf=buf)
113-
114-
# Base case. No children.
115-
116-
117-
def pre_order_visit(node: AuxNode) -> Iterable[AuxNode]:
118-
yield node
119-
120-
for child in node.children:
121-
yield from pre_order_visit(child)
122-
123-
124-
def breadth_first_visit(node: AuxNode) -> Iterable[AuxNode]:
125-
queue: Deque[AuxNode] = deque([node])
126-
127-
while queue:
128-
_node = queue.popleft()
129-
yield _node
130-
queue.extend(_node.children)
131-
132-
133-
def nondeterministic_visit(root: AuxNode) -> Iterable[AuxNode]:
134-
queue: Deque[AuxNode] = deque(root.children)
135-
yield root
136-
137-
while queue:
138-
_node = queue.popleft()
139-
yield _node
140-
# Visit child nodes now or queue them for later?
141-
visit_children = random.choice([True, False])
142-
for child in _node.children:
143-
if visit_children:
144-
yield child
145-
queue.extend(child.children)
146-
else:
147-
queue.append(child)
148-
149-
150-
def get_perms(root: AuxNode) -> List[Tuple[AuxNode, ...]]:
151-
perms = {tuple(nondeterministic_visit(root)) for _ in range(1000)}
152-
perms.add(tuple(pre_order_visit(root)))
153-
return sorted(perms, key=lambda t: str(t))
154-
155-
156-
def pp_json_path_perms(data: JSONLikeData) -> None:
16+
from jsonpath_rfc9535 import JSONLikeData
17+
18+
19+
def pp_json_path_perms(data: JSONLikeData) -> None: # noqa: D103
15720
print("Input data")
15821
print(f"\033[92m{data}\033[0m")
15922
aux_tree = AuxNode.from_(data)
16023
print("\nTree view")
161-
pptree(aux_tree)
24+
pp_tree(aux_tree)
16225

16326
print("\nPre order")
16427
print(", ".join(str(n) for n in pre_order_visit(aux_tree)))
@@ -167,12 +30,12 @@ def pp_json_path_perms(data: JSONLikeData) -> None:
16730
print(", ".join(str(n) for n in breadth_first_visit(aux_tree)))
16831

16932
print("\nNondeterministic order")
170-
for perm in get_perms(aux_tree):
33+
for perm in all_perms(aux_tree):
17134
print(", ".join(str(node) for node in perm))
17235

17336
print("\n---\n\nCollections only")
174-
aux_tree = AuxNode.collections(data)
175-
pptree(aux_tree)
37+
aux_tree = AuxNode.from_(data, collections_only=True)
38+
pp_tree(aux_tree)
17639

17740
print("\nPre order")
17841
print(", ".join(str(n) for n in pre_order_visit(aux_tree)))
@@ -181,7 +44,7 @@ def pp_json_path_perms(data: JSONLikeData) -> None:
18144
print(", ".join(str(n) for n in breadth_first_visit(aux_tree)))
18245

18346
print("\nNondeterministic order")
184-
for perm in get_perms(aux_tree):
47+
for perm in all_perms(aux_tree):
18548
print(", ".join(str(node) for node in perm))
18649

18750

0 commit comments

Comments
 (0)