python-jsonpath 1.3.2__py3-none-any.whl → 2.0.1__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/patch.py CHANGED
@@ -7,6 +7,7 @@ import json
7
7
  from abc import ABC
8
8
  from abc import abstractmethod
9
9
  from io import IOBase
10
+ from typing import Any
10
11
  from typing import Dict
11
12
  from typing import Iterable
12
13
  from typing import List
@@ -70,7 +71,11 @@ class OpAdd(Op):
70
71
  if target == "-":
71
72
  parent.append(self.value)
72
73
  else:
73
- raise JSONPatchError("index out of range")
74
+ index = self.path._index(target) # noqa: SLF001
75
+ if index == len(parent):
76
+ parent.append(self.value)
77
+ else:
78
+ raise JSONPatchError("index out of range")
74
79
  else:
75
80
  parent.insert(int(target), self.value)
76
81
  elif isinstance(parent, MutableMapping):
@@ -628,7 +633,7 @@ class JSONPatch:
628
633
 
629
634
  def apply(
630
635
  self,
631
- data: Union[str, IOBase, MutableSequence[object], MutableMapping[str, object]],
636
+ data: Union[str, IOBase, MutableSequence[Any], MutableMapping[str, Any]],
632
637
  ) -> object:
633
638
  """Apply all operations from this patch to _data_.
634
639
 
@@ -676,7 +681,7 @@ class JSONPatch:
676
681
 
677
682
  def apply(
678
683
  patch: Union[str, IOBase, Iterable[Mapping[str, object]], None],
679
- data: Union[str, IOBase, MutableSequence[object], MutableMapping[str, object]],
684
+ data: Union[str, IOBase, MutableSequence[Any], MutableMapping[str, Any]],
680
685
  *,
681
686
  unicode_escape: bool = True,
682
687
  uri_decode: bool = False,
jsonpath/path.py CHANGED
@@ -1,15 +1,13 @@
1
- # noqa: D100
1
+ """A compiled JSONPath ready to be applied to a JSON string or Python object."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import itertools
5
6
  from typing import TYPE_CHECKING
6
- from typing import Any
7
7
  from typing import AsyncIterable
8
8
  from typing import Iterable
9
9
  from typing import List
10
- from typing import Mapping
11
10
  from typing import Optional
12
- from typing import Sequence
13
11
  from typing import Tuple
14
12
  from typing import TypeVar
15
13
  from typing import Union
@@ -18,15 +16,15 @@ from jsonpath._data import load_data
18
16
  from jsonpath.fluent_api import Query
19
17
  from jsonpath.match import FilterContextVars
20
18
  from jsonpath.match import JSONPathMatch
19
+ from jsonpath.segments import JSONPathRecursiveDescentSegment
21
20
  from jsonpath.selectors import IndexSelector
22
- from jsonpath.selectors import ListSelector
23
- from jsonpath.selectors import PropertySelector
21
+ from jsonpath.selectors import NameSelector
24
22
 
25
23
  if TYPE_CHECKING:
26
- from io import IOBase
24
+ from jsonpath._types import JSONData
27
25
 
28
26
  from .env import JSONPathEnvironment
29
- from .selectors import JSONPathSelector
27
+ from .segments import JSONPathSegment
30
28
 
31
29
 
32
30
  class JSONPath:
@@ -34,9 +32,9 @@ class JSONPath:
34
32
 
35
33
  Arguments:
36
34
  env: The `JSONPathEnvironment` this path is bound to.
37
- selectors: An iterable of `JSONPathSelector` objects, as generated by
35
+ segments: An iterable of `JSONPathSegment` instances, as generated by
38
36
  a `Parser`.
39
- fake_root: Indicates if target JSON values should be wrapped in a single-
37
+ pseudo_root: Indicates if target JSON values should be wrapped in a single-
40
38
  element array, so as to make the target root value selectable.
41
39
 
42
40
 
@@ -45,35 +43,30 @@ class JSONPath:
45
43
  selectors: The `JSONPathSelector` instances that make up this path.
46
44
  """
47
45
 
48
- __slots__ = ("env", "fake_root", "selectors")
46
+ __slots__ = ("env", "pseudo_root", "segments")
49
47
 
50
48
  def __init__(
51
49
  self,
52
50
  *,
53
51
  env: JSONPathEnvironment,
54
- selectors: Iterable[JSONPathSelector],
55
- fake_root: bool = False,
52
+ segments: Iterable[JSONPathSegment],
53
+ pseudo_root: bool = False,
56
54
  ) -> None:
57
55
  self.env = env
58
- self.selectors = tuple(selectors)
59
- self.fake_root = fake_root
56
+ self.segments = tuple(segments)
57
+ self.pseudo_root = pseudo_root
60
58
 
61
59
  def __str__(self) -> str:
62
- return self.env.root_token + "".join(
63
- str(selector) for selector in self.selectors
64
- )
60
+ return self.env.root_token + "".join(str(segment) for segment in self.segments)
65
61
 
66
62
  def __eq__(self, __value: object) -> bool:
67
- return isinstance(__value, JSONPath) and self.selectors == __value.selectors
63
+ return isinstance(__value, JSONPath) and self.segments == __value.segments
68
64
 
69
65
  def __hash__(self) -> int:
70
- return hash(self.selectors)
66
+ return hash(self.segments)
71
67
 
72
68
  def findall(
73
- self,
74
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
75
- *,
76
- filter_context: Optional[FilterContextVars] = None,
69
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
77
70
  ) -> List[object]:
78
71
  """Find all objects in `data` matching the given JSONPath `path`.
79
72
 
@@ -100,10 +93,7 @@ class JSONPath:
100
93
  ]
101
94
 
102
95
  def finditer(
103
- self,
104
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
105
- *,
106
- filter_context: Optional[FilterContextVars] = None,
96
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
107
97
  ) -> Iterable[JSONPathMatch]:
108
98
  """Generate `JSONPathMatch` objects for each match.
109
99
 
@@ -125,27 +115,26 @@ class JSONPath:
125
115
  an incompatible way.
126
116
  """
127
117
  _data = load_data(data)
118
+ path = self.env.pseudo_root_token if self.pseudo_root else self.env.root_token
119
+
128
120
  matches: Iterable[JSONPathMatch] = [
129
121
  JSONPathMatch(
130
122
  filter_context=filter_context or {},
131
- obj=[_data] if self.fake_root else _data,
123
+ obj=[_data] if self.pseudo_root else _data,
132
124
  parent=None,
133
- path=self.env.root_token,
125
+ path=path,
134
126
  parts=(),
135
127
  root=_data,
136
128
  )
137
129
  ]
138
130
 
139
- for selector in self.selectors:
140
- matches = selector.resolve(matches)
131
+ for segment in self.segments:
132
+ matches = segment.resolve(matches)
141
133
 
142
134
  return matches
143
135
 
144
136
  async def findall_async(
145
- self,
146
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
147
- *,
148
- filter_context: Optional[FilterContextVars] = None,
137
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
149
138
  ) -> List[object]:
150
139
  """An async version of `findall()`."""
151
140
  return [
@@ -156,36 +145,31 @@ class JSONPath:
156
145
  ]
157
146
 
158
147
  async def finditer_async(
159
- self,
160
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
161
- *,
162
- filter_context: Optional[FilterContextVars] = None,
148
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
163
149
  ) -> AsyncIterable[JSONPathMatch]:
164
150
  """An async version of `finditer()`."""
165
151
  _data = load_data(data)
152
+ path = self.env.pseudo_root_token if self.pseudo_root else self.env.root_token
166
153
 
167
154
  async def root_iter() -> AsyncIterable[JSONPathMatch]:
168
155
  yield self.env.match_class(
169
156
  filter_context=filter_context or {},
170
- obj=[_data] if self.fake_root else _data,
157
+ obj=[_data] if self.pseudo_root else _data,
171
158
  parent=None,
172
- path=self.env.root_token,
159
+ path=path,
173
160
  parts=(),
174
161
  root=_data,
175
162
  )
176
163
 
177
164
  matches: AsyncIterable[JSONPathMatch] = root_iter()
178
165
 
179
- for selector in self.selectors:
180
- matches = selector.resolve_async(matches)
166
+ for segment in self.segments:
167
+ matches = segment.resolve_async(matches)
181
168
 
182
169
  return matches
183
170
 
184
171
  def match(
185
- self,
186
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
187
- *,
188
- filter_context: Optional[FilterContextVars] = None,
172
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
189
173
  ) -> Union[JSONPathMatch, None]:
190
174
  """Return a `JSONPathMatch` instance for the first object found in _data_.
191
175
 
@@ -212,10 +196,7 @@ class JSONPath:
212
196
  return None
213
197
 
214
198
  def query(
215
- self,
216
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
217
- *,
218
- filter_context: Optional[FilterContextVars] = None,
199
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
219
200
  ) -> Query:
220
201
  """Return a `Query` iterator over matches found by applying this path to _data_.
221
202
 
@@ -237,20 +218,21 @@ class JSONPath:
237
218
 
238
219
  def empty(self) -> bool:
239
220
  """Return `True` if this path has no selectors."""
240
- return not bool(self.selectors)
221
+ return not bool(self.segments)
241
222
 
242
223
  def singular_query(self) -> bool:
243
224
  """Return `True` if this JSONPath query is a singular query."""
244
- for selector in self.selectors:
245
- if isinstance(selector, (PropertySelector, IndexSelector)):
246
- continue
247
- if (
248
- isinstance(selector, ListSelector)
249
- and len(selector.items) == 1
250
- and isinstance(selector.items[0], (PropertySelector, IndexSelector))
225
+ for segment in self.segments:
226
+ if isinstance(segment, JSONPathRecursiveDescentSegment):
227
+ return False
228
+
229
+ if len(segment.selectors) == 1 and isinstance(
230
+ segment.selectors[0], (NameSelector, IndexSelector)
251
231
  ):
252
232
  continue
233
+
253
234
  return False
235
+
254
236
  return True
255
237
 
256
238
 
@@ -288,10 +270,7 @@ class CompoundJSONPath:
288
270
  return hash((self.path, self.paths))
289
271
 
290
272
  def findall(
291
- self,
292
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
293
- *,
294
- filter_context: Optional[FilterContextVars] = None,
273
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
295
274
  ) -> List[object]:
296
275
  """Find all objects in `data` matching the given JSONPath `path`.
297
276
 
@@ -326,10 +305,7 @@ class CompoundJSONPath:
326
305
  return objs
327
306
 
328
307
  def finditer(
329
- self,
330
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
331
- *,
332
- filter_context: Optional[FilterContextVars] = None,
308
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
333
309
  ) -> Iterable[JSONPathMatch]:
334
310
  """Generate `JSONPathMatch` objects for each match.
335
311
 
@@ -364,10 +340,7 @@ class CompoundJSONPath:
364
340
  return matches
365
341
 
366
342
  def match(
367
- self,
368
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
369
- *,
370
- filter_context: Optional[FilterContextVars] = None,
343
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
371
344
  ) -> Union[JSONPathMatch, None]:
372
345
  """Return a `JSONPathMatch` instance for the first object found in _data_.
373
346
 
@@ -394,10 +367,7 @@ class CompoundJSONPath:
394
367
  return None
395
368
 
396
369
  async def findall_async(
397
- self,
398
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
399
- *,
400
- filter_context: Optional[FilterContextVars] = None,
370
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
401
371
  ) -> List[object]:
402
372
  """An async version of `findall()`."""
403
373
  objs = await self.path.findall_async(data, filter_context=filter_context)
@@ -413,10 +383,7 @@ class CompoundJSONPath:
413
383
  return objs
414
384
 
415
385
  async def finditer_async(
416
- self,
417
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
418
- *,
419
- filter_context: Optional[FilterContextVars] = None,
386
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
420
387
  ) -> AsyncIterable[JSONPathMatch]:
421
388
  """An async version of `finditer()`."""
422
389
  matches = await self.path.finditer_async(data, filter_context=filter_context)
@@ -433,10 +400,7 @@ class CompoundJSONPath:
433
400
  return matches
434
401
 
435
402
  def query(
436
- self,
437
- data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
438
- *,
439
- filter_context: Optional[FilterContextVars] = None,
403
+ self, data: JSONData, *, filter_context: Optional[FilterContextVars] = None
440
404
  ) -> Query:
441
405
  """Return a `Query` iterator over matches found by applying this path to _data_.
442
406
 
jsonpath/pointer.py CHANGED
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING
10
10
  from typing import Any
11
11
  from typing import Iterable
12
12
  from typing import Mapping
13
+ from typing import Optional
13
14
  from typing import Sequence
14
15
  from typing import Tuple
15
16
  from typing import Union
@@ -58,14 +59,14 @@ class JSONPointer:
58
59
  max_int_index (int): The maximum integer allowed when resolving array
59
60
  items by index. Defaults to `(2**53) - 1`.
60
61
  min_int_index (int): The minimum integer allowed when resolving array
61
- items by index. Defaults to `-(2**53) + 1`.
62
+ items by index. Defaults to `0`.
62
63
  """
63
64
 
64
65
  __slots__ = ("_s", "parts")
65
66
 
66
67
  keys_selector = "~"
67
68
  max_int_index = (2**53) - 1
68
- min_int_index = -(2**53) + 1
69
+ min_int_index = 0
69
70
 
70
71
  def __init__(
71
72
  self,
@@ -75,11 +76,15 @@ class JSONPointer:
75
76
  unicode_escape: bool = True,
76
77
  uri_decode: bool = False,
77
78
  ) -> None:
78
- self.parts = parts or self._parse(
79
- pointer,
80
- unicode_escape=unicode_escape,
81
- uri_decode=uri_decode,
82
- )
79
+ if parts:
80
+ self.parts = tuple(str(part) for part in parts)
81
+ else:
82
+ self.parts = self._parse(
83
+ pointer,
84
+ unicode_escape=unicode_escape,
85
+ uri_decode=uri_decode,
86
+ )
87
+
83
88
  self._s = self._encode(self.parts)
84
89
 
85
90
  def __str__(self) -> str:
@@ -91,7 +96,7 @@ class JSONPointer:
91
96
  *,
92
97
  unicode_escape: bool,
93
98
  uri_decode: bool,
94
- ) -> Tuple[Union[int, str], ...]:
99
+ ) -> Tuple[str, ...]:
95
100
  if uri_decode:
96
101
  s = unquote(s)
97
102
  if unicode_escape:
@@ -103,43 +108,49 @@ class JSONPointer:
103
108
  "pointer must start with a slash or be the empty string"
104
109
  )
105
110
 
106
- return tuple(
107
- self._index(p.replace("~1", "/").replace("~0", "~")) for p in s.split("/")
108
- )[1:]
111
+ return tuple(p.replace("~1", "/").replace("~0", "~") for p in s.split("/"))[1:]
109
112
 
110
- def _index(self, s: str) -> Union[str, int]:
111
- # Reject non-zero ints that start with a zero.
112
- if len(s) > 1 and s.startswith("0"):
113
- return s
113
+ def _index(self, key: str) -> Optional[int]:
114
+ """Return an array index for `key`.
115
+
116
+ Return `None` if key can't be converted to an index.
117
+ """
118
+ # Reject indexes that start with a zero.
119
+ if len(key) > 1 and key.startswith("0"):
120
+ return None
114
121
 
115
122
  try:
116
- index = int(s)
117
- if index < self.min_int_index or index > self.max_int_index:
118
- raise JSONPointerError("index out of range")
119
- return index
123
+ index = int(key)
120
124
  except ValueError:
121
- return s
125
+ return None
122
126
 
123
- def _getitem(self, obj: Any, key: Any) -> Any:
127
+ if index < self.min_int_index or index > self.max_int_index:
128
+ raise JSONPointerIndexError(
129
+ f"array indices must be between {self.min_int_index}"
130
+ f" and {self.max_int_index}"
131
+ )
132
+
133
+ return index
134
+
135
+ def _getitem(self, obj: Any, key: str) -> Any:
124
136
  try:
125
137
  # Handle the most common cases. A mapping with a string key, or a sequence
126
138
  # with an integer index.
127
- #
128
- # Note that `obj` does not have to be a Mapping or Sequence here. Any object
129
- # implementing `__getitem__` will do.
139
+ if isinstance(obj, Sequence) and not isinstance(obj, str):
140
+ index = self._index(key)
141
+ if isinstance(index, int):
142
+ return getitem(obj, index)
130
143
  return getitem(obj, key)
131
144
  except KeyError as err:
132
145
  return self._handle_key_error(obj, key, err)
133
146
  except TypeError as err:
134
147
  return self._handle_type_error(obj, key, err)
135
148
  except IndexError as err:
136
- raise JSONPointerIndexError(f"index out of range: {key}") from err
137
-
138
- def _handle_key_error(self, obj: Any, key: Any, err: Exception) -> object:
139
- if isinstance(key, int):
140
- # Try a string repr of the index-like item as a mapping key.
141
- return self._getitem(obj, str(key))
149
+ if not isinstance(err, JSONPointerIndexError):
150
+ raise JSONPointerIndexError(f"index out of range: {key}") from err
151
+ raise
142
152
 
153
+ def _handle_key_error(self, obj: Any, key: str, err: Exception) -> object:
143
154
  # Handle non-standard key/property selector/pointer.
144
155
  #
145
156
  # For the benefit of `RelativeJSONPointer.to()` and `JSONPathMatch.pointer()`,
@@ -149,8 +160,7 @@ class JSONPointer:
149
160
  # Note that if a key with a leading `#`/`~` exists in `obj`, it will have been
150
161
  # handled by `_getitem`.
151
162
  if (
152
- isinstance(key, str)
153
- and isinstance(obj, Mapping)
163
+ isinstance(obj, Mapping)
154
164
  and key.startswith((self.keys_selector, "#"))
155
165
  and key[1:] in obj
156
166
  ):
@@ -158,17 +168,10 @@ class JSONPointer:
158
168
 
159
169
  raise JSONPointerKeyError(key) from err
160
170
 
161
- def _handle_type_error(self, obj: Any, key: Any, err: Exception) -> object:
162
- if (
163
- isinstance(obj, str)
164
- or not isinstance(obj, Sequence)
165
- or not isinstance(key, str)
166
- ):
171
+ def _handle_type_error(self, obj: Any, key: str, err: Exception) -> object:
172
+ if not isinstance(obj, Sequence) or not isinstance(key, str):
167
173
  raise JSONPointerTypeError(f"{key}: {err}") from err
168
174
 
169
- # `obj` is array-like
170
- # `key` is a string
171
-
172
175
  if key == "-":
173
176
  # "-" is a valid index when appending to a JSON array with JSON Patch, but
174
177
  # not when resolving a JSON Pointer.
@@ -185,11 +188,6 @@ class JSONPointer:
185
188
  raise JSONPointerIndexError(f"index out of range: {_index}") from err
186
189
  return _index
187
190
 
188
- # Try int index. Reject non-zero ints that start with a zero.
189
- index = self._index(key)
190
- if isinstance(index, int):
191
- return self._getitem(obj, index)
192
-
193
191
  raise JSONPointerTypeError(f"{key}: {err}") from err
194
192
 
195
193
  def resolve(
@@ -349,13 +347,13 @@ class JSONPointer:
349
347
  )
350
348
 
351
349
  def __eq__(self, other: object) -> bool:
352
- return isinstance(other, JSONPointer) and self.parts == other.parts
350
+ return isinstance(other, self.__class__) and self.parts == other.parts
353
351
 
354
352
  def __hash__(self) -> int:
355
- return hash(self.parts) # pragma: no cover
353
+ return hash((self.__class__, self.parts)) # pragma: no cover
356
354
 
357
355
  def __repr__(self) -> str:
358
- return f"JSONPointer({self._s!r})" # pragma: no cover
356
+ return f"{self.__class__.__name__}({self._s!r})" # pragma: no cover
359
357
 
360
358
  def exists(
361
359
  self, data: Union[str, IOBase, Sequence[object], Mapping[str, object]]
@@ -391,7 +389,7 @@ class JSONPointer:
391
389
  if not self.parts:
392
390
  return self
393
391
  parent_parts = self.parts[:-1]
394
- return JSONPointer(
392
+ return self.__class__(
395
393
  self._encode(parent_parts),
396
394
  parts=parent_parts,
397
395
  unicode_escape=False,
@@ -415,14 +413,13 @@ class JSONPointer:
415
413
 
416
414
  other = self._unicode_escape(other.lstrip())
417
415
  if other.startswith("/"):
418
- return JSONPointer(other, unicode_escape=False, uri_decode=False)
416
+ return self.__class__(other, unicode_escape=False, uri_decode=False)
419
417
 
420
418
  parts = self.parts + tuple(
421
- self._index(p.replace("~1", "/").replace("~0", "~"))
422
- for p in other.split("/")
419
+ p.replace("~1", "/").replace("~0", "~") for p in other.split("/")
423
420
  )
424
421
 
425
- return JSONPointer(
422
+ return self.__class__(
426
423
  self._encode(parts), parts=parts, unicode_escape=False, uri_decode=False
427
424
  )
428
425
 
@@ -612,7 +609,7 @@ class RelativeJSONPointer:
612
609
  raise RelativeJSONPointerIndexError(
613
610
  f"index offset out of range {new_index}"
614
611
  )
615
- parts[-1] = int(parts[-1]) + self.index
612
+ parts[-1] = str(int(parts[-1]) + self.index)
616
613
 
617
614
  # Pointer or index/property
618
615
  if isinstance(self.pointer, JSONPointer):
jsonpath/segments.py ADDED
@@ -0,0 +1,131 @@
1
+ """JSONPath child and descendant segment definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC
6
+ from abc import abstractmethod
7
+ from typing import TYPE_CHECKING
8
+ from typing import AsyncIterable
9
+ from typing import Iterable
10
+ from typing import Mapping
11
+ from typing import Sequence
12
+ from typing import Tuple
13
+
14
+ from .exceptions import JSONPathRecursionError
15
+
16
+ if TYPE_CHECKING:
17
+ from .env import JSONPathEnvironment
18
+ from .match import JSONPathMatch
19
+ from .selectors import JSONPathSelector
20
+ from .token import Token
21
+
22
+
23
+ class JSONPathSegment(ABC):
24
+ """Base class for all JSONPath segments."""
25
+
26
+ __slots__ = ("env", "token", "selectors")
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ env: JSONPathEnvironment,
32
+ token: Token,
33
+ selectors: Tuple[JSONPathSelector, ...],
34
+ ) -> None:
35
+ self.env = env
36
+ self.token = token
37
+ self.selectors = selectors
38
+
39
+ @abstractmethod
40
+ def resolve(self, nodes: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
41
+ """Apply this segment to each `JSONPathMatch` in _nodes_."""
42
+
43
+ @abstractmethod
44
+ def resolve_async(
45
+ self, nodes: AsyncIterable[JSONPathMatch]
46
+ ) -> AsyncIterable[JSONPathMatch]:
47
+ """An async version of `resolve`."""
48
+
49
+
50
+ class JSONPathChildSegment(JSONPathSegment):
51
+ """The JSONPath child selection segment."""
52
+
53
+ def resolve(self, nodes: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
54
+ """Select children of each node in _nodes_."""
55
+ for node in nodes:
56
+ for selector in self.selectors:
57
+ yield from selector.resolve(node)
58
+
59
+ async def resolve_async(
60
+ self, nodes: AsyncIterable[JSONPathMatch]
61
+ ) -> AsyncIterable[JSONPathMatch]:
62
+ """An async version of `resolve`."""
63
+ async for node in nodes:
64
+ for selector in self.selectors:
65
+ async for match in selector.resolve_async(node):
66
+ yield match
67
+
68
+ def __str__(self) -> str:
69
+ return f"[{', '.join(str(itm) for itm in self.selectors)}]"
70
+
71
+ def __eq__(self, __value: object) -> bool:
72
+ return (
73
+ isinstance(__value, JSONPathChildSegment)
74
+ and self.selectors == __value.selectors
75
+ and self.token == __value.token
76
+ )
77
+
78
+ def __hash__(self) -> int:
79
+ return hash((self.selectors, self.token))
80
+
81
+
82
+ class JSONPathRecursiveDescentSegment(JSONPathSegment):
83
+ """The JSONPath recursive descent segment."""
84
+
85
+ def resolve(self, nodes: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
86
+ """Select descendants of each node in _nodes_."""
87
+ for node in nodes:
88
+ for _node in self._visit(node):
89
+ for selector in self.selectors:
90
+ yield from selector.resolve(_node)
91
+
92
+ async def resolve_async(
93
+ self, nodes: AsyncIterable[JSONPathMatch]
94
+ ) -> AsyncIterable[JSONPathMatch]:
95
+ """An async version of `resolve`."""
96
+ async for node in nodes:
97
+ for _node in self._visit(node):
98
+ for selector in self.selectors:
99
+ async for match in selector.resolve_async(_node):
100
+ yield match
101
+
102
+ def _visit(self, node: JSONPathMatch, depth: int = 1) -> Iterable[JSONPathMatch]:
103
+ """Depth-first, pre-order node traversal."""
104
+ if depth > self.env.max_recursion_depth:
105
+ raise JSONPathRecursionError("recursion limit exceeded", token=self.token)
106
+
107
+ yield node
108
+
109
+ if isinstance(node.obj, Mapping):
110
+ for name, val in node.obj.items():
111
+ if isinstance(val, (Mapping, Sequence)):
112
+ _node = node.new_child(val, name)
113
+ yield from self._visit(_node, depth + 1)
114
+ elif isinstance(node.obj, Sequence) and not isinstance(node.obj, str):
115
+ for i, item in enumerate(node.obj):
116
+ if isinstance(item, (Mapping, Sequence)):
117
+ _node = node.new_child(item, i)
118
+ yield from self._visit(_node, depth + 1)
119
+
120
+ def __str__(self) -> str:
121
+ return f"..[{', '.join(str(itm) for itm in self.selectors)}]"
122
+
123
+ def __eq__(self, __value: object) -> bool:
124
+ return (
125
+ isinstance(__value, JSONPathRecursiveDescentSegment)
126
+ and self.selectors == __value.selectors
127
+ and self.token == __value.token
128
+ )
129
+
130
+ def __hash__(self) -> int:
131
+ return hash(("..", self.selectors, self.token))