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/__about__.py +1 -1
- jsonpath/__init__.py +290 -8
- jsonpath/_types.py +31 -0
- jsonpath/cli.py +11 -1
- jsonpath/env.py +100 -47
- jsonpath/exceptions.py +78 -7
- jsonpath/filter.py +78 -84
- jsonpath/function_extensions/__init__.py +4 -2
- jsonpath/function_extensions/_pattern.py +112 -0
- jsonpath/function_extensions/keys.py +27 -8
- jsonpath/function_extensions/match.py +11 -13
- jsonpath/function_extensions/search.py +11 -13
- jsonpath/function_extensions/starts_with.py +21 -0
- jsonpath/lex.py +113 -64
- jsonpath/lru_cache.py +130 -0
- jsonpath/match.py +13 -0
- jsonpath/parse.py +448 -302
- jsonpath/patch.py +8 -3
- jsonpath/path.py +48 -84
- jsonpath/pointer.py +52 -55
- jsonpath/segments.py +131 -0
- jsonpath/selectors.py +448 -482
- jsonpath/stream.py +68 -70
- jsonpath/token.py +59 -61
- jsonpath/unescape.py +134 -0
- {python_jsonpath-1.3.2.dist-info → python_jsonpath-2.0.1.dist-info}/METADATA +7 -3
- python_jsonpath-2.0.1.dist-info/RECORD +42 -0
- python_jsonpath-1.3.2.dist-info/RECORD +0 -36
- {python_jsonpath-1.3.2.dist-info → python_jsonpath-2.0.1.dist-info}/WHEEL +0 -0
- {python_jsonpath-1.3.2.dist-info → python_jsonpath-2.0.1.dist-info}/entry_points.txt +0 -0
- {python_jsonpath-1.3.2.dist-info → python_jsonpath-2.0.1.dist-info}/licenses/LICENSE.txt +0 -0
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
|
-
|
|
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[
|
|
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[
|
|
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
|
-
|
|
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
|
|
23
|
-
from jsonpath.selectors import PropertySelector
|
|
21
|
+
from jsonpath.selectors import NameSelector
|
|
24
22
|
|
|
25
23
|
if TYPE_CHECKING:
|
|
26
|
-
from
|
|
24
|
+
from jsonpath._types import JSONData
|
|
27
25
|
|
|
28
26
|
from .env import JSONPathEnvironment
|
|
29
|
-
from .
|
|
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
|
-
|
|
35
|
+
segments: An iterable of `JSONPathSegment` instances, as generated by
|
|
38
36
|
a `Parser`.
|
|
39
|
-
|
|
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", "
|
|
46
|
+
__slots__ = ("env", "pseudo_root", "segments")
|
|
49
47
|
|
|
50
48
|
def __init__(
|
|
51
49
|
self,
|
|
52
50
|
*,
|
|
53
51
|
env: JSONPathEnvironment,
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
segments: Iterable[JSONPathSegment],
|
|
53
|
+
pseudo_root: bool = False,
|
|
56
54
|
) -> None:
|
|
57
55
|
self.env = env
|
|
58
|
-
self.
|
|
59
|
-
self.
|
|
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.
|
|
63
|
+
return isinstance(__value, JSONPath) and self.segments == __value.segments
|
|
68
64
|
|
|
69
65
|
def __hash__(self) -> int:
|
|
70
|
-
return hash(self.
|
|
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.
|
|
123
|
+
obj=[_data] if self.pseudo_root else _data,
|
|
132
124
|
parent=None,
|
|
133
|
-
path=
|
|
125
|
+
path=path,
|
|
134
126
|
parts=(),
|
|
135
127
|
root=_data,
|
|
136
128
|
)
|
|
137
129
|
]
|
|
138
130
|
|
|
139
|
-
for
|
|
140
|
-
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.
|
|
157
|
+
obj=[_data] if self.pseudo_root else _data,
|
|
171
158
|
parent=None,
|
|
172
|
-
path=
|
|
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
|
|
180
|
-
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.
|
|
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
|
|
245
|
-
if isinstance(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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[
|
|
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,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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(
|
|
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
|
|
125
|
+
return None
|
|
122
126
|
|
|
123
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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(
|
|
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:
|
|
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,
|
|
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"
|
|
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
|
|
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
|
|
416
|
+
return self.__class__(other, unicode_escape=False, uri_decode=False)
|
|
419
417
|
|
|
420
418
|
parts = self.parts + tuple(
|
|
421
|
-
|
|
422
|
-
for p in other.split("/")
|
|
419
|
+
p.replace("~1", "/").replace("~0", "~") for p in other.split("/")
|
|
423
420
|
)
|
|
424
421
|
|
|
425
|
-
return
|
|
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))
|