python-jsonpath 1.3.2__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/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))