Skip to content

Commit dd91567

Browse files
committed
Tidy and an extra test case
1 parent 18789c4 commit dd91567

File tree

2 files changed

+105
-56
lines changed

2 files changed

+105
-56
lines changed

jsonpath/fluent_api.py

Lines changed: 55 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""A fluent API for managing JSONPathMatch iterators."""
1+
"""A fluent API for working with `JSONPathMatch` iterators."""
22

33
from __future__ import annotations
44

@@ -154,6 +154,41 @@ def pointers(self) -> Iterable[JSONPointer]:
154154
"""Return an iterable of JSONPointers, one for each match."""
155155
return (m.pointer() for m in self._it)
156156

157+
def first_one(self) -> Optional[JSONPathMatch]:
158+
"""Return the first `JSONPathMatch` or `None` if there were no matches."""
159+
try:
160+
return next(self._it)
161+
except StopIteration:
162+
return None
163+
164+
def one(self) -> Optional[JSONPathMatch]:
165+
"""Return the first `JSONPathMatch` or `None` if there were no matches.
166+
167+
`one()` is an alias for `first_one()`.
168+
"""
169+
return self.first_one()
170+
171+
def last_one(self) -> Optional[JSONPathMatch]:
172+
"""Return the last `JSONPathMatch` or `None` if there were no matches."""
173+
try:
174+
return next(iter(self.tail(1)))
175+
except StopIteration:
176+
return None
177+
178+
def tee(self, n: int = 2) -> Tuple[Query, ...]:
179+
"""Return _n_ independent queries by teeing this query's iterator.
180+
181+
It is not safe to use a `Query` instance after calling `tee()`.
182+
"""
183+
return tuple(Query(it, self._env) for it in itertools.tee(self._it, n))
184+
185+
def take(self, n: int) -> Query:
186+
"""Return a new query iterating over the next _n_ matches.
187+
188+
It is safe to continue using this query after calling take.
189+
"""
190+
return Query(list(itertools.islice(self._it, n)), self._env)
191+
157192
def select(
158193
self,
159194
*expressions: str,
@@ -199,7 +234,7 @@ def _select(
199234
for expr in expressions:
200235
self._patch(match, expr, patch, projection)
201236

202-
return _sparse_values(patch.apply(obj))
237+
return _fix_sparse_arrays(patch.apply(obj))
203238

204239
def _patch(
205240
self,
@@ -219,87 +254,51 @@ def _patch(
219254
str(p).replace("~", "~0").replace("/", "~1")
220255
for p in rel_match.parts
221256
)
222-
pointer = _patch_parents(root_pointer / rel_pointer, patch, match.root) # type: ignore
257+
pointer = root_pointer / rel_pointer
258+
_patch_parents(pointer.parent(), patch, match.root)
223259
patch.addap(pointer, rel_match.obj)
224260
else:
225261
# Natural projection
226-
pointer = _patch_parents(rel_match.pointer(), patch, match.obj) # type: ignore
262+
pointer = rel_match.pointer()
263+
_patch_parents(pointer.parent(), patch, match.obj) # type: ignore
227264
patch.addap(pointer, rel_match.obj)
228265

229-
def first_one(self) -> Optional[JSONPathMatch]:
230-
"""Return the first `JSONPathMatch` or `None` if there were no matches."""
231-
try:
232-
return next(self._it)
233-
except StopIteration:
234-
return None
235-
236-
def one(self) -> Optional[JSONPathMatch]:
237-
"""Return the first `JSONPathMatch` or `None` if there were no matches.
238-
239-
`one()` is an alias for `first_one()`.
240-
"""
241-
return self.first_one()
242-
243-
def last_one(self) -> Optional[JSONPathMatch]:
244-
"""Return the last `JSONPathMatch` or `None` if there were no matches."""
245-
try:
246-
return next(iter(self.tail(1)))
247-
except StopIteration:
248-
return None
249-
250-
def tee(self, n: int = 2) -> Tuple[Query, ...]:
251-
"""Return _n_ independent queries by teeing this query's iterator.
252-
253-
It is not safe to use a `Query` instance after calling `tee()`.
254-
"""
255-
return tuple(Query(it, self._env) for it in itertools.tee(self._it, n))
256-
257-
def take(self, n: int) -> Query:
258-
"""Return a new query iterating over the next _n_ matches.
259-
260-
It is safe to continue using this query after calling take.
261-
"""
262-
return Query(list(itertools.islice(self._it, n)), self._env)
263-
264266

265267
def _patch_parents(
266268
pointer: JSONPointer,
267269
patch: JSONPatch,
268270
obj: Union[Sequence[Any], Mapping[str, Any]],
269-
) -> JSONPointer:
270-
parent = pointer.parent()
271-
if parent.parent().parts:
272-
_patch_parents(parent, patch, obj)
271+
) -> None:
272+
if pointer.parent().parts:
273+
_patch_parents(pointer.parent(), patch, obj)
273274

274-
if parent.parts:
275+
if pointer.parts:
275276
try:
276-
_obj = parent.resolve(obj)
277+
_obj = pointer.resolve(obj)
277278
except JSONPointerKeyError:
278279
_obj = obj
279280

280-
# For lack of a better solution, we're patching arrays to dictionaries with
281-
# integer keys. This is to handle sparse array selections without having to
282-
# keep track of indexes and how they map from the root JSON value to the
283-
# selected JSON value.
281+
# For lack of a better idea, we're patching arrays to dictionaries with
282+
# integer keys. This is to handle sparse array selections without having
283+
# to keep track of indexes and how they map from the root JSON value to
284+
# the selected JSON value.
284285
#
285286
# We'll fix these "sparse arrays" after the patch has been applied.
286287
if isinstance(_obj, (Sequence, Mapping)) and not isinstance(_obj, str):
287-
patch.addne(parent, {})
288-
289-
return pointer
288+
patch.addne(pointer, {})
290289

291290

292-
def _sparse_values(obj: Any) -> object:
291+
def _fix_sparse_arrays(obj: Any) -> object:
293292
"""Fix sparse arrays (dictionaries with integer keys)."""
294293
if isinstance(obj, str) or not obj:
295294
return obj
296295

297296
if isinstance(obj, Sequence):
298-
return [_sparse_values(e) for e in obj]
297+
return [_fix_sparse_arrays(e) for e in obj]
299298

300299
if isinstance(obj, Mapping):
301300
if isinstance(next(iter(obj)), int):
302-
return [_sparse_values(v) for v in obj.values()]
303-
return {k: _sparse_values(v) for k, v in obj.items()}
301+
return [_fix_sparse_arrays(v) for v in obj.values()]
302+
return {k: _fix_sparse_arrays(v) for k, v in obj.items()}
304303

305304
return obj

tests/test_query_projection.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,53 @@ def test_select_nested_objects_flat_projection() -> None:
109109
projection = ("foo.bar",)
110110
it = jsonpath.query(expr, data).select(*projection, projection=Projection.FLAT)
111111
assert list(it) == [[42]]
112+
113+
114+
def test_sparse_array_selection() -> None:
115+
expr = "$..products[[email protected]]"
116+
data = {
117+
"categories": [
118+
{
119+
"name": "footwear",
120+
"products": [
121+
{
122+
"title": "Trainers",
123+
"description": "Fashionable trainers.",
124+
"price": 89.99,
125+
},
126+
{
127+
"title": "Barefoot Trainers",
128+
"description": "Running trainers.",
129+
"price": 130.00,
130+
},
131+
],
132+
},
133+
{
134+
"name": "headwear",
135+
"products": [
136+
{
137+
"title": "Cap",
138+
"description": "Baseball cap",
139+
"price": 15.00,
140+
},
141+
{
142+
"title": "Beanie",
143+
"description": "Winter running hat.",
144+
"price": 9.00,
145+
"social": {"likes": 12, "shares": 7},
146+
},
147+
],
148+
},
149+
],
150+
"price_cap": 10,
151+
}
152+
153+
it = jsonpath.query(expr, data).select(
154+
"title",
155+
"social.shares",
156+
projection=Projection.ROOT,
157+
)
158+
159+
assert list(it) == [
160+
{"categories": [{"products": [{"title": "Beanie", "social": {"shares": 7}}]}]}
161+
]

0 commit comments

Comments
 (0)