Skip to content

Commit 105dcf0

Browse files
committed
Add simple query projection
1 parent 91e2f12 commit 105dcf0

File tree

3 files changed

+127
-4
lines changed

3 files changed

+127
-4
lines changed

jsonpath/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ def query(
353353
...
354354
```
355355
"""
356-
return Query(self.finditer(path, data, filter_context=filter_context))
356+
return Query(self.finditer(path, data, filter_context=filter_context), self)
357357

358358
async def findall_async(
359359
self,

jsonpath/fluent_api.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,22 @@
55
import collections
66
import itertools
77
from typing import TYPE_CHECKING
8+
from typing import Any
9+
from typing import Dict
810
from typing import Iterable
911
from typing import Iterator
12+
from typing import List
13+
from typing import Mapping
1014
from typing import Optional
15+
from typing import Sequence
1116
from typing import Tuple
17+
from typing import Union
18+
19+
from .exceptions import JSONPointerKeyError
20+
from .patch import JSONPatch
1221

1322
if TYPE_CHECKING:
23+
from jsonpath import JSONPathEnvironment
1424
from jsonpath import JSONPathMatch
1525
from jsonpath import JSONPointer
1626

@@ -28,8 +38,9 @@ class Query:
2838
**New in version 1.1.0**
2939
"""
3040

31-
def __init__(self, it: Iterable[JSONPathMatch]) -> None:
41+
def __init__(self, it: Iterable[JSONPathMatch], env: JSONPathEnvironment) -> None:
3242
self._it = iter(it)
43+
self._env = env
3344

3445
def __iter__(self) -> Iterator[JSONPathMatch]:
3546
return self._it
@@ -126,6 +137,31 @@ def pointers(self) -> Iterable[JSONPointer]:
126137
"""Return an iterable of JSONPointers, one for each match."""
127138
return (m.pointer() for m in self._it)
128139

140+
def select(self, *expressions: str) -> Iterable[object]:
141+
"""Query projection using relative JSONPaths.
142+
143+
Returns an iterable of objects built from selecting _expressions_ relative to
144+
each match from the current query.
145+
"""
146+
for m in self._it:
147+
if isinstance(m.obj, Sequence):
148+
obj: Union[List[Any], Dict[str, Any]] = []
149+
elif isinstance(m.obj, Mapping):
150+
obj = {}
151+
else:
152+
return iter([])
153+
154+
patch = JSONPatch()
155+
156+
for expr in expressions:
157+
for match in self._env.finditer(expr, m.obj): # type: ignore
158+
_pointer = match.pointer()
159+
_patch_parents(_pointer.parent(), patch, m.obj) # type: ignore
160+
patch.add(_pointer, match.obj)
161+
162+
patch.apply(obj)
163+
yield obj
164+
129165
def first_one(self) -> Optional[JSONPathMatch]:
130166
"""Return the first `JSONPathMatch` or `None` if there were no matches."""
131167
try:
@@ -152,11 +188,31 @@ def tee(self, n: int = 2) -> Tuple[Query, ...]:
152188
153189
It is not safe to use a `Query` instance after calling `tee()`.
154190
"""
155-
return tuple(Query(it) for it in itertools.tee(self._it, n))
191+
return tuple(Query(it, self._env) for it in itertools.tee(self._it, n))
156192

157193
def take(self, n: int) -> Query:
158194
"""Return a new query iterating over the next _n_ matches.
159195
160196
It is safe to continue using this query after calling take.
161197
"""
162-
return Query(list(itertools.islice(self._it, n)))
198+
return Query(list(itertools.islice(self._it, n)), self._env)
199+
200+
201+
def _patch_parents(
202+
pointer: JSONPointer,
203+
patch: JSONPatch,
204+
obj: Union[Sequence[Any], Mapping[str, Any]],
205+
) -> None:
206+
if pointer.parent().parts:
207+
_patch_parents(pointer.parent(), patch, obj)
208+
209+
try:
210+
_obj = pointer.resolve(obj)
211+
except JSONPointerKeyError:
212+
_obj = obj
213+
214+
if pointer.parts:
215+
if isinstance(_obj, Sequence):
216+
patch.addne(pointer, [])
217+
elif isinstance(_obj, Mapping):
218+
patch.addne(pointer, {})

jsonpath/patch.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,52 @@ def asdict(self) -> Dict[str, object]:
8585
return {"op": self.name, "path": str(self.path), "value": self.value}
8686

8787

88+
class OpAddNe(Op):
89+
"""A non-standard _add if not exists_ operation."""
90+
91+
__slots__ = ("path", "value")
92+
93+
name = "add"
94+
95+
def __init__(self, path: JSONPointer, value: object) -> None:
96+
self.path = path
97+
self.value = value
98+
99+
def apply(
100+
self, data: Union[MutableSequence[object], MutableMapping[str, object]]
101+
) -> Union[MutableSequence[object], MutableMapping[str, object]]:
102+
"""Apply this patch operation to _data_."""
103+
parent, obj = self.path.resolve_parent(data)
104+
if parent is None:
105+
# Replace the root object.
106+
# The following op, if any, will raise a JSONPatchError if needed.
107+
return self.value # type: ignore
108+
109+
target = self.path.parts[-1]
110+
if isinstance(parent, MutableSequence):
111+
if obj is UNDEFINED:
112+
if target == "-":
113+
parent.append(self.value)
114+
else:
115+
raise JSONPatchError("index out of range")
116+
else:
117+
parent.insert(int(target), self.value)
118+
elif (
119+
isinstance(parent, MutableMapping)
120+
and parent.get(target, UNDEFINED) == UNDEFINED
121+
):
122+
parent[target] = self.value
123+
# else:
124+
# raise JSONPatchError(
125+
# f"unexpected operation on {parent.__class__.__name__!r}"
126+
# )
127+
return data
128+
129+
def asdict(self) -> Dict[str, object]:
130+
"""Return a dictionary representation of this operation."""
131+
return {"op": self.name, "path": str(self.path), "value": self.value}
132+
133+
88134
class OpRemove(Op):
89135
"""The JSON Patch _remove_ operation."""
90136

@@ -340,6 +386,11 @@ def _build(self, patch: Iterable[Mapping[str, object]]) -> None:
340386
path=self._op_pointer(operation, "path", "add", i),
341387
value=self._op_value(operation, "value", "add", i),
342388
)
389+
elif op == "addne":
390+
self.addne(
391+
path=self._op_pointer(operation, "path", "add", i),
392+
value=self._op_value(operation, "value", "add", i),
393+
)
343394
elif op == "remove":
344395
self.remove(path=self._op_pointer(operation, "path", "add", i))
345396
elif op == "replace":
@@ -424,6 +475,22 @@ def add(self: Self, path: Union[str, JSONPointer], value: object) -> Self:
424475
self.ops.append(OpAdd(path=pointer, value=value))
425476
return self
426477

478+
def addne(self: Self, path: Union[str, JSONPointer], value: object) -> Self:
479+
"""Append an _addne_ operation to this patch.
480+
481+
Arguments:
482+
path: A string representation of a JSON Pointer, or one that has
483+
already been parsed.
484+
value: The object to add.
485+
486+
Returns:
487+
This `JSONPatch` instance, so we can build a JSON Patch by chaining
488+
calls to JSON Patch operation methods.
489+
"""
490+
pointer = self._ensure_pointer(path)
491+
self.ops.append(OpAddNe(path=pointer, value=value))
492+
return self
493+
427494
def remove(self: Self, path: Union[str, JSONPointer]) -> Self:
428495
"""Append a _remove_ operation to this patch.
429496

0 commit comments

Comments
 (0)