Skip to content

Commit 6ce68d2

Browse files
committed
Add a strict argument to convenience functions
1 parent cfa891c commit 6ce68d2

File tree

6 files changed

+347
-69
lines changed

6 files changed

+347
-69
lines changed

jsonpath/__init__.py

Lines changed: 290 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
# SPDX-FileCopyrightText: 2023-present James Prior <[email protected]>
22
#
33
# SPDX-License-Identifier: MIT
4+
from __future__ import annotations
45

6+
from typing import TYPE_CHECKING
7+
from typing import AsyncIterable
8+
from typing import Iterable
9+
from typing import List
10+
from typing import Optional
11+
from typing import Union
12+
13+
from ._types import JSON
14+
from ._types import JSONData
15+
from ._types import JSONScalar
516
from .env import JSONPathEnvironment
617
from .exceptions import JSONPatchError
718
from .exceptions import JSONPatchTestFailure
@@ -32,6 +43,10 @@
3243
from .pointer import RelativeJSONPointer
3344
from .pointer import resolve
3445

46+
if TYPE_CHECKING:
47+
from .match import FilterContextVars
48+
49+
3550
__all__ = (
3651
"compile",
3752
"CompoundJSONPath",
@@ -68,16 +83,283 @@
6883
"RelativeJSONPointerIndexError",
6984
"RelativeJSONPointerSyntaxError",
7085
"resolve",
86+
"JSON",
87+
"JSONData",
88+
"JSONScalar",
7189
"UNDEFINED",
7290
)
7391

7492

75-
# For convenience
93+
# For convenience and to delegate to strict or non-strict environments.
7694
DEFAULT_ENV = JSONPathEnvironment()
77-
compile = DEFAULT_ENV.compile # noqa: A001
78-
findall = DEFAULT_ENV.findall
79-
findall_async = DEFAULT_ENV.findall_async
80-
finditer = DEFAULT_ENV.finditer
81-
finditer_async = DEFAULT_ENV.finditer_async
82-
match = DEFAULT_ENV.match
83-
query = DEFAULT_ENV.query
95+
STRICT_ENV = JSONPathEnvironment(strict=True)
96+
97+
98+
def compile(path: str, *, strict: bool = False) -> Union[JSONPath, CompoundJSONPath]: # noqa: A001
99+
"""Prepare a path string ready for repeated matching against different data.
100+
101+
Arguments:
102+
path: A JSONPath as a string.
103+
strict: When `True`, compile the path for strict compliance with RFC 9535.
104+
105+
Returns:
106+
A `JSONPath` or `CompoundJSONPath`, ready to match against some data.
107+
Expect a `CompoundJSONPath` if the path string uses the _union_ or
108+
_intersection_ operators.
109+
110+
Raises:
111+
JSONPathSyntaxError: If _path_ is invalid.
112+
JSONPathTypeError: If filter functions are given arguments of an
113+
unacceptable type.
114+
"""
115+
return STRICT_ENV.compile(path) if strict else DEFAULT_ENV.compile(path)
116+
117+
118+
def findall(
119+
path: str,
120+
data: JSONData,
121+
*,
122+
filter_context: Optional[FilterContextVars] = None,
123+
strict: bool = False,
124+
) -> List[object]:
125+
"""Find all objects in _data_ matching the JSONPath _path_.
126+
127+
If _data_ is a string or a file-like objects, it will be loaded
128+
using `json.loads()` and the default `JSONDecoder`.
129+
130+
Arguments:
131+
path: The JSONPath as a string.
132+
data: A JSON document or Python object implementing the `Sequence`
133+
or `Mapping` interfaces.
134+
filter_context: Arbitrary data made available to filters using
135+
the _filter context_ selector.
136+
strict: When `True`, compile and evaluate with strict compliance with
137+
RFC 9535.
138+
139+
Returns:
140+
A list of matched objects. If there are no matches, the list will
141+
be empty.
142+
143+
Raises:
144+
JSONPathSyntaxError: If the path is invalid.
145+
JSONPathTypeError: If a filter expression attempts to use types in
146+
an incompatible way.
147+
"""
148+
return (
149+
STRICT_ENV.findall(path, data, filter_context=filter_context)
150+
if strict
151+
else DEFAULT_ENV.findall(path, data, filter_context=filter_context)
152+
)
153+
154+
155+
async def findall_async(
156+
path: str,
157+
data: JSONData,
158+
*,
159+
filter_context: Optional[FilterContextVars] = None,
160+
strict: bool = False,
161+
) -> List[object]:
162+
"""Find all objects in _data_ matching the JSONPath _path_.
163+
164+
If _data_ is a string or a file-like objects, it will be loaded
165+
using `json.loads()` and the default `JSONDecoder`.
166+
167+
Arguments:
168+
path: The JSONPath as a string.
169+
data: A JSON document or Python object implementing the `Sequence`
170+
or `Mapping` interfaces.
171+
filter_context: Arbitrary data made available to filters using
172+
the _filter context_ selector.
173+
strict: When `True`, compile and evaluate with strict compliance with
174+
RFC 9535.
175+
176+
Returns:
177+
A list of matched objects. If there are no matches, the list will
178+
be empty.
179+
180+
Raises:
181+
JSONPathSyntaxError: If the path is invalid.
182+
JSONPathTypeError: If a filter expression attempts to use types in
183+
an incompatible way.
184+
"""
185+
return (
186+
await STRICT_ENV.findall_async(path, data, filter_context=filter_context)
187+
if strict
188+
else await DEFAULT_ENV.findall_async(path, data, filter_context=filter_context)
189+
)
190+
191+
192+
def finditer(
193+
path: str,
194+
data: JSONData,
195+
*,
196+
filter_context: Optional[FilterContextVars] = None,
197+
strict: bool = False,
198+
) -> Iterable[JSONPathMatch]:
199+
"""Generate `JSONPathMatch` objects for each match of _path_ in _data_.
200+
201+
If _data_ is a string or a file-like objects, it will be loaded using
202+
`json.loads()` and the default `JSONDecoder`.
203+
204+
Arguments:
205+
path: The JSONPath as a string.
206+
data: A JSON document or Python object implementing the `Sequence`
207+
or `Mapping` interfaces.
208+
filter_context: Arbitrary data made available to filters using
209+
the _filter context_ selector.
210+
strict: When `True`, compile and evaluate with strict compliance with
211+
RFC 9535.
212+
213+
Returns:
214+
An iterator yielding `JSONPathMatch` objects for each match.
215+
216+
Raises:
217+
JSONPathSyntaxError: If the path is invalid.
218+
JSONPathTypeError: If a filter expression attempts to use types in
219+
an incompatible way.
220+
"""
221+
return (
222+
STRICT_ENV.finditer(path, data, filter_context=filter_context)
223+
if strict
224+
else DEFAULT_ENV.finditer(path, data, filter_context=filter_context)
225+
)
226+
227+
228+
async def finditer_async(
229+
path: str,
230+
data: JSONData,
231+
*,
232+
filter_context: Optional[FilterContextVars] = None,
233+
strict: bool = False,
234+
) -> AsyncIterable[JSONPathMatch]:
235+
"""Find all objects in _data_ matching the JSONPath _path_.
236+
237+
If _data_ is a string or a file-like objects, it will be loaded
238+
using `json.loads()` and the default `JSONDecoder`.
239+
240+
Arguments:
241+
path: The JSONPath as a string.
242+
data: A JSON document or Python object implementing the `Sequence`
243+
or `Mapping` interfaces.
244+
filter_context: Arbitrary data made available to filters using
245+
the _filter context_ selector.
246+
strict: When `True`, compile and evaluate with strict compliance with
247+
RFC 9535.
248+
249+
Returns:
250+
A list of matched objects. If there are no matches, the list will
251+
be empty.
252+
253+
Raises:
254+
JSONPathSyntaxError: If the path is invalid.
255+
JSONPathTypeError: If a filter expression attempts to use types in
256+
an incompatible way.
257+
"""
258+
return (
259+
await STRICT_ENV.finditer_async(path, data, filter_context=filter_context)
260+
if strict
261+
else await DEFAULT_ENV.finditer_async(path, data, filter_context=filter_context)
262+
)
263+
264+
265+
def match(
266+
path: str,
267+
data: JSONData,
268+
*,
269+
filter_context: Optional[FilterContextVars] = None,
270+
strict: bool = False,
271+
) -> Union[JSONPathMatch, None]:
272+
"""Return a `JSONPathMatch` instance for the first object found in _data_.
273+
274+
`None` is returned if there are no matches.
275+
276+
Arguments:
277+
path: The JSONPath as a string.
278+
data: A JSON document or Python object implementing the `Sequence`
279+
or `Mapping` interfaces.
280+
filter_context: Arbitrary data made available to filters using
281+
the _filter context_ selector.
282+
strict: When `True`, compile and evaluate with strict compliance with
283+
RFC 9535.
284+
285+
Returns:
286+
A `JSONPathMatch` object for the first match, or `None` if there were
287+
no matches.
288+
289+
Raises:
290+
JSONPathSyntaxError: If the path is invalid.
291+
JSONPathTypeError: If a filter expression attempts to use types in
292+
an incompatible way.
293+
"""
294+
return (
295+
STRICT_ENV.match(path, data, filter_context=filter_context)
296+
if strict
297+
else DEFAULT_ENV.match(path, data, filter_context=filter_context)
298+
)
299+
300+
301+
def query(
302+
path: str,
303+
data: JSONData,
304+
*,
305+
filter_context: Optional[FilterContextVars] = None,
306+
strict: bool = False,
307+
) -> Query:
308+
"""Return a `Query` iterator over matches found by applying _path_ to _data_.
309+
310+
`Query` objects are iterable.
311+
312+
```
313+
for match in jsonpath.query("$.foo..bar", data):
314+
...
315+
```
316+
317+
You can skip and limit results with `Query.skip()` and `Query.limit()`.
318+
319+
```
320+
matches = (
321+
jsonpath.query("$.foo..bar", data)
322+
.skip(5)
323+
.limit(10)
324+
)
325+
326+
for match in matches
327+
...
328+
```
329+
330+
`Query.tail()` will get the last _n_ results.
331+
332+
```
333+
for match in jsonpath.query("$.foo..bar", data).tail(5):
334+
...
335+
```
336+
337+
Get values for each match using `Query.values()`.
338+
339+
```
340+
for obj in jsonpath.query("$.foo..bar", data).limit(5).values():
341+
...
342+
```
343+
344+
Arguments:
345+
path: The JSONPath as a string.
346+
data: A JSON document or Python object implementing the `Sequence`
347+
or `Mapping` interfaces.
348+
filter_context: Arbitrary data made available to filters using
349+
the _filter context_ selector.
350+
strict: When `True`, compile and evaluate with strict compliance with
351+
RFC 9535.
352+
353+
Returns:
354+
A query iterator.
355+
356+
Raises:
357+
JSONPathSyntaxError: If the path is invalid.
358+
JSONPathTypeError: If a filter expression attempts to use types in
359+
an incompatible way.
360+
"""
361+
return (
362+
STRICT_ENV.query(path, data, filter_context=filter_context)
363+
if strict
364+
else DEFAULT_ENV.query(path, data, filter_context=filter_context)
365+
)

jsonpath/_types.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from __future__ import annotations
2+
3+
from io import IOBase
4+
from typing import Any
5+
from typing import Mapping
6+
from typing import Sequence
7+
from typing import Union
8+
9+
JSONScalar = Union[str, int, float, bool, None]
10+
"""A scalar JSON-like value.
11+
12+
This includes primitive types that can appear in JSON:
13+
string, number, boolean, or null.
14+
"""
15+
16+
JSON = Union[JSONScalar, Sequence[Any], Mapping[str, Any]]
17+
"""A JSON-like data structure.
18+
19+
This covers scalars, sequences (e.g. lists, tuples), and mappings (e.g.
20+
dictionaries with string keys). Values inside may be untyped (`Any`) rather
21+
than recursively constrained to `JSON` for flexibility.
22+
"""
23+
24+
JSONData = Union[str, IOBase, JSON]
25+
"""Input representing JSON content.
26+
27+
Accepts:
28+
- a JSON-like object (`JSON`),
29+
- a raw JSON string,
30+
- or a file-like object containing JSON data.
31+
"""

jsonpath/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def path_sub_command(parser: argparse.ArgumentParser) -> None: # noqa: D103
1919
parser.set_defaults(func=handle_path_command)
2020
group = parser.add_mutually_exclusive_group(required=True)
2121

22+
# TODO: add "strict" argument
23+
2224
group.add_argument(
2325
"-q",
2426
"--query",

0 commit comments

Comments
 (0)