python-jsonpath 1.3.1__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jsonpath/selectors.py CHANGED
@@ -11,23 +11,22 @@ from typing import TYPE_CHECKING
11
11
  from typing import Any
12
12
  from typing import AsyncIterable
13
13
  from typing import Iterable
14
- from typing import List
15
14
  from typing import Optional
16
- from typing import TypeVar
17
15
  from typing import Union
18
16
 
19
17
  from .exceptions import JSONPathIndexError
18
+ from .exceptions import JSONPathSyntaxError
20
19
  from .exceptions import JSONPathTypeError
20
+ from .match import NodeList
21
21
  from .serialize import canonical_string
22
22
 
23
23
  if TYPE_CHECKING:
24
24
  from .env import JSONPathEnvironment
25
- from .filter import BooleanExpression
25
+ from .filter import FilterExpression
26
26
  from .match import JSONPathMatch
27
+ from .path import JSONPath
27
28
  from .token import Token
28
29
 
29
- # ruff: noqa: D102
30
-
31
30
 
32
31
  class JSONPathSelector(ABC):
33
32
  """Base class for all JSONPath segments and selectors."""
@@ -39,13 +38,11 @@ class JSONPathSelector(ABC):
39
38
  self.token = token
40
39
 
41
40
  @abstractmethod
42
- def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
41
+ def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
43
42
  """Apply the segment/selector to each node in _matches_.
44
43
 
45
44
  Arguments:
46
- matches: Nodes matched by preceding segments/selectors. This is like
47
- a lazy _NodeList_, as described in RFC 9535, but each match carries
48
- more than the node's value and location.
45
+ node: A node matched by preceding segments/selectors.
49
46
 
50
47
  Returns:
51
48
  The `JSONPathMatch` instances created by applying this selector to each
@@ -53,39 +50,25 @@ class JSONPathSelector(ABC):
53
50
  """
54
51
 
55
52
  @abstractmethod
56
- def resolve_async(
57
- self, matches: AsyncIterable[JSONPathMatch]
58
- ) -> AsyncIterable[JSONPathMatch]:
53
+ def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
59
54
  """An async version of `resolve`."""
60
55
 
61
56
 
62
- class PropertySelector(JSONPathSelector):
63
- """A shorthand or bracketed property selector."""
57
+ class NameSelector(JSONPathSelector):
58
+ """Select at most one object member value given an object member name."""
64
59
 
65
- __slots__ = ("name", "shorthand")
60
+ __slots__ = ("name",)
66
61
 
67
- def __init__(
68
- self,
69
- *,
70
- env: JSONPathEnvironment,
71
- token: Token,
72
- name: str,
73
- shorthand: bool,
74
- ) -> None:
62
+ def __init__(self, *, env: JSONPathEnvironment, token: Token, name: str) -> None:
75
63
  super().__init__(env=env, token=token)
76
64
  self.name = name
77
- self.shorthand = shorthand
78
65
 
79
66
  def __str__(self) -> str:
80
- return (
81
- f"[{canonical_string(self.name)}]"
82
- if self.shorthand
83
- else f"{canonical_string(self.name)}"
84
- )
67
+ return canonical_string(self.name)
85
68
 
86
69
  def __eq__(self, __value: object) -> bool:
87
70
  return (
88
- isinstance(__value, PropertySelector)
71
+ isinstance(__value, NameSelector)
89
72
  and self.name == __value.name
90
73
  and self.token == __value.token
91
74
  )
@@ -93,50 +76,25 @@ class PropertySelector(JSONPathSelector):
93
76
  def __hash__(self) -> int:
94
77
  return hash((self.name, self.token))
95
78
 
96
- def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
97
- for match in matches:
98
- if not isinstance(match.obj, Mapping):
99
- continue
100
-
79
+ def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
80
+ if isinstance(node.obj, Mapping):
101
81
  with suppress(KeyError):
102
- _match = self.env.match_class(
103
- filter_context=match.filter_context(),
104
- obj=self.env.getitem(match.obj, self.name),
105
- parent=match,
106
- parts=match.parts + (self.name,),
107
- path=match.path + f"[{canonical_string(self.name)}]",
108
- root=match.root,
109
- )
110
- match.add_child(_match)
111
- yield _match
112
-
113
- async def resolve_async(
114
- self, matches: AsyncIterable[JSONPathMatch]
115
- ) -> AsyncIterable[JSONPathMatch]:
116
- async for match in matches:
117
- if not isinstance(match.obj, Mapping):
118
- continue
82
+ match = node.new_child(self.env.getitem(node.obj, self.name), self.name)
83
+ node.add_child(match)
84
+ yield match
119
85
 
86
+ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
87
+ if isinstance(node.obj, Mapping):
120
88
  with suppress(KeyError):
121
- _match = self.env.match_class(
122
- filter_context=match.filter_context(),
123
- obj=await self.env.getitem_async(match.obj, self.name),
124
- parent=match,
125
- parts=match.parts + (self.name,),
126
- path=match.path + f"[{canonical_string(self.name)}]",
127
- root=match.root,
89
+ match = node.new_child(
90
+ await self.env.getitem_async(node.obj, self.name), self.name
128
91
  )
129
- match.add_child(_match)
130
- yield _match
92
+ node.add_child(match)
93
+ yield match
131
94
 
132
95
 
133
96
  class IndexSelector(JSONPathSelector):
134
- """Select an element from an array by index.
135
-
136
- Considering we don't require mapping (JSON object) keys/properties to
137
- be quoted, and that we support mappings with numeric keys, we also check
138
- to see if the "index" is a mapping key, which is non-standard.
139
- """
97
+ """Select at most one array element value given an index."""
140
98
 
141
99
  __slots__ = ("index", "_as_key")
142
100
 
@@ -172,122 +130,139 @@ class IndexSelector(JSONPathSelector):
172
130
  return len(obj) + self.index
173
131
  return self.index
174
132
 
175
- def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
176
- for match in matches:
177
- if isinstance(match.obj, Mapping):
178
- # Try the string representation of the index as a key.
179
- with suppress(KeyError):
180
- _match = self.env.match_class(
181
- filter_context=match.filter_context(),
182
- obj=self.env.getitem(match.obj, self._as_key),
183
- parent=match,
184
- parts=match.parts + (self._as_key,),
185
- path=f"{match.path}['{self.index}']",
186
- root=match.root,
187
- )
188
- match.add_child(_match)
189
- yield _match
190
- elif isinstance(match.obj, Sequence) and not isinstance(match.obj, str):
191
- norm_index = self._normalized_index(match.obj)
192
- with suppress(IndexError):
193
- _match = self.env.match_class(
194
- filter_context=match.filter_context(),
195
- obj=self.env.getitem(match.obj, self.index),
196
- parent=match,
197
- parts=match.parts + (norm_index,),
198
- path=match.path + f"[{norm_index}]",
199
- root=match.root,
200
- )
201
- match.add_child(_match)
202
- yield _match
203
-
204
- async def resolve_async(
205
- self, matches: AsyncIterable[JSONPathMatch]
206
- ) -> AsyncIterable[JSONPathMatch]:
207
- async for match in matches:
208
- if isinstance(match.obj, Mapping):
209
- # Try the string representation of the index as a key.
210
- with suppress(KeyError):
211
- _match = self.env.match_class(
212
- filter_context=match.filter_context(),
213
- obj=await self.env.getitem_async(match.obj, self._as_key),
214
- parent=match,
215
- parts=match.parts + (self._as_key,),
216
- path=f"{match.path}['{self.index}']",
217
- root=match.root,
218
- )
219
- match.add_child(_match)
220
- yield _match
221
- elif isinstance(match.obj, Sequence) and not isinstance(match.obj, str):
222
- norm_index = self._normalized_index(match.obj)
223
- with suppress(IndexError):
224
- _match = self.env.match_class(
225
- filter_context=match.filter_context(),
226
- obj=await self.env.getitem_async(match.obj, self.index),
227
- parent=match,
228
- parts=match.parts + (norm_index,),
229
- path=match.path + f"[{norm_index}]",
230
- root=match.root,
231
- )
232
- match.add_child(_match)
233
- yield _match
133
+ def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
134
+ # Optionally try string representation of int
135
+ if not self.env.strict and isinstance(node.obj, Mapping):
136
+ # Try the string representation of the index as a key.
137
+ with suppress(KeyError):
138
+ match = node.new_child(
139
+ self.env.getitem(node.obj, self._as_key), self.index
140
+ )
141
+ node.add_child(match)
142
+ yield match
143
+ if isinstance(node.obj, Sequence) and not isinstance(node.obj, str):
144
+ norm_index = self._normalized_index(node.obj)
145
+ with suppress(IndexError):
146
+ match = node.new_child(
147
+ self.env.getitem(node.obj, self.index), norm_index
148
+ )
149
+ node.add_child(match)
150
+ yield match
151
+
152
+ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
153
+ if not self.env.strict and isinstance(node.obj, Mapping):
154
+ # Try the string representation of the index as a key.
155
+ with suppress(KeyError):
156
+ match = node.new_child(
157
+ await self.env.getitem_async(node.obj, self._as_key), self.index
158
+ )
159
+ node.add_child(match)
160
+ yield match
161
+ if isinstance(node.obj, Sequence) and not isinstance(node.obj, str):
162
+ norm_index = self._normalized_index(node.obj)
163
+ with suppress(IndexError):
164
+ match = node.new_child(
165
+ await self.env.getitem_async(node.obj, self.index), norm_index
166
+ )
167
+ node.add_child(match)
168
+ yield match
234
169
 
235
170
 
236
- class KeysSelector(JSONPathSelector):
237
- """Select mapping/object keys/properties.
171
+ class KeySelector(JSONPathSelector):
172
+ """Select at most one name from an object member, given the name.
173
+
174
+ The key selector is introduced to facilitate valid normalized paths for nodes
175
+ produced by the "keys selector" and the "keys filter selector". It is not expected
176
+ to be of much use elsewhere.
238
177
 
239
178
  NOTE: This is a non-standard selector.
179
+
180
+ See https://jg-rp.github.io/json-p3/guides/jsonpath-extra#key-selector.
240
181
  """
241
182
 
242
- __slots__ = ("shorthand",)
183
+ __slots__ = ("key",)
243
184
 
244
- def __init__(
245
- self, *, env: JSONPathEnvironment, token: Token, shorthand: bool
246
- ) -> None:
185
+ def __init__(self, *, env: JSONPathEnvironment, token: Token, key: str) -> None:
247
186
  super().__init__(env=env, token=token)
248
- self.shorthand = shorthand
187
+ self.key = key
249
188
 
250
189
  def __str__(self) -> str:
190
+ return f"{self.env.keys_selector_token}{canonical_string(self.key)}"
191
+
192
+ def __eq__(self, __value: object) -> bool:
251
193
  return (
252
- f"[{self.env.keys_selector_token}]"
253
- if self.shorthand
254
- else self.env.keys_selector_token
194
+ isinstance(__value, KeySelector)
195
+ and self.token == __value.token
196
+ and self.key == __value.key
255
197
  )
256
198
 
199
+ def __hash__(self) -> int:
200
+ return hash((self.token, self.key))
201
+
202
+ def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
203
+ if isinstance(node.obj, Mapping) and self.key in node.obj:
204
+ match = node.__class__(
205
+ filter_context=node.filter_context(),
206
+ obj=self.key,
207
+ parent=node,
208
+ parts=node.parts + (f"{self.env.keys_selector_token}{self.key}",),
209
+ path=f"{node.path}[{self}]",
210
+ root=node.root,
211
+ )
212
+ node.add_child(match)
213
+ yield match
214
+
215
+ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
216
+ for _node in self.resolve(node):
217
+ yield _node
218
+
219
+
220
+ class KeysSelector(JSONPathSelector):
221
+ """Select all names from an object's name/value members.
222
+
223
+ NOTE: This is a non-standard selector.
224
+
225
+ See https://jg-rp.github.io/json-p3/guides/jsonpath-extra#keys-selector
226
+ """
227
+
228
+ __slots__ = ()
229
+
230
+ def __init__(self, *, env: JSONPathEnvironment, token: Token) -> None:
231
+ super().__init__(env=env, token=token)
232
+
233
+ def __str__(self) -> str:
234
+ return self.env.keys_selector_token
235
+
257
236
  def __eq__(self, __value: object) -> bool:
258
237
  return isinstance(__value, KeysSelector) and self.token == __value.token
259
238
 
260
239
  def __hash__(self) -> int:
261
240
  return hash(self.token)
262
241
 
263
- def _keys(self, match: JSONPathMatch) -> Iterable[JSONPathMatch]:
264
- if isinstance(match.obj, Mapping):
265
- for i, key in enumerate(match.obj.keys()):
266
- _match = self.env.match_class(
267
- filter_context=match.filter_context(),
242
+ def _keys(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
243
+ if isinstance(node.obj, Mapping):
244
+ for key in node.obj:
245
+ match = node.__class__(
246
+ filter_context=node.filter_context(),
268
247
  obj=key,
269
- parent=match,
270
- parts=match.parts + (f"{self.env.keys_selector_token}{key}",),
271
- path=f"{match.path}[{self.env.keys_selector_token}][{i}]",
272
- root=match.root,
248
+ parent=node,
249
+ parts=node.parts + (f"{self.env.keys_selector_token}{key}",),
250
+ path=f"{node.path}[{self.env.keys_selector_token}{canonical_string(key)}]",
251
+ root=node.root,
273
252
  )
274
- match.add_child(_match)
275
- yield _match
253
+ node.add_child(match)
254
+ yield match
276
255
 
277
- def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
278
- for match in matches:
279
- yield from self._keys(match)
256
+ def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
257
+ yield from self._keys(node)
280
258
 
281
- async def resolve_async(
282
- self, matches: AsyncIterable[JSONPathMatch]
283
- ) -> AsyncIterable[JSONPathMatch]:
284
- async for match in matches:
285
- for _match in self._keys(match):
286
- yield _match
259
+ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
260
+ for match in self._keys(node):
261
+ yield match
287
262
 
288
263
 
289
264
  class SliceSelector(JSONPathSelector):
290
- """Sequence slicing selector."""
265
+ """Select array elements given a start index, a stop index and a step."""
291
266
 
292
267
  __slots__ = ("slice",)
293
268
 
@@ -327,258 +302,191 @@ class SliceSelector(JSONPathSelector):
327
302
  ):
328
303
  raise JSONPathIndexError("index out of range", token=self.token)
329
304
 
330
- def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
331
- for match in matches:
332
- if not isinstance(match.obj, Sequence) or self.slice.step == 0:
333
- continue
305
+ def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
306
+ if not isinstance(node.obj, Sequence) or self.slice.step == 0:
307
+ return
334
308
 
335
- for norm_index, obj in zip( # noqa: B905
336
- range(*self.slice.indices(len(match.obj))),
337
- self.env.getitem(match.obj, self.slice),
338
- ):
339
- _match = self.env.match_class(
340
- filter_context=match.filter_context(),
341
- obj=obj,
342
- parent=match,
343
- parts=match.parts + (norm_index,),
344
- path=f"{match.path}[{norm_index}]",
345
- root=match.root,
346
- )
347
- match.add_child(_match)
348
- yield _match
349
-
350
- async def resolve_async(
351
- self, matches: AsyncIterable[JSONPathMatch]
352
- ) -> AsyncIterable[JSONPathMatch]:
353
- async for match in matches:
354
- if not isinstance(match.obj, Sequence) or self.slice.step == 0:
355
- continue
356
-
357
- for norm_index, obj in zip( # noqa: B905
358
- range(*self.slice.indices(len(match.obj))),
359
- await self.env.getitem_async(match.obj, self.slice),
360
- ):
361
- _match = self.env.match_class(
362
- filter_context=match.filter_context(),
363
- obj=obj,
364
- parent=match,
365
- parts=match.parts + (norm_index,),
366
- path=f"{match.path}[{norm_index}]",
367
- root=match.root,
368
- )
369
- match.add_child(_match)
370
- yield _match
309
+ for norm_index, obj in zip( # noqa: B905
310
+ range(*self.slice.indices(len(node.obj))),
311
+ self.env.getitem(node.obj, self.slice),
312
+ ):
313
+ match = node.new_child(obj, norm_index)
314
+ node.add_child(match)
315
+ yield match
371
316
 
317
+ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
318
+ if not isinstance(node.obj, Sequence) or self.slice.step == 0:
319
+ return
372
320
 
373
- class WildSelector(JSONPathSelector):
374
- """Select all items from a sequence/array or values from a mapping/object."""
321
+ for norm_index, obj in zip( # noqa: B905
322
+ range(*self.slice.indices(len(node.obj))),
323
+ await self.env.getitem_async(node.obj, self.slice),
324
+ ):
325
+ match = node.new_child(obj, norm_index)
326
+ node.add_child(match)
327
+ yield match
375
328
 
376
- __slots__ = ("shorthand",)
377
329
 
378
- def __init__(
379
- self, *, env: JSONPathEnvironment, token: Token, shorthand: bool
380
- ) -> None:
381
- super().__init__(env=env, token=token)
382
- self.shorthand = shorthand
330
+ class WildcardSelector(JSONPathSelector):
331
+ """Select nodes of all children of an object or array."""
332
+
333
+ __slots__ = ()
383
334
 
384
335
  def __str__(self) -> str:
385
- return "[*]" if self.shorthand else "*"
336
+ return "*"
386
337
 
387
338
  def __eq__(self, __value: object) -> bool:
388
- return isinstance(__value, WildSelector) and self.token == __value.token
339
+ return isinstance(__value, WildcardSelector) and self.token == __value.token
389
340
 
390
341
  def __hash__(self) -> int:
391
342
  return hash(self.token)
392
343
 
393
- def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
394
- for match in matches:
395
- if isinstance(match.obj, str):
396
- continue
397
- if isinstance(match.obj, Mapping):
398
- for key, val in match.obj.items():
399
- _match = self.env.match_class(
400
- filter_context=match.filter_context(),
401
- obj=val,
402
- parent=match,
403
- parts=match.parts + (key,),
404
- path=match.path + f"[{canonical_string(key)}]",
405
- root=match.root,
406
- )
407
- match.add_child(_match)
408
- yield _match
409
- elif isinstance(match.obj, Sequence):
410
- for i, val in enumerate(match.obj):
411
- _match = self.env.match_class(
412
- filter_context=match.filter_context(),
413
- obj=val,
414
- parent=match,
415
- parts=match.parts + (i,),
416
- path=f"{match.path}[{i}]",
417
- root=match.root,
418
- )
419
- match.add_child(_match)
420
- yield _match
421
-
422
- async def resolve_async(
423
- self, matches: AsyncIterable[JSONPathMatch]
424
- ) -> AsyncIterable[JSONPathMatch]:
425
- async for match in matches:
426
- if isinstance(match.obj, Mapping):
427
- for key, val in match.obj.items():
428
- _match = self.env.match_class(
429
- filter_context=match.filter_context(),
430
- obj=val,
431
- parent=match,
432
- parts=match.parts + (key,),
433
- path=match.path + f"[{canonical_string(key)}]",
434
- root=match.root,
435
- )
436
- match.add_child(_match)
437
- yield _match
438
- elif isinstance(match.obj, Sequence):
439
- for i, val in enumerate(match.obj):
440
- _match = self.env.match_class(
441
- filter_context=match.filter_context(),
442
- obj=val,
443
- parent=match,
444
- parts=match.parts + (i,),
445
- path=f"{match.path}[{i}]",
446
- root=match.root,
447
- )
448
- match.add_child(_match)
449
- yield _match
450
-
451
-
452
- class RecursiveDescentSelector(JSONPathSelector):
453
- """A JSONPath selector that visits all nodes recursively.
454
-
455
- NOTE: Strictly this is a "segment", not a "selector".
344
+ def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
345
+ if isinstance(node.obj, Mapping):
346
+ for key, val in node.obj.items():
347
+ match = node.new_child(val, key)
348
+ node.add_child(match)
349
+ yield match
350
+
351
+ elif isinstance(node.obj, Sequence) and not isinstance(node.obj, str):
352
+ for i, val in enumerate(node.obj):
353
+ match = node.new_child(val, i)
354
+ node.add_child(match)
355
+ yield match
356
+
357
+ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
358
+ if isinstance(node.obj, Mapping):
359
+ for key, val in node.obj.items():
360
+ match = node.new_child(val, key)
361
+ node.add_child(match)
362
+ yield match
363
+
364
+ elif isinstance(node.obj, Sequence) and not isinstance(node.obj, str):
365
+ for i, val in enumerate(node.obj):
366
+ match = node.new_child(val, i)
367
+ node.add_child(match)
368
+ yield match
369
+
370
+
371
+ class SingularQuerySelector(JSONPathSelector):
372
+ """An embedded absolute query.
373
+
374
+ The result of the embedded query is used as an object member name or array element
375
+ index.
376
+
377
+ NOTE: This is a non-standard selector.
456
378
  """
457
379
 
380
+ __slots__ = ("query",)
381
+
382
+ def __init__(
383
+ self, *, env: JSONPathEnvironment, token: Token, query: JSONPath
384
+ ) -> None:
385
+ super().__init__(env=env, token=token)
386
+ self.query = query
387
+
388
+ if env.strict:
389
+ raise JSONPathSyntaxError("unexpected query selector", token=token)
390
+
458
391
  def __str__(self) -> str:
459
- return ".."
392
+ return str(self.query)
460
393
 
461
394
  def __eq__(self, __value: object) -> bool:
462
395
  return (
463
- isinstance(__value, RecursiveDescentSelector)
396
+ isinstance(__value, SingularQuerySelector)
397
+ and self.query == __value.query
464
398
  and self.token == __value.token
465
399
  )
466
400
 
467
401
  def __hash__(self) -> int:
468
- return hash(self.token)
402
+ return hash((self.query, self.token))
469
403
 
470
- def _expand(self, match: JSONPathMatch) -> Iterable[JSONPathMatch]:
471
- if isinstance(match.obj, Mapping):
472
- for key, val in match.obj.items():
473
- if isinstance(val, str):
474
- pass
475
- elif isinstance(val, (Mapping, Sequence)):
476
- _match = self.env.match_class(
477
- filter_context=match.filter_context(),
478
- obj=val,
479
- parent=match,
480
- parts=match.parts + (key,),
481
- path=match.path + f"[{canonical_string(key)}]",
482
- root=match.root,
483
- )
484
- match.add_child(_match)
485
- yield _match
486
- yield from self._expand(_match)
487
- elif isinstance(match.obj, Sequence) and not isinstance(match.obj, str):
488
- for i, val in enumerate(match.obj):
489
- if isinstance(val, str):
490
- pass
491
- elif isinstance(val, (Mapping, Sequence)):
492
- _match = self.env.match_class(
493
- filter_context=match.filter_context(),
494
- obj=val,
495
- parent=match,
496
- parts=match.parts + (i,),
497
- path=f"{match.path}[{i}]",
498
- root=match.root,
499
- )
500
- match.add_child(_match)
501
- yield _match
502
- yield from self._expand(_match)
503
-
504
- def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
505
- for match in matches:
506
- yield match
507
- yield from self._expand(match)
404
+ def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
405
+ if isinstance(node.obj, Mapping):
406
+ nodes = NodeList(self.query.finditer(node.root))
508
407
 
509
- async def resolve_async(
510
- self, matches: AsyncIterable[JSONPathMatch]
511
- ) -> AsyncIterable[JSONPathMatch]:
512
- async for match in matches:
513
- yield match
514
- for _match in self._expand(match):
515
- yield _match
408
+ if nodes.empty():
409
+ return
516
410
 
411
+ value = nodes[0].value
517
412
 
518
- T = TypeVar("T")
413
+ if not isinstance(value, str):
414
+ return
519
415
 
416
+ with suppress(KeyError):
417
+ match = node.new_child(self.env.getitem(node.obj, value), value)
418
+ node.add_child(match)
419
+ yield match
520
420
 
521
- async def _alist(it: List[T]) -> AsyncIterable[T]:
522
- for item in it:
523
- yield item
421
+ if isinstance(node.obj, Sequence) and not isinstance(node.obj, str):
422
+ nodes = NodeList(self.query.finditer(node.root))
524
423
 
424
+ if nodes.empty():
425
+ return
525
426
 
526
- class ListSelector(JSONPathSelector):
527
- """A bracketed list of selectors, the results of which are concatenated together.
427
+ value = nodes[0].value
528
428
 
529
- NOTE: Strictly this is a "segment", not a "selector".
530
- """
429
+ if not isinstance(value, int):
430
+ return
531
431
 
532
- __slots__ = ("items",)
432
+ index = self._normalized_index(node.obj, value)
533
433
 
534
- def __init__(
535
- self,
536
- *,
537
- env: JSONPathEnvironment,
538
- token: Token,
539
- items: List[
540
- Union[
541
- SliceSelector,
542
- KeysSelector,
543
- IndexSelector,
544
- PropertySelector,
545
- WildSelector,
546
- Filter,
547
- ]
548
- ],
549
- ) -> None:
550
- super().__init__(env=env, token=token)
551
- self.items = tuple(items)
434
+ with suppress(IndexError):
435
+ match = node.new_child(self.env.getitem(node.obj, index), index)
436
+ node.add_child(match)
437
+ yield match
552
438
 
553
- def __str__(self) -> str:
554
- return f"[{', '.join(str(itm) for itm in self.items)}]"
439
+ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
440
+ if isinstance(node.obj, Mapping):
441
+ nodes = NodeList(
442
+ [match async for match in await self.query.finditer_async(node.root)]
443
+ )
555
444
 
556
- def __eq__(self, __value: object) -> bool:
557
- return (
558
- isinstance(__value, ListSelector)
559
- and self.items == __value.items
560
- and self.token == __value.token
561
- )
445
+ if nodes.empty():
446
+ return
562
447
 
563
- def __hash__(self) -> int:
564
- return hash((self.items, self.token))
448
+ value = nodes[0].value
449
+
450
+ if not isinstance(value, str):
451
+ return
452
+
453
+ with suppress(KeyError):
454
+ match = node.new_child(
455
+ await self.env.getitem_async(node.obj, value), value
456
+ )
457
+ node.add_child(match)
458
+ yield match
459
+
460
+ if isinstance(node.obj, Sequence) and not isinstance(node.obj, str):
461
+ nodes = NodeList(
462
+ [match async for match in await self.query.finditer_async(node.root)]
463
+ )
565
464
 
566
- def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
567
- for match_ in matches:
568
- for item in self.items:
569
- yield from item.resolve([match_])
465
+ if nodes.empty():
466
+ return
570
467
 
571
- async def resolve_async(
572
- self, matches: AsyncIterable[JSONPathMatch]
573
- ) -> AsyncIterable[JSONPathMatch]:
574
- async for match_ in matches:
575
- for item in self.items:
576
- async for m in item.resolve_async(_alist([match_])):
577
- yield m
468
+ value = nodes[0].value
469
+
470
+ if not isinstance(value, int):
471
+ return
472
+
473
+ index = self._normalized_index(node.obj, value)
474
+
475
+ with suppress(IndexError):
476
+ match = node.new_child(
477
+ await self.env.getitem_async(node.obj, index), index
478
+ )
479
+ node.add_child(match)
480
+ yield match
481
+
482
+ def _normalized_index(self, obj: Sequence[object], index: int) -> int:
483
+ if index < 0 and len(obj) >= abs(index):
484
+ return len(obj) + index
485
+ return index
578
486
 
579
487
 
580
488
  class Filter(JSONPathSelector):
581
- """Filter sequence/array items or mapping/object values with a filter expression."""
489
+ """Select array elements or object values according to a filter expression."""
582
490
 
583
491
  __slots__ = ("expression", "cacheable_nodes")
584
492
 
@@ -587,7 +495,7 @@ class Filter(JSONPathSelector):
587
495
  *,
588
496
  env: JSONPathEnvironment,
589
497
  token: Token,
590
- expression: BooleanExpression,
498
+ expression: FilterExpression,
591
499
  ) -> None:
592
500
  super().__init__(env=env, token=token)
593
501
  self.expression = expression
@@ -607,132 +515,190 @@ class Filter(JSONPathSelector):
607
515
  def __hash__(self) -> int:
608
516
  return hash((str(self.expression), self.token))
609
517
 
610
- def resolve( # noqa: PLR0912
611
- self, matches: Iterable[JSONPathMatch]
612
- ) -> Iterable[JSONPathMatch]:
518
+ def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
613
519
  if self.cacheable_nodes and self.env.filter_caching:
614
520
  expr = self.expression.cache_tree()
615
521
  else:
616
522
  expr = self.expression
617
523
 
618
- for match in matches:
619
- if isinstance(match.obj, Mapping):
620
- for key, val in match.obj.items():
621
- context = FilterContext(
622
- env=self.env,
623
- current=val,
624
- root=match.root,
625
- extra_context=match.filter_context(),
626
- current_key=key,
627
- )
628
- try:
629
- if expr.evaluate(context):
630
- _match = self.env.match_class(
631
- filter_context=match.filter_context(),
632
- obj=val,
633
- parent=match,
634
- parts=match.parts + (key,),
635
- path=match.path + f"[{canonical_string(key)}]",
636
- root=match.root,
637
- )
638
- match.add_child(_match)
639
- yield _match
640
- except JSONPathTypeError as err:
641
- if not err.token:
642
- err.token = self.token
643
- raise
644
-
645
- elif isinstance(match.obj, Sequence) and not isinstance(match.obj, str):
646
- for i, obj in enumerate(match.obj):
647
- context = FilterContext(
648
- env=self.env,
649
- current=obj,
650
- root=match.root,
651
- extra_context=match.filter_context(),
652
- current_key=i,
653
- )
654
- try:
655
- if expr.evaluate(context):
656
- _match = self.env.match_class(
657
- filter_context=match.filter_context(),
658
- obj=obj,
659
- parent=match,
660
- parts=match.parts + (i,),
661
- path=f"{match.path}[{i}]",
662
- root=match.root,
663
- )
664
- match.add_child(_match)
665
- yield _match
666
- except JSONPathTypeError as err:
667
- if not err.token:
668
- err.token = self.token
669
- raise
670
-
671
- async def resolve_async( # noqa: PLR0912
672
- self, matches: AsyncIterable[JSONPathMatch]
673
- ) -> AsyncIterable[JSONPathMatch]:
524
+ if isinstance(node.obj, Mapping):
525
+ for key, val in node.obj.items():
526
+ context = FilterContext(
527
+ env=self.env,
528
+ current=val,
529
+ root=node.root,
530
+ extra_context=node.filter_context(),
531
+ current_key=key,
532
+ )
533
+ try:
534
+ if expr.evaluate(context):
535
+ match = node.new_child(val, key)
536
+ node.add_child(match)
537
+ yield match
538
+ except JSONPathTypeError as err:
539
+ if not err.token:
540
+ err.token = self.token
541
+ raise
542
+
543
+ elif isinstance(node.obj, Sequence) and not isinstance(node.obj, str):
544
+ for i, obj in enumerate(node.obj):
545
+ context = FilterContext(
546
+ env=self.env,
547
+ current=obj,
548
+ root=node.root,
549
+ extra_context=node.filter_context(),
550
+ current_key=i,
551
+ )
552
+ try:
553
+ if expr.evaluate(context):
554
+ match = node.new_child(obj, i)
555
+ node.add_child(match)
556
+ yield match
557
+ except JSONPathTypeError as err:
558
+ if not err.token:
559
+ err.token = self.token
560
+ raise
561
+
562
+ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
674
563
  if self.cacheable_nodes and self.env.filter_caching:
675
564
  expr = self.expression.cache_tree()
676
565
  else:
677
566
  expr = self.expression
678
567
 
679
- async for match in matches:
680
- if isinstance(match.obj, Mapping):
681
- for key, val in match.obj.items():
682
- context = FilterContext(
683
- env=self.env,
684
- current=val,
685
- root=match.root,
686
- extra_context=match.filter_context(),
687
- current_key=key,
688
- )
689
-
690
- try:
691
- result = await expr.evaluate_async(context)
692
- except JSONPathTypeError as err:
693
- if not err.token:
694
- err.token = self.token
695
- raise
696
-
697
- if result:
698
- _match = self.env.match_class(
699
- filter_context=match.filter_context(),
700
- obj=val,
701
- parent=match,
702
- parts=match.parts + (key,),
703
- path=match.path + f"[{canonical_string(key)}]",
704
- root=match.root,
568
+ if isinstance(node.obj, Mapping):
569
+ for key, val in node.obj.items():
570
+ context = FilterContext(
571
+ env=self.env,
572
+ current=val,
573
+ root=node.root,
574
+ extra_context=node.filter_context(),
575
+ current_key=key,
576
+ )
577
+
578
+ try:
579
+ result = await expr.evaluate_async(context)
580
+ except JSONPathTypeError as err:
581
+ if not err.token:
582
+ err.token = self.token
583
+ raise
584
+
585
+ if result:
586
+ match = node.new_child(val, key)
587
+ node.add_child(match)
588
+ yield match
589
+
590
+ elif isinstance(node.obj, Sequence) and not isinstance(node.obj, str):
591
+ for i, obj in enumerate(node.obj):
592
+ context = FilterContext(
593
+ env=self.env,
594
+ current=obj,
595
+ root=node.root,
596
+ extra_context=node.filter_context(),
597
+ current_key=i,
598
+ )
599
+
600
+ try:
601
+ result = await expr.evaluate_async(context)
602
+ except JSONPathTypeError as err:
603
+ if not err.token:
604
+ err.token = self.token
605
+ raise
606
+ if result:
607
+ match = node.new_child(obj, i)
608
+ node.add_child(match)
609
+ yield match
610
+
611
+
612
+ class KeysFilter(JSONPathSelector):
613
+ """Selects names from an object's name/value members.
614
+
615
+ NOTE: This is a non-standard selector.
616
+
617
+ See https://jg-rp.github.io/json-p3/guides/jsonpath-extra#keys-filter-selector
618
+ """
619
+
620
+ __slots__ = ("expression",)
621
+
622
+ def __init__(
623
+ self,
624
+ *,
625
+ env: JSONPathEnvironment,
626
+ token: Token,
627
+ expression: FilterExpression,
628
+ ) -> None:
629
+ super().__init__(env=env, token=token)
630
+ self.expression = expression
631
+
632
+ def __str__(self) -> str:
633
+ return f"~?{self.expression}"
634
+
635
+ def __eq__(self, __value: object) -> bool:
636
+ return (
637
+ isinstance(__value, Filter)
638
+ and self.expression == __value.expression
639
+ and self.token == __value.token
640
+ )
641
+
642
+ def __hash__(self) -> int:
643
+ return hash(("~", str(self.expression), self.token))
644
+
645
+ def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
646
+ if isinstance(node.value, Mapping):
647
+ for key, val in node.value.items():
648
+ context = FilterContext(
649
+ env=self.env,
650
+ current=val,
651
+ root=node.root,
652
+ extra_context=node.filter_context(),
653
+ current_key=key,
654
+ )
655
+
656
+ try:
657
+ if self.expression.evaluate(context):
658
+ match = node.__class__(
659
+ filter_context=node.filter_context(),
660
+ obj=key,
661
+ parent=node,
662
+ parts=node.parts
663
+ + (f"{self.env.keys_selector_token}{key}",),
664
+ path=f"{node.path}[{self.env.keys_selector_token}{canonical_string(key)}]",
665
+ root=node.root,
705
666
  )
706
- match.add_child(_match)
707
- yield _match
708
-
709
- elif isinstance(match.obj, Sequence) and not isinstance(match.obj, str):
710
- for i, obj in enumerate(match.obj):
711
- context = FilterContext(
712
- env=self.env,
713
- current=obj,
714
- root=match.root,
715
- extra_context=match.filter_context(),
716
- current_key=i,
717
- )
718
-
719
- try:
720
- result = await expr.evaluate_async(context)
721
- except JSONPathTypeError as err:
722
- if not err.token:
723
- err.token = self.token
724
- raise
725
- if result:
726
- _match = self.env.match_class(
727
- filter_context=match.filter_context(),
728
- obj=obj,
729
- parent=match,
730
- parts=match.parts + (i,),
731
- path=f"{match.path}[{i}]",
732
- root=match.root,
667
+ node.add_child(match)
668
+ yield match
669
+ except JSONPathTypeError as err:
670
+ if not err.token:
671
+ err.token = self.token
672
+ raise
673
+
674
+ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
675
+ if isinstance(node.value, Mapping):
676
+ for key, val in node.value.items():
677
+ context = FilterContext(
678
+ env=self.env,
679
+ current=val,
680
+ root=node.root,
681
+ extra_context=node.filter_context(),
682
+ current_key=key,
683
+ )
684
+
685
+ try:
686
+ if await self.expression.evaluate_async(context):
687
+ match = node.__class__(
688
+ filter_context=node.filter_context(),
689
+ obj=key,
690
+ parent=node,
691
+ parts=node.parts
692
+ + (f"{self.env.keys_selector_token}{key}",),
693
+ path=f"{node.path}[{self.env.keys_selector_token}{canonical_string(key)}]",
694
+ root=node.root,
733
695
  )
734
- match.add_child(_match)
735
- yield _match
696
+ node.add_child(match)
697
+ yield match
698
+ except JSONPathTypeError as err:
699
+ if not err.token:
700
+ err.token = self.token
701
+ raise
736
702
 
737
703
 
738
704
  class FilterContext: