From b1367d4c0b82394edc79d7ec9a6f943c24a60689 Mon Sep 17 00:00:00 2001 From: James Prior Date: Mon, 24 Jun 2024 15:10:09 +0100 Subject: [PATCH 1/2] Node locations as persistent linked lists --- jsonpath_rfc9535/location.py | 33 +++++++++++++++++++++++++++++++++ jsonpath_rfc9535/node.py | 11 +++++++---- jsonpath_rfc9535/query.py | 3 ++- jsonpath_rfc9535/segments.py | 8 ++++---- jsonpath_rfc9535/selectors.py | 14 +++++++------- 5 files changed, 53 insertions(+), 16 deletions(-) create mode 100644 jsonpath_rfc9535/location.py diff --git a/jsonpath_rfc9535/location.py b/jsonpath_rfc9535/location.py new file mode 100644 index 0000000..42a98ea --- /dev/null +++ b/jsonpath_rfc9535/location.py @@ -0,0 +1,33 @@ +"""JSONPath node locations as persistent linked lists.""" + +from __future__ import annotations + +from typing import Iterator +from typing import Optional +from typing import Union + + +class Location: + """JSONPath node location.""" + + __slots__ = ("value", "next") + + def __init__( + self, value: Union[int, str, None], _next: Optional[Location] = None + ) -> None: + self.value = value + self.next = _next + + def __iter__(self) -> Iterator[Union[int, str]]: + if self.value is not None: + yield self.value + + _next = self.next + while _next is not None: + if _next.value is not None: + yield _next.value + _next = _next.next + + def prepend(self, value: Union[int, str]) -> Location: + """Return a copy of this location with _value_ appended to the front.""" + return Location(value, self) diff --git a/jsonpath_rfc9535/node.py b/jsonpath_rfc9535/node.py index 3aa291f..29e504e 100644 --- a/jsonpath_rfc9535/node.py +++ b/jsonpath_rfc9535/node.py @@ -5,10 +5,10 @@ from typing import TYPE_CHECKING from typing import List from typing import Tuple -from typing import Union if TYPE_CHECKING: from .environment import JSONValue + from .location import Location class JSONPathNode: @@ -29,17 +29,20 @@ def __init__( self, *, value: object, - location: Tuple[Union[int, str], ...], + location: Location, root: JSONValue, ) -> None: self.value: object = value - self.location: Tuple[Union[int, str], ...] = location + self.location: Location = location self.root = root def path(self) -> str: """Return the normalized path to this node.""" return "$" + "".join( - (f"['{p}']" if isinstance(p, str) else f"[{p}]" for p in self.location) + ( + f"['{p}']" if isinstance(p, str) else f"[{p}]" + for p in reversed(list(self.location)) + ) ) def __str__(self) -> str: diff --git a/jsonpath_rfc9535/query.py b/jsonpath_rfc9535/query.py index 35c6ea2..92d14ad 100644 --- a/jsonpath_rfc9535/query.py +++ b/jsonpath_rfc9535/query.py @@ -7,6 +7,7 @@ from typing import Optional from typing import Tuple +from .location import Location from .node import JSONPathNode from .node import JSONPathNodeList from .segments import JSONPathRecursiveDescentSegment @@ -69,7 +70,7 @@ def finditer( nodes: Iterable[JSONPathNode] = [ JSONPathNode( value=value, - location=(), + location=Location(None), root=value, ) ] diff --git a/jsonpath_rfc9535/segments.py b/jsonpath_rfc9535/segments.py index b5a8b17..5716e6b 100644 --- a/jsonpath_rfc9535/segments.py +++ b/jsonpath_rfc9535/segments.py @@ -90,7 +90,7 @@ def _visit(self, node: JSONPathNode, depth: int = 1) -> Iterable[JSONPathNode]: if isinstance(val, (dict, list)): _node = JSONPathNode( value=val, - location=node.location + (name,), + location=node.location.prepend(name), root=node.root, ) yield from self._visit(_node, depth + 1) @@ -99,7 +99,7 @@ def _visit(self, node: JSONPathNode, depth: int = 1) -> Iterable[JSONPathNode]: if isinstance(element, (dict, list)): _node = JSONPathNode( value=element, - location=node.location + (i,), + location=node.location.prepend(i), root=node.root, ) yield from self._visit(_node, depth + 1) @@ -177,13 +177,13 @@ def _nondeterministic_children(node: JSONPathNode) -> Iterable[JSONPathNode]: for name, val in items: yield JSONPathNode( value=val, - location=node.location + (name,), + location=node.location.prepend(name), root=node.root, ) elif isinstance(node.value, list): for i, element in enumerate(node.value): yield JSONPathNode( value=element, - location=node.location + (i,), + location=node.location.prepend(i), root=node.root, ) diff --git a/jsonpath_rfc9535/selectors.py b/jsonpath_rfc9535/selectors.py index cebd2a8..dac602e 100644 --- a/jsonpath_rfc9535/selectors.py +++ b/jsonpath_rfc9535/selectors.py @@ -78,7 +78,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: with suppress(KeyError): yield JSONPathNode( value=node.value[self.name], - location=node.location + (self.name,), + location=node.location.prepend(self.name), root=node.root, ) @@ -127,7 +127,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: with suppress(IndexError): _node = JSONPathNode( value=node.value[self.index], - location=node.location + (norm_index,), + location=node.location.prepend(norm_index), root=node.root, ) yield _node @@ -188,7 +188,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: norm_index = self._normalized_index(node.value, idx) _node = JSONPathNode( value=element, - location=node.location + (norm_index,), + location=node.location.prepend(norm_index), root=node.root, ) yield _node @@ -223,7 +223,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: for name, val in members: _node = JSONPathNode( value=val, - location=node.location + (name,), + location=node.location.prepend(name), root=node.root, ) yield _node @@ -232,7 +232,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: for i, element in enumerate(node.value): _node = JSONPathNode( value=element, - location=node.location + (i,), + location=node.location.prepend(i), root=node.root, ) yield _node @@ -286,7 +286,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: # noqa: PLR091 if self.expression.evaluate(context): yield JSONPathNode( value=val, - location=node.location + (name,), + location=node.location.prepend(name), root=node.root, ) except JSONPathTypeError as err: @@ -305,7 +305,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: # noqa: PLR091 if self.expression.evaluate(context): yield JSONPathNode( value=element, - location=node.location + (i,), + location=node.location.prepend(i), root=node.root, ) except JSONPathTypeError as err: From f37c0503d4b01b289c41a620d96c7f26559c2538 Mon Sep 17 00:00:00 2001 From: James Prior Date: Tue, 25 Jun 2024 07:58:41 +0100 Subject: [PATCH 2/2] Refactor new node creation --- .gitignore | 4 +++ jsonpath_rfc9535/location.py | 33 --------------------- jsonpath_rfc9535/node.py | 19 ++++++++----- jsonpath_rfc9535/query.py | 3 +- jsonpath_rfc9535/segments.py | 26 ++++------------- jsonpath_rfc9535/selectors.py | 49 ++++++-------------------------- tests/test_cts_nondeterminism.py | 2 +- 7 files changed, 31 insertions(+), 105 deletions(-) delete mode 100644 jsonpath_rfc9535/location.py diff --git a/.gitignore b/.gitignore index b6659a1..f147690 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,9 @@ coverage.xml .pytest_cache/ cov.xml +# Profiling data +mprofile* + # Translations *.mo *.pot @@ -81,6 +84,7 @@ ENV/ # Dev utils dev.py profile_.py +tests/test_dev.py # Test fixtures comparison_regression_suite.yaml diff --git a/jsonpath_rfc9535/location.py b/jsonpath_rfc9535/location.py deleted file mode 100644 index 42a98ea..0000000 --- a/jsonpath_rfc9535/location.py +++ /dev/null @@ -1,33 +0,0 @@ -"""JSONPath node locations as persistent linked lists.""" - -from __future__ import annotations - -from typing import Iterator -from typing import Optional -from typing import Union - - -class Location: - """JSONPath node location.""" - - __slots__ = ("value", "next") - - def __init__( - self, value: Union[int, str, None], _next: Optional[Location] = None - ) -> None: - self.value = value - self.next = _next - - def __iter__(self) -> Iterator[Union[int, str]]: - if self.value is not None: - yield self.value - - _next = self.next - while _next is not None: - if _next.value is not None: - yield _next.value - _next = _next.next - - def prepend(self, value: Union[int, str]) -> Location: - """Return a copy of this location with _value_ appended to the front.""" - return Location(value, self) diff --git a/jsonpath_rfc9535/node.py b/jsonpath_rfc9535/node.py index 29e504e..4d9dd54 100644 --- a/jsonpath_rfc9535/node.py +++ b/jsonpath_rfc9535/node.py @@ -5,10 +5,10 @@ from typing import TYPE_CHECKING from typing import List from typing import Tuple +from typing import Union if TYPE_CHECKING: from .environment import JSONValue - from .location import Location class JSONPathNode: @@ -29,20 +29,25 @@ def __init__( self, *, value: object, - location: Location, + location: Tuple[Union[int, str], ...], root: JSONValue, ) -> None: self.value: object = value - self.location: Location = location + self.location: Tuple[Union[int, str], ...] = location self.root = root def path(self) -> str: """Return the normalized path to this node.""" return "$" + "".join( - ( - f"['{p}']" if isinstance(p, str) else f"[{p}]" - for p in reversed(list(self.location)) - ) + f"['{p}']" if isinstance(p, str) else f"[{p}]" for p in self.location + ) + + def new_child(self, value: object, key: Union[int, str]) -> JSONPathNode: + """Return a new node using this node's location.""" + return JSONPathNode( + value=value, + location=self.location + (key,), + root=self.root, ) def __str__(self) -> str: diff --git a/jsonpath_rfc9535/query.py b/jsonpath_rfc9535/query.py index 92d14ad..35c6ea2 100644 --- a/jsonpath_rfc9535/query.py +++ b/jsonpath_rfc9535/query.py @@ -7,7 +7,6 @@ from typing import Optional from typing import Tuple -from .location import Location from .node import JSONPathNode from .node import JSONPathNodeList from .segments import JSONPathRecursiveDescentSegment @@ -70,7 +69,7 @@ def finditer( nodes: Iterable[JSONPathNode] = [ JSONPathNode( value=value, - location=Location(None), + location=(), root=value, ) ] diff --git a/jsonpath_rfc9535/segments.py b/jsonpath_rfc9535/segments.py index 5716e6b..d5813bf 100644 --- a/jsonpath_rfc9535/segments.py +++ b/jsonpath_rfc9535/segments.py @@ -12,10 +12,10 @@ from typing import Tuple from .exceptions import JSONPathRecursionError -from .node import JSONPathNode if TYPE_CHECKING: from .environment import JSONPathEnvironment + from .node import JSONPathNode from .selectors import JSONPathSelector from .tokens import Token @@ -88,20 +88,12 @@ def _visit(self, node: JSONPathNode, depth: int = 1) -> Iterable[JSONPathNode]: if isinstance(node.value, dict): for name, val in node.value.items(): if isinstance(val, (dict, list)): - _node = JSONPathNode( - value=val, - location=node.location.prepend(name), - root=node.root, - ) + _node = node.new_child(val, name) yield from self._visit(_node, depth + 1) elif isinstance(node.value, list): for i, element in enumerate(node.value): if isinstance(element, (dict, list)): - _node = JSONPathNode( - value=element, - location=node.location.prepend(i), - root=node.root, - ) + _node = node.new_child(element, i) yield from self._visit(_node, depth + 1) def _nondeterministic_visit( @@ -175,15 +167,7 @@ def _nondeterministic_children(node: JSONPathNode) -> Iterable[JSONPathNode]: items = list(node.value.items()) random.shuffle(items) for name, val in items: - yield JSONPathNode( - value=val, - location=node.location.prepend(name), - root=node.root, - ) + yield node.new_child(val, name) elif isinstance(node.value, list): for i, element in enumerate(node.value): - yield JSONPathNode( - value=element, - location=node.location.prepend(i), - root=node.root, - ) + yield node.new_child(element, i) diff --git a/jsonpath_rfc9535/selectors.py b/jsonpath_rfc9535/selectors.py index dac602e..184ef50 100644 --- a/jsonpath_rfc9535/selectors.py +++ b/jsonpath_rfc9535/selectors.py @@ -15,11 +15,11 @@ from .exceptions import JSONPathIndexError from .exceptions import JSONPathTypeError from .filter_expressions import FilterContext -from .node import JSONPathNode if TYPE_CHECKING: from .environment import JSONPathEnvironment from .filter_expressions import FilterExpression + from .node import JSONPathNode from .tokens import Token @@ -76,11 +76,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: """Select a value from a dict/object by its property/key.""" if isinstance(node.value, dict): with suppress(KeyError): - yield JSONPathNode( - value=node.value[self.name], - location=node.location.prepend(self.name), - root=node.root, - ) + yield node.new_child(node.value[self.name], self.name) class IndexSelector(JSONPathSelector): @@ -125,12 +121,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: if isinstance(node.value, list): norm_index = self._normalized_index(node.value) with suppress(IndexError): - _node = JSONPathNode( - value=node.value[self.index], - location=node.location.prepend(norm_index), - root=node.root, - ) - yield _node + yield node.new_child(node.value[self.index], norm_index) class SliceSelector(JSONPathSelector): @@ -185,13 +176,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: idx = self.slice.start or 0 step = self.slice.step or 1 for element in node.value[self.slice]: - norm_index = self._normalized_index(node.value, idx) - _node = JSONPathNode( - value=element, - location=node.location.prepend(norm_index), - root=node.root, - ) - yield _node + yield node.new_child(element, self._normalized_index(node.value, idx)) idx += step @@ -221,21 +206,11 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: members = node.value.items() for name, val in members: - _node = JSONPathNode( - value=val, - location=node.location.prepend(name), - root=node.root, - ) - yield _node + yield node.new_child(val, name) elif isinstance(node.value, list): for i, element in enumerate(node.value): - _node = JSONPathNode( - value=element, - location=node.location.prepend(i), - root=node.root, - ) - yield _node + yield node.new_child(element, i) class Filter(JSONPathSelector): @@ -284,11 +259,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: # noqa: PLR091 ) try: if self.expression.evaluate(context): - yield JSONPathNode( - value=val, - location=node.location.prepend(name), - root=node.root, - ) + yield node.new_child(val, name) except JSONPathTypeError as err: if not err.token: err.token = self.token @@ -303,11 +274,7 @@ def resolve(self, node: JSONPathNode) -> Iterable[JSONPathNode]: # noqa: PLR091 ) try: if self.expression.evaluate(context): - yield JSONPathNode( - value=element, - location=node.location.prepend(i), - root=node.root, - ) + yield node.new_child(element, i) except JSONPathTypeError as err: if not err.token: err.token = self.token diff --git a/tests/test_cts_nondeterminism.py b/tests/test_cts_nondeterminism.py index 91a4b55..0d97522 100644 --- a/tests/test_cts_nondeterminism.py +++ b/tests/test_cts_nondeterminism.py @@ -72,7 +72,7 @@ def _result_repr(rv: List[object]) -> Tuple[str, ...]: env = MockEnv() - # Repeat enough times to has high probability that we've covered all + # Repeat enough times so as to have high probability that we've covered all # valid permutations. results = { _result_repr(env.find(case.selector, case.document).values())