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/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
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
32
32
 
33
33
  class _Undefined:
34
34
  def __str__(self) -> str:
35
- return "<jsonpath.pointer.UNDEFINED>"
35
+ return "<jsonpath.pointer.UNDEFINED>" # pragma: no cover
36
36
 
37
37
 
38
38
  UNDEFINED = _Undefined()
@@ -115,59 +115,83 @@ class JSONPointer:
115
115
  try:
116
116
  index = int(s)
117
117
  if index < self.min_int_index or index > self.max_int_index:
118
- raise JSONPointerIndexError("index out of range")
118
+ raise JSONPointerError("index out of range")
119
119
  return index
120
120
  except ValueError:
121
121
  return s
122
122
 
123
- def _getitem(self, obj: Any, key: Any) -> Any: # noqa: PLR0912
123
+ def _getitem(self, obj: Any, key: Any) -> Any:
124
124
  try:
125
+ # Handle the most common cases. A mapping with a string key, or a sequence
126
+ # 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.
125
130
  return getitem(obj, key)
126
131
  except KeyError as err:
127
- # Try a string repr of the index-like item as a mapping key.
128
- if isinstance(key, int):
129
- try:
130
- return getitem(obj, str(key))
131
- except KeyError:
132
- raise JSONPointerKeyError(key) from err
133
- # Handle non-standard keys/property selector/pointer.
134
- if (
135
- isinstance(key, str)
136
- and isinstance(obj, Mapping)
137
- and key.startswith((self.keys_selector, "#"))
138
- and key[1:] in obj
139
- ):
140
- return key[1:]
141
- # Handle non-standard index/property pointer (`#`)
142
- raise JSONPointerKeyError(key) from err
132
+ return self._handle_key_error(obj, key, err)
143
133
  except TypeError as err:
144
- if isinstance(obj, Sequence) and not isinstance(obj, str):
145
- if key == "-":
146
- # "-" is a valid index when appending to a JSON array
147
- # with JSON Patch, but not when resolving a JSON Pointer.
148
- raise JSONPointerIndexError("index out of range") from None
149
- # Handle non-standard index pointer.
150
- if isinstance(key, str) and key.startswith("#"):
151
- _index = int(key[1:])
152
- if _index >= len(obj):
153
- raise JSONPointerIndexError(
154
- f"index out of range: {_index}"
155
- ) from err
156
- return _index
157
- # Try int index. Reject non-zero ints that start with a zero.
158
- if isinstance(key, str):
159
- index = self._index(key)
160
- if isinstance(index, int):
161
- try:
162
- return getitem(obj, int(key))
163
- except IndexError as index_err:
164
- raise JSONPointerIndexError(
165
- f"index out of range: {key}"
166
- ) from index_err
167
- raise JSONPointerTypeError(f"{key}: {err}") from err
134
+ return self._handle_type_error(obj, key, err)
168
135
  except IndexError as err:
169
136
  raise JSONPointerIndexError(f"index out of range: {key}") from err
170
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))
142
+
143
+ # Handle non-standard key/property selector/pointer.
144
+ #
145
+ # For the benefit of `RelativeJSONPointer.to()` and `JSONPathMatch.pointer()`,
146
+ # treat keys starting with a `#` or `~` as a "key pointer". If `key[1:]` is a
147
+ # key in `obj`, return the key.
148
+ #
149
+ # Note that if a key with a leading `#`/`~` exists in `obj`, it will have been
150
+ # handled by `_getitem`.
151
+ if (
152
+ isinstance(key, str)
153
+ and isinstance(obj, Mapping)
154
+ and key.startswith((self.keys_selector, "#"))
155
+ and key[1:] in obj
156
+ ):
157
+ return key[1:]
158
+
159
+ raise JSONPointerKeyError(key) from err
160
+
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
+ ):
167
+ raise JSONPointerTypeError(f"{key}: {err}") from err
168
+
169
+ # `obj` is array-like
170
+ # `key` is a string
171
+
172
+ if key == "-":
173
+ # "-" is a valid index when appending to a JSON array with JSON Patch, but
174
+ # not when resolving a JSON Pointer.
175
+ raise JSONPointerIndexError("index out of range") from None
176
+
177
+ # Handle non-standard index pointer.
178
+ #
179
+ # For the benefit of `RelativeJSONPointer.to()`, treat keys starting with a `#`
180
+ # and followed by a valid index as an "index pointer". If `int(key[1:])` is
181
+ # less than `len(obj)`, return the index.
182
+ if re.match(r"#[1-9]\d*", key):
183
+ _index = int(key[1:])
184
+ if _index >= len(obj):
185
+ raise JSONPointerIndexError(f"index out of range: {_index}") from err
186
+ return _index
187
+
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
+ raise JSONPointerTypeError(f"{key}: {err}") from err
194
+
171
195
  def resolve(
172
196
  self,
173
197
  data: Union[str, IOBase, Sequence[object], Mapping[str, object]],
@@ -263,7 +287,7 @@ class JSONPointer:
263
287
  pointer = cls._encode(match.parts)
264
288
  else:
265
289
  # This should not happen, unless the JSONPathMatch has been tampered with.
266
- pointer = ""
290
+ pointer = "" # pragma: no cover
267
291
 
268
292
  return cls(
269
293
  pointer,
@@ -328,10 +352,10 @@ class JSONPointer:
328
352
  return isinstance(other, JSONPointer) and self.parts == other.parts
329
353
 
330
354
  def __hash__(self) -> int:
331
- return hash(self.parts)
355
+ return hash(self.parts) # pragma: no cover
332
356
 
333
357
  def __repr__(self) -> str:
334
- return f"JSONPointer({self._s!r})"
358
+ return f"JSONPointer({self._s!r})" # pragma: no cover
335
359
 
336
360
  def exists(
337
361
  self, data: Union[str, IOBase, Sequence[object], Mapping[str, object]]
@@ -486,7 +510,7 @@ class RelativeJSONPointer:
486
510
  return isinstance(__value, RelativeJSONPointer) and str(self) == str(__value)
487
511
 
488
512
  def __hash__(self) -> int:
489
- return hash((self.origin, self.index, self.pointer))
513
+ return hash((self.origin, self.index, self.pointer)) # pragma: no cover
490
514
 
491
515
  def _parse(
492
516
  self,
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))