python-jsonpath 1.1.0__tar.gz → 1.2.0__tar.gz

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.
Files changed (36) hide show
  1. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/.gitignore +4 -1
  2. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/PKG-INFO +12 -3
  3. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/README.md +11 -2
  4. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/__about__.py +1 -1
  5. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/__init__.py +8 -2
  6. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/env.py +20 -4
  7. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/filter.py +10 -6
  8. python_jsonpath-1.2.0/jsonpath/fluent_api.py +286 -0
  9. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/lex.py +3 -2
  10. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/parse.py +53 -1
  11. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/patch.py +116 -0
  12. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/path.py +49 -0
  13. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/pointer.py +4 -0
  14. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/selectors.py +35 -11
  15. python_jsonpath-1.1.0/jsonpath/fluent_api.py +0 -161
  16. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/LICENSE.txt +0 -0
  17. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/__main__.py +0 -0
  18. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/_data.py +0 -0
  19. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/cli.py +0 -0
  20. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/exceptions.py +0 -0
  21. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/__init__.py +0 -0
  22. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/arguments.py +0 -0
  23. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/count.py +0 -0
  24. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/filter_function.py +0 -0
  25. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/is_instance.py +0 -0
  26. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/keys.py +0 -0
  27. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/length.py +0 -0
  28. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/match.py +0 -0
  29. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/search.py +0 -0
  30. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/typeof.py +0 -0
  31. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/function_extensions/value.py +0 -0
  32. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/match.py +0 -0
  33. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/py.typed +0 -0
  34. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/stream.py +0 -0
  35. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/jsonpath/token.py +0 -0
  36. {python_jsonpath-1.1.0 → python_jsonpath-1.2.0}/pyproject.toml +0 -0
@@ -85,4 +85,7 @@ profile_.py
85
85
 
86
86
  # Test fixtures
87
87
  comparison_regression_suite.yaml
88
- cts.json
88
+ cts.json
89
+
90
+ # System
91
+ .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-jsonpath
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: JSONPath, JSON Pointer and JSON Patch for Python.
5
5
  Project-URL: Documentation, https://jg-rp.github.io/python-jsonpath/
6
6
  Project-URL: Issues, https://github.com/jg-rp/python-jsonpath/issues
@@ -56,6 +56,7 @@ We follow <a href="https://datatracker.ietf.org/doc/html/rfc9535">RFC 9535</a> a
56
56
 
57
57
  - [Install](#install)
58
58
  - [Links](#links)
59
+ - [Related projects](#related-projects)
59
60
  - [Examples](#examples)
60
61
  - [License](#license)
61
62
 
@@ -88,6 +89,14 @@ conda install -c conda-forge python-jsonpath
88
89
  - Source code: https://github.com/jg-rp/python-jsonpath
89
90
  - Issue tracker: https://github.com/jg-rp/python-jsonpath/issues
90
91
 
92
+ ## Related projects
93
+
94
+ - [Python JSONPath RFC 9535](https://github.com/jg-rp/python-jsonpath-rfc9535) - An implementation of JSONPath that follows RFC 9535 much more strictly. If you require maximum interoperability with JSONPath implemented in other languages - at the expense of extra features - choose python-jsonpath-rfc9535 over python-jsonpath.
95
+
96
+ python-jsonpath-rfc9535 matches RFC 9535's JSONPath model internally and is careful to match the spec's terminology. It also includes utilities for verifying and testing the [JSONPath Compliance Test Suite](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite). Most notably the nondeterministic behavior of some JSONPath selectors.
97
+
98
+ - [JSON P3](https://github.com/jg-rp/json-p3) - RFC 9535 implemented in TypeScript. JSON P3 does not include all the non-standard features of Python JSONPath, but does define some optional [extra syntax](https://jg-rp.github.io/json-p3/guides/jsonpath-extra).
99
+
91
100
  ## Examples
92
101
 
93
102
  ### JSONPath
@@ -110,7 +119,7 @@ print(user_names) # ['John', 'Sally', 'Jane']
110
119
 
111
120
  ### JSON Pointer
112
121
 
113
- Since version 0.8.0, we include an [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) compliant implementation of JSON Pointer. See JSON Pointer [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#pointerresolvepointer-data), [guide](https://jg-rp.github.io/python-jsonpath/pointers/) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPointer)
122
+ We include an [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) compliant implementation of JSON Pointer. See JSON Pointer [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#pointerresolvepointer-data), [guide](https://jg-rp.github.io/python-jsonpath/pointers/) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPointer)
114
123
 
115
124
  ```python
116
125
  from jsonpath import pointer
@@ -133,7 +142,7 @@ print(jane_score) # 55
133
142
 
134
143
  ### JSON Patch
135
144
 
136
- Since version 0.8.0, we also include an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) compliant implementation of JSON Patch. See JSON Patch [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#patchapplypatch-data) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPatch)
145
+ We also include an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) compliant implementation of JSON Patch. See JSON Patch [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#patchapplypatch-data) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPatch)
137
146
 
138
147
  ```python
139
148
  from jsonpath import patch
@@ -31,6 +31,7 @@ We follow <a href="https://datatracker.ietf.org/doc/html/rfc9535">RFC 9535</a> a
31
31
 
32
32
  - [Install](#install)
33
33
  - [Links](#links)
34
+ - [Related projects](#related-projects)
34
35
  - [Examples](#examples)
35
36
  - [License](#license)
36
37
 
@@ -63,6 +64,14 @@ conda install -c conda-forge python-jsonpath
63
64
  - Source code: https://github.com/jg-rp/python-jsonpath
64
65
  - Issue tracker: https://github.com/jg-rp/python-jsonpath/issues
65
66
 
67
+ ## Related projects
68
+
69
+ - [Python JSONPath RFC 9535](https://github.com/jg-rp/python-jsonpath-rfc9535) - An implementation of JSONPath that follows RFC 9535 much more strictly. If you require maximum interoperability with JSONPath implemented in other languages - at the expense of extra features - choose python-jsonpath-rfc9535 over python-jsonpath.
70
+
71
+ python-jsonpath-rfc9535 matches RFC 9535's JSONPath model internally and is careful to match the spec's terminology. It also includes utilities for verifying and testing the [JSONPath Compliance Test Suite](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite). Most notably the nondeterministic behavior of some JSONPath selectors.
72
+
73
+ - [JSON P3](https://github.com/jg-rp/json-p3) - RFC 9535 implemented in TypeScript. JSON P3 does not include all the non-standard features of Python JSONPath, but does define some optional [extra syntax](https://jg-rp.github.io/json-p3/guides/jsonpath-extra).
74
+
66
75
  ## Examples
67
76
 
68
77
  ### JSONPath
@@ -85,7 +94,7 @@ print(user_names) # ['John', 'Sally', 'Jane']
85
94
 
86
95
  ### JSON Pointer
87
96
 
88
- Since version 0.8.0, we include an [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) compliant implementation of JSON Pointer. See JSON Pointer [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#pointerresolvepointer-data), [guide](https://jg-rp.github.io/python-jsonpath/pointers/) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPointer)
97
+ We include an [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) compliant implementation of JSON Pointer. See JSON Pointer [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#pointerresolvepointer-data), [guide](https://jg-rp.github.io/python-jsonpath/pointers/) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPointer)
89
98
 
90
99
  ```python
91
100
  from jsonpath import pointer
@@ -108,7 +117,7 @@ print(jane_score) # 55
108
117
 
109
118
  ### JSON Patch
110
119
 
111
- Since version 0.8.0, we also include an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) compliant implementation of JSON Patch. See JSON Patch [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#patchapplypatch-data) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPatch)
120
+ We also include an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) compliant implementation of JSON Patch. See JSON Patch [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#patchapplypatch-data) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPatch)
112
121
 
113
122
  ```python
114
123
  from jsonpath import patch
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2023-present James Prior <jamesgr.prior@gmail.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "1.1.0"
4
+ __version__ = "1.2.0"
@@ -3,11 +3,14 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
5
  from .env import JSONPathEnvironment
6
+ from .exceptions import JSONPatchError
7
+ from .exceptions import JSONPatchTestFailure
6
8
  from .exceptions import JSONPathError
7
9
  from .exceptions import JSONPathIndexError
8
10
  from .exceptions import JSONPathNameError
9
11
  from .exceptions import JSONPathSyntaxError
10
12
  from .exceptions import JSONPathTypeError
13
+ from .exceptions import JSONPointerEncodeError
11
14
  from .exceptions import JSONPointerError
12
15
  from .exceptions import JSONPointerIndexError
13
16
  from .exceptions import JSONPointerKeyError
@@ -17,6 +20,7 @@ from .exceptions import RelativeJSONPointerError
17
20
  from .exceptions import RelativeJSONPointerIndexError
18
21
  from .exceptions import RelativeJSONPointerSyntaxError
19
22
  from .filter import UNDEFINED
23
+ from .fluent_api import Projection
20
24
  from .fluent_api import Query
21
25
  from .lex import Lexer
22
26
  from .match import JSONPathMatch
@@ -31,13 +35,13 @@ from .pointer import resolve
31
35
  __all__ = (
32
36
  "compile",
33
37
  "CompoundJSONPath",
34
- "find",
35
38
  "findall_async",
36
39
  "findall",
37
40
  "finditer_async",
38
41
  "finditer",
39
- "first",
40
42
  "JSONPatch",
43
+ "JSONPatchError",
44
+ "JSONPatchTestFailure",
41
45
  "JSONPath",
42
46
  "JSONPathEnvironment",
43
47
  "JSONPathError",
@@ -47,6 +51,7 @@ __all__ = (
47
51
  "JSONPathSyntaxError",
48
52
  "JSONPathTypeError",
49
53
  "JSONPointer",
54
+ "JSONPointerEncodeError",
50
55
  "JSONPointerError",
51
56
  "JSONPointerIndexError",
52
57
  "JSONPointerKeyError",
@@ -55,6 +60,7 @@ __all__ = (
55
60
  "Lexer",
56
61
  "match",
57
62
  "Parser",
63
+ "Projection",
58
64
  "query",
59
65
  "Query",
60
66
  "RelativeJSONPointer",
@@ -1,4 +1,5 @@
1
1
  """Core JSONPath configuration object."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import re
@@ -317,7 +318,7 @@ class JSONPathEnvironment:
317
318
  data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
318
319
  filter_context: Optional[FilterContextVars] = None,
319
320
  ) -> Query:
320
- """Return a `Query` object over matches found by applying _path_ to _data_.
321
+ """Return a `Query` iterator over matches found by applying _path_ to _data_.
321
322
 
322
323
  `Query` objects are iterable.
323
324
 
@@ -352,8 +353,23 @@ class JSONPathEnvironment:
352
353
  for obj in jsonpath.query("$.foo..bar", data).limit(5).values():
353
354
  ...
354
355
  ```
356
+
357
+ Arguments:
358
+ path: The JSONPath as a string.
359
+ data: A JSON document or Python object implementing the `Sequence`
360
+ or `Mapping` interfaces.
361
+ filter_context: Arbitrary data made available to filters using
362
+ the _filter context_ selector.
363
+
364
+ Returns:
365
+ A query iterator.
366
+
367
+ Raises:
368
+ JSONPathSyntaxError: If the path is invalid.
369
+ JSONPathTypeError: If a filter expression attempts to use types in
370
+ an incompatible way.
355
371
  """
356
- return Query(self.finditer(path, data, filter_context=filter_context))
372
+ return Query(self.finditer(path, data, filter_context=filter_context), self)
357
373
 
358
374
  async def findall_async(
359
375
  self,
@@ -548,9 +564,9 @@ class JSONPathEnvironment:
548
564
  return self._lt(right, left) or self._eq(left, right)
549
565
  if operator == "<=":
550
566
  return self._lt(left, right) or self._eq(left, right)
551
- if operator == "in" and isinstance(right, Sequence):
567
+ if operator == "in" and isinstance(right, (Mapping, Sequence)):
552
568
  return left in right
553
- if operator == "contains" and isinstance(left, Sequence):
569
+ if operator == "contains" and isinstance(left, (Mapping, Sequence)):
554
570
  return right in left
555
571
  if operator == "=~" and isinstance(right, re.Pattern) and isinstance(left, str):
556
572
  return bool(right.fullmatch(left))
@@ -318,7 +318,7 @@ class PrefixExpression(FilterExpression):
318
318
  class InfixExpression(FilterExpression):
319
319
  """A pair of expressions and a comparison or logical operator."""
320
320
 
321
- __slots__ = ("left", "operator", "right")
321
+ __slots__ = ("left", "operator", "right", "logical")
322
322
 
323
323
  def __init__(
324
324
  self,
@@ -329,10 +329,11 @@ class InfixExpression(FilterExpression):
329
329
  self.left = left
330
330
  self.operator = operator
331
331
  self.right = right
332
+ self.logical = operator in ("&&", "||")
332
333
  super().__init__()
333
334
 
334
335
  def __str__(self) -> str:
335
- if self.operator in ("&&", "||"):
336
+ if self.logical:
336
337
  return f"({self.left} {self.operator} {self.right})"
337
338
  return f"{self.left} {self.operator} {self.right}"
338
339
 
@@ -346,22 +347,22 @@ class InfixExpression(FilterExpression):
346
347
 
347
348
  def evaluate(self, context: FilterContext) -> bool:
348
349
  left = self.left.evaluate(context)
349
- if isinstance(left, NodeList) and len(left) == 1:
350
+ if not self.logical and isinstance(left, NodeList) and len(left) == 1:
350
351
  left = left[0].obj
351
352
 
352
353
  right = self.right.evaluate(context)
353
- if isinstance(right, NodeList) and len(right) == 1:
354
+ if not self.logical and isinstance(right, NodeList) and len(right) == 1:
354
355
  right = right[0].obj
355
356
 
356
357
  return context.env.compare(left, self.operator, right)
357
358
 
358
359
  async def evaluate_async(self, context: FilterContext) -> bool:
359
360
  left = await self.left.evaluate_async(context)
360
- if isinstance(left, NodeList) and len(left) == 1:
361
+ if not self.logical and isinstance(left, NodeList) and len(left) == 1:
361
362
  left = left[0].obj
362
363
 
363
364
  right = await self.right.evaluate_async(context)
364
- if isinstance(right, NodeList) and len(right) == 1:
365
+ if not self.logical and isinstance(right, NodeList) and len(right) == 1:
365
366
  right = right[0].obj
366
367
 
367
368
  return context.env.compare(left, self.operator, right)
@@ -667,6 +668,9 @@ class CurrentKey(FilterExpression):
667
668
  super().__init__()
668
669
  self.volatile = True
669
670
 
671
+ def __str__(self) -> str:
672
+ return "#"
673
+
670
674
  def __eq__(self, other: object) -> bool:
671
675
  return isinstance(other, CurrentKey)
672
676
 
@@ -0,0 +1,286 @@
1
+ """A fluent API for working with `JSONPathMatch` iterators."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import collections
6
+ import itertools
7
+ from enum import Enum
8
+ from enum import auto
9
+ from typing import TYPE_CHECKING
10
+ from typing import Any
11
+ from typing import Dict
12
+ from typing import Iterable
13
+ from typing import Iterator
14
+ from typing import List
15
+ from typing import Mapping
16
+ from typing import Optional
17
+ from typing import Sequence
18
+ from typing import Tuple
19
+ from typing import Union
20
+
21
+ if TYPE_CHECKING:
22
+ from jsonpath import CompoundJSONPath
23
+ from jsonpath import JSONPath
24
+ from jsonpath import JSONPathEnvironment
25
+ from jsonpath import JSONPathMatch
26
+ from jsonpath import JSONPointer
27
+
28
+
29
+ class Projection(Enum):
30
+ """Projection style used by `Query.select()`."""
31
+
32
+ RELATIVE = auto()
33
+ """The default projection. Selections include parent arrays and objects relative
34
+ to the JSONPathMatch."""
35
+
36
+ ROOT = auto()
37
+ """Selections include parent arrays and objects relative to the root JSON value."""
38
+
39
+ FLAT = auto()
40
+ """All selections are appended to a new array/list, without arrays and objects
41
+ on the path to the selected value."""
42
+
43
+
44
+ class Query:
45
+ """A fluent API for managing `JSONPathMatch` iterators.
46
+
47
+ Usually you'll want to use `jsonpath.query()` or `JSONPathEnvironment.query()`
48
+ to create instances of `Query` rather than instantiating `Query` directly.
49
+
50
+ Arguments:
51
+ it: A `JSONPathMatch` iterable, as you'd get from `jsonpath.finditer()` or
52
+ `JSONPathEnvironment.finditer()`.
53
+
54
+ **New in version 1.1.0**
55
+ """
56
+
57
+ def __init__(self, it: Iterable[JSONPathMatch], env: JSONPathEnvironment) -> None:
58
+ self._it = iter(it)
59
+ self._env = env
60
+
61
+ def __iter__(self) -> Iterator[JSONPathMatch]:
62
+ return self._it
63
+
64
+ def limit(self, n: int) -> Query:
65
+ """Limit the query iterator to at most _n_ matches.
66
+
67
+ Raises:
68
+ ValueError: If _n_ < 0.
69
+ """
70
+ if n < 0:
71
+ raise ValueError("can't limit by a negative number of matches")
72
+
73
+ self._it = itertools.islice(self._it, n)
74
+ return self
75
+
76
+ def head(self, n: int) -> Query:
77
+ """Limit the query iterator to at most the first _n_ matches.
78
+
79
+ `head()` is an alias for `limit()`.
80
+
81
+ Raises:
82
+ ValueError: If _n_ < 0.
83
+ """
84
+ return self.limit(n)
85
+
86
+ def first(self, n: int) -> Query:
87
+ """Limit the query iterator to at most the first _n_ matches.
88
+
89
+ `first()` is an alias for `limit()`.
90
+
91
+ Raises:
92
+ ValueError: If _n_ < 0.
93
+ """
94
+ return self.limit(n)
95
+
96
+ def drop(self, n: int) -> Query:
97
+ """Skip up to _n_ matches from the query iterator.
98
+
99
+ Raises:
100
+ ValueError: If _n_ < 0.
101
+ """
102
+ if n < 0:
103
+ raise ValueError("can't drop a negative number of matches")
104
+
105
+ if n > 0:
106
+ next(itertools.islice(self._it, n, n), None)
107
+
108
+ return self
109
+
110
+ def skip(self, n: int) -> Query:
111
+ """Skip up to _n_ matches from the query iterator.
112
+
113
+ Raises:
114
+ ValueError: If _n_ < 0.
115
+ """
116
+ return self.drop(n)
117
+
118
+ def tail(self, n: int) -> Query:
119
+ """Drop matches up to the last _n_ matches from the iterator.
120
+
121
+ Raises:
122
+ ValueError: If _n_ < 0.
123
+ """
124
+ if n < 0:
125
+ raise ValueError("can't select a negative number of matches")
126
+
127
+ self._it = iter(collections.deque(self._it, maxlen=n))
128
+ return self
129
+
130
+ def last(self, n: int) -> Query:
131
+ """Drop up to the last _n_ matches from the iterator.
132
+
133
+ `last()` is an alias for `tail()`.
134
+
135
+ Raises:
136
+ ValueError: If _n_ < 0.
137
+ """
138
+ return self.tail(n)
139
+
140
+ def values(self) -> Iterable[object]:
141
+ """Return an iterable of objects associated with each match."""
142
+ return (m.obj for m in self._it)
143
+
144
+ def locations(self) -> Iterable[str]:
145
+ """Return an iterable of normalized paths, one for each match."""
146
+ return (m.path for m in self._it)
147
+
148
+ def items(self) -> Iterable[Tuple[str, object]]:
149
+ """Return an iterable of (path, object) tuples, one for each match."""
150
+ return ((m.path, m.obj) for m in self._it)
151
+
152
+ def pointers(self) -> Iterable[JSONPointer]:
153
+ """Return an iterable of JSONPointers, one for each match."""
154
+ return (m.pointer() for m in self._it)
155
+
156
+ def first_one(self) -> Optional[JSONPathMatch]:
157
+ """Return the first `JSONPathMatch` or `None` if there were no matches."""
158
+ try:
159
+ return next(self._it)
160
+ except StopIteration:
161
+ return None
162
+
163
+ def one(self) -> Optional[JSONPathMatch]:
164
+ """Return the first `JSONPathMatch` or `None` if there were no matches.
165
+
166
+ `one()` is an alias for `first_one()`.
167
+ """
168
+ return self.first_one()
169
+
170
+ def last_one(self) -> Optional[JSONPathMatch]:
171
+ """Return the last `JSONPathMatch` or `None` if there were no matches."""
172
+ try:
173
+ return next(iter(self.tail(1)))
174
+ except StopIteration:
175
+ return None
176
+
177
+ def tee(self, n: int = 2) -> Tuple[Query, ...]:
178
+ """Return _n_ independent queries by teeing this query's iterator.
179
+
180
+ It is not safe to use a `Query` instance after calling `tee()`.
181
+ """
182
+ return tuple(Query(it, self._env) for it in itertools.tee(self._it, n))
183
+
184
+ def take(self, n: int) -> Query:
185
+ """Return a new query iterating over the next _n_ matches.
186
+
187
+ It is safe to continue using this query after calling take.
188
+ """
189
+ return Query(list(itertools.islice(self._it, n)), self._env)
190
+
191
+ def select(
192
+ self,
193
+ *expressions: Union[str, JSONPath, CompoundJSONPath],
194
+ projection: Projection = Projection.RELATIVE,
195
+ ) -> Iterable[object]:
196
+ """Query projection using relative JSONPaths.
197
+
198
+ Arguments:
199
+ expressions: One or more JSONPath query expressions to select relative
200
+ to each match in this query iterator.
201
+ projection: The style of projection used when selecting values. Can be
202
+ one of `Projection.RELATIVE`, `Projection.ROOT` or `Projection.FLAT`.
203
+ Defaults to `Projection.RELATIVE`.
204
+
205
+ Returns:
206
+ An iterable of objects built from selecting _expressions_ relative to
207
+ each match from the current query.
208
+
209
+ **New in version 1.2.0**
210
+ """
211
+ return filter(
212
+ bool,
213
+ (self._select(m, expressions, projection) for m in self._it),
214
+ )
215
+
216
+ def _select(
217
+ self,
218
+ match: JSONPathMatch,
219
+ expressions: Tuple[Union[str, JSONPath, CompoundJSONPath], ...],
220
+ projection: Projection,
221
+ ) -> object:
222
+ if not isinstance(match.obj, (Mapping, Sequence)) or isinstance(match.obj, str):
223
+ return None
224
+
225
+ if projection == Projection.RELATIVE:
226
+ obj: Dict[Union[int, str], Any] = {}
227
+ for expr in expressions:
228
+ path = self._env.compile(expr) if isinstance(expr, str) else expr
229
+ for rel_match in path.finditer(match.obj): # type: ignore
230
+ _patch_obj(rel_match.parts, obj, rel_match.obj)
231
+
232
+ return _fix_sparse_arrays(obj)
233
+
234
+ if projection == Projection.FLAT:
235
+ arr: List[object] = []
236
+ for expr in expressions:
237
+ path = self._env.compile(expr) if isinstance(expr, str) else expr
238
+ for rel_match in path.finditer(match.obj): # type: ignore
239
+ arr.append(rel_match.obj)
240
+ return arr
241
+
242
+ # Project from the root document
243
+ obj = {}
244
+ for expr in expressions:
245
+ path = self._env.compile(expr) if isinstance(expr, str) else expr
246
+ for rel_match in path.finditer(match.obj): # type: ignore
247
+ _patch_obj(match.parts + rel_match.parts, obj, rel_match.obj)
248
+
249
+ return _fix_sparse_arrays(obj)
250
+
251
+
252
+ def _patch_obj(
253
+ parts: Tuple[Union[int, str], ...],
254
+ obj: Mapping[Union[str, int], Any],
255
+ value: object,
256
+ ) -> None:
257
+ _obj = obj
258
+
259
+ # For lack of a better idea, we're patching arrays to dictionaries with
260
+ # integer keys. This is to handle sparse array selections without having
261
+ # to keep track of indexes and how they map from the root JSON value to
262
+ # the selected JSON value.
263
+ #
264
+ # We'll fix these "sparse arrays" after the patch has been applied.
265
+ for part in parts[:-1]:
266
+ if part not in _obj:
267
+ _obj[part] = {} # type: ignore
268
+ _obj = _obj[part]
269
+
270
+ _obj[parts[-1]] = value # type: ignore
271
+
272
+
273
+ def _fix_sparse_arrays(obj: Any) -> object:
274
+ """Fix sparse arrays (dictionaries with integer keys)."""
275
+ if isinstance(obj, str) or not obj:
276
+ return obj
277
+
278
+ if isinstance(obj, Sequence):
279
+ return [_fix_sparse_arrays(e) for e in obj]
280
+
281
+ if isinstance(obj, Mapping):
282
+ if isinstance(next(iter(obj)), int):
283
+ return [_fix_sparse_arrays(v) for v in obj.values()]
284
+ return {k: _fix_sparse_arrays(v) for k, v in obj.items()}
285
+
286
+ return obj
@@ -1,4 +1,5 @@
1
1
  """JSONPath tokenization."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import re
@@ -138,8 +139,8 @@ class Lexer:
138
139
  (TOKEN_LIST_SLICE, self.slice_list_pattern),
139
140
  (TOKEN_FUNCTION, self.function_pattern),
140
141
  (TOKEN_DOT_PROPERTY, self.dot_property_pattern),
141
- (TOKEN_FLOAT, r"-?\d+\.\d*(?:e[+-]?\d+)?"),
142
- (TOKEN_INT, r"-?\d+(?P<G_EXP>e[+\-]?\d+)?\b"),
142
+ (TOKEN_FLOAT, r"-?\d+\.\d*(?:[eE][+-]?\d+)?"),
143
+ (TOKEN_INT, r"-?\d+(?P<G_EXP>[eE][+\-]?\d+)?\b"),
143
144
  (TOKEN_DDOT, r"\.\."),
144
145
  (TOKEN_AND, self.logical_and_pattern),
145
146
  (TOKEN_OR, self.logical_or_pattern),
@@ -30,6 +30,8 @@ from .filter import FunctionExtension
30
30
  from .filter import InfixExpression
31
31
  from .filter import IntegerLiteral
32
32
  from .filter import ListLiteral
33
+ from .filter import Literal
34
+ from .filter import Nil
33
35
  from .filter import Path
34
36
  from .filter import PrefixExpression
35
37
  from .filter import RegexLiteral
@@ -191,6 +193,23 @@ class Parser:
191
193
  "<=",
192
194
  "<",
193
195
  "!=",
196
+ "=~",
197
+ ]
198
+ )
199
+
200
+ # Infix operators that accept filter expression literals.
201
+ INFIX_LITERAL_OPERATORS = frozenset(
202
+ [
203
+ "==",
204
+ ">=",
205
+ ">",
206
+ "<=",
207
+ "<",
208
+ "!=",
209
+ "<>",
210
+ "=~",
211
+ "in",
212
+ "contains",
194
213
  ]
195
214
  )
196
215
 
@@ -455,6 +474,12 @@ class Parser:
455
474
  stream.expect_peek(TOKEN_COMMA)
456
475
  stream.next_token()
457
476
 
477
+ if stream.peek.kind == TOKEN_RBRACKET:
478
+ raise JSONPathSyntaxError(
479
+ "unexpected trailing comma",
480
+ token=stream.peek,
481
+ )
482
+
458
483
  stream.next_token()
459
484
 
460
485
  if not list_items:
@@ -477,6 +502,13 @@ class Parser:
477
502
  f"result of {expr.name}() must be compared", token=tok
478
503
  )
479
504
 
505
+ if isinstance(expr, (Literal, Nil)):
506
+ raise JSONPathSyntaxError(
507
+ "filter expression literals outside of "
508
+ "function expressions must be compared",
509
+ token=tok,
510
+ )
511
+
480
512
  return Filter(env=self.env, token=tok, expression=BooleanExpression(expr))
481
513
 
482
514
  def parse_boolean(self, stream: TokenStream) -> FilterExpression:
@@ -520,6 +552,20 @@ class Parser:
520
552
  self._raise_for_non_comparable_function(left, tok)
521
553
  self._raise_for_non_comparable_function(right, tok)
522
554
 
555
+ if operator not in self.INFIX_LITERAL_OPERATORS:
556
+ if isinstance(left, (Literal, Nil)):
557
+ raise JSONPathSyntaxError(
558
+ "filter expression literals outside of "
559
+ "function expressions must be compared",
560
+ token=tok,
561
+ )
562
+ if isinstance(right, (Literal, Nil)):
563
+ raise JSONPathSyntaxError(
564
+ "filter expression literals outside of "
565
+ "function expressions must be compared",
566
+ token=tok,
567
+ )
568
+
523
569
  return InfixExpression(left, operator, right)
524
570
 
525
571
  def parse_grouped_expression(self, stream: TokenStream) -> FilterExpression:
@@ -532,6 +578,13 @@ class Parser:
532
578
  raise JSONPathSyntaxError(
533
579
  "unbalanced parentheses", token=stream.current
534
580
  )
581
+
582
+ if stream.current.kind not in self.BINARY_OPERATORS:
583
+ raise JSONPathSyntaxError(
584
+ f"expected an expression, found '{stream.current.value}'",
585
+ token=stream.current,
586
+ )
587
+
535
588
  expr = self.parse_infix_expression(stream, expr)
536
589
 
537
590
  stream.expect(TOKEN_RPAREN)
@@ -539,7 +592,6 @@ class Parser:
539
592
 
540
593
  def parse_root_path(self, stream: TokenStream) -> FilterExpression:
541
594
  root = stream.next_token()
542
- assert root.kind in {TOKEN_ROOT, TOKEN_FAKE_ROOT} # XXX:
543
595
  return RootPath(
544
596
  JSONPath(
545
597
  env=self.env,
@@ -1,4 +1,5 @@
1
1
  """JSON Patch, as per RFC 6902."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import copy
@@ -85,6 +86,78 @@ class OpAdd(Op):
85
86
  return {"op": self.name, "path": str(self.path), "value": self.value}
86
87
 
87
88
 
89
+ class OpAddNe(OpAdd):
90
+ """A non-standard _add if not exists_ operation.
91
+
92
+ This is like _OpAdd_, but only adds object/dict keys/values if they key does
93
+ not already exist.
94
+
95
+ **New in version 1.2.0**
96
+ """
97
+
98
+ __slots__ = ("path", "value")
99
+
100
+ name = "addne"
101
+
102
+ def apply(
103
+ self, data: Union[MutableSequence[object], MutableMapping[str, object]]
104
+ ) -> Union[MutableSequence[object], MutableMapping[str, object]]:
105
+ """Apply this patch operation to _data_."""
106
+ parent, obj = self.path.resolve_parent(data)
107
+ if parent is None:
108
+ # Replace the root object.
109
+ # The following op, if any, will raise a JSONPatchError if needed.
110
+ return self.value # type: ignore
111
+
112
+ target = self.path.parts[-1]
113
+ if isinstance(parent, MutableSequence):
114
+ if obj is UNDEFINED:
115
+ parent.append(self.value)
116
+ else:
117
+ parent.insert(int(target), self.value)
118
+ elif isinstance(parent, MutableMapping) and target not in parent:
119
+ parent[target] = self.value
120
+ return data
121
+
122
+
123
+ class OpAddAp(OpAdd):
124
+ """A non-standard add operation that appends to arrays/lists .
125
+
126
+ This is like _OpAdd_, but assumes an index of "-" if the path can not
127
+ be resolved.
128
+
129
+ **New in version 1.2.0**
130
+ """
131
+
132
+ __slots__ = ("path", "value")
133
+
134
+ name = "addap"
135
+
136
+ def apply(
137
+ self, data: Union[MutableSequence[object], MutableMapping[str, object]]
138
+ ) -> Union[MutableSequence[object], MutableMapping[str, object]]:
139
+ """Apply this patch operation to _data_."""
140
+ parent, obj = self.path.resolve_parent(data)
141
+ if parent is None:
142
+ # Replace the root object.
143
+ # The following op, if any, will raise a JSONPatchError if needed.
144
+ return self.value # type: ignore
145
+
146
+ target = self.path.parts[-1]
147
+ if isinstance(parent, MutableSequence):
148
+ if obj is UNDEFINED:
149
+ parent.append(self.value)
150
+ else:
151
+ parent.insert(int(target), self.value)
152
+ elif isinstance(parent, MutableMapping):
153
+ parent[target] = self.value
154
+ else:
155
+ raise JSONPatchError(
156
+ f"unexpected operation on {parent.__class__.__name__!r}"
157
+ )
158
+ return data
159
+
160
+
88
161
  class OpRemove(Op):
89
162
  """The JSON Patch _remove_ operation."""
90
163
 
@@ -340,6 +413,16 @@ class JSONPatch:
340
413
  path=self._op_pointer(operation, "path", "add", i),
341
414
  value=self._op_value(operation, "value", "add", i),
342
415
  )
416
+ elif op == "addne":
417
+ self.addne(
418
+ path=self._op_pointer(operation, "path", "addne", i),
419
+ value=self._op_value(operation, "value", "addne", i),
420
+ )
421
+ elif op == "addap":
422
+ self.addne(
423
+ path=self._op_pointer(operation, "path", "addap", i),
424
+ value=self._op_value(operation, "value", "addap", i),
425
+ )
343
426
  elif op == "remove":
344
427
  self.remove(path=self._op_pointer(operation, "path", "add", i))
345
428
  elif op == "replace":
@@ -424,6 +507,38 @@ class JSONPatch:
424
507
  self.ops.append(OpAdd(path=pointer, value=value))
425
508
  return self
426
509
 
510
+ def addne(self: Self, path: Union[str, JSONPointer], value: object) -> Self:
511
+ """Append an _addne_ operation to this patch.
512
+
513
+ Arguments:
514
+ path: A string representation of a JSON Pointer, or one that has
515
+ already been parsed.
516
+ value: The object to add.
517
+
518
+ Returns:
519
+ This `JSONPatch` instance, so we can build a JSON Patch by chaining
520
+ calls to JSON Patch operation methods.
521
+ """
522
+ pointer = self._ensure_pointer(path)
523
+ self.ops.append(OpAddNe(path=pointer, value=value))
524
+ return self
525
+
526
+ def addap(self: Self, path: Union[str, JSONPointer], value: object) -> Self:
527
+ """Append an _addap_ operation to this patch.
528
+
529
+ Arguments:
530
+ path: A string representation of a JSON Pointer, or one that has
531
+ already been parsed.
532
+ value: The object to add.
533
+
534
+ Returns:
535
+ This `JSONPatch` instance, so we can build a JSON Patch by chaining
536
+ calls to JSON Patch operation methods.
537
+ """
538
+ pointer = self._ensure_pointer(path)
539
+ self.ops.append(OpAddAp(path=pointer, value=value))
540
+ return self
541
+
427
542
  def remove(self: Self, path: Union[str, JSONPointer]) -> Self:
428
543
  """Append a _remove_ operation to this patch.
429
544
 
@@ -551,6 +666,7 @@ class JSONPatch:
551
666
  raise JSONPatchError(f"{err} ({op.name}:{i})") from err
552
667
  except (JSONPointerError, JSONPatchError) as err:
553
668
  raise JSONPatchError(f"{err} ({op.name}:{i})") from err
669
+
554
670
  return _data
555
671
 
556
672
  def asdicts(self) -> List[Dict[str, object]]:
@@ -15,6 +15,7 @@ from typing import TypeVar
15
15
  from typing import Union
16
16
 
17
17
  from jsonpath._data import load_data
18
+ from jsonpath.fluent_api import Query
18
19
  from jsonpath.match import FilterContextVars
19
20
  from jsonpath.match import JSONPathMatch
20
21
  from jsonpath.selectors import IndexSelector
@@ -210,6 +211,30 @@ class JSONPath:
210
211
  except StopIteration:
211
212
  return None
212
213
 
214
+ def query(
215
+ self,
216
+ data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
217
+ *,
218
+ filter_context: Optional[FilterContextVars] = None,
219
+ ) -> Query:
220
+ """Return a `Query` iterator over matches found by applying this path to _data_.
221
+
222
+ Arguments:
223
+ data: A JSON document or Python object implementing the `Sequence`
224
+ or `Mapping` interfaces.
225
+ filter_context: Arbitrary data made available to filters using
226
+ the _filter context_ selector.
227
+
228
+ Returns:
229
+ A query iterator.
230
+
231
+ Raises:
232
+ JSONPathSyntaxError: If the path is invalid.
233
+ JSONPathTypeError: If a filter expression attempts to use types in
234
+ an incompatible way.
235
+ """
236
+ return Query(self.finditer(data, filter_context=filter_context), self.env)
237
+
213
238
  def empty(self) -> bool:
214
239
  """Return `True` if this path has no selectors."""
215
240
  return not bool(self.selectors)
@@ -407,6 +432,30 @@ class CompoundJSONPath:
407
432
 
408
433
  return matches
409
434
 
435
+ def query(
436
+ self,
437
+ data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
438
+ *,
439
+ filter_context: Optional[FilterContextVars] = None,
440
+ ) -> Query:
441
+ """Return a `Query` iterator over matches found by applying this path to _data_.
442
+
443
+ Arguments:
444
+ data: A JSON document or Python object implementing the `Sequence`
445
+ or `Mapping` interfaces.
446
+ filter_context: Arbitrary data made available to filters using
447
+ the _filter context_ selector.
448
+
449
+ Returns:
450
+ A query iterator.
451
+
452
+ Raises:
453
+ JSONPathSyntaxError: If the path is invalid.
454
+ JSONPathTypeError: If a filter expression attempts to use types in
455
+ an incompatible way.
456
+ """
457
+ return Query(self.finditer(data, filter_context=filter_context), self.env)
458
+
410
459
  def union(self, path: JSONPath) -> CompoundJSONPath:
411
460
  """Union of this path and another path."""
412
461
  return self.__class__(
@@ -1,4 +1,5 @@
1
1
  """JSON Pointer. See https://datatracker.ietf.org/doc/html/rfc6901."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import codecs
@@ -326,6 +327,9 @@ class JSONPointer:
326
327
  def __eq__(self, other: object) -> bool:
327
328
  return isinstance(other, JSONPointer) and self.parts == other.parts
328
329
 
330
+ def __hash__(self) -> int:
331
+ return hash(self.parts)
332
+
329
333
  def __repr__(self) -> str:
330
334
  return f"JSONPointer({self._s!r})"
331
335
 
@@ -1,4 +1,4 @@
1
- """JSONPath selector objects, as returned from `Parser.parse`."""
1
+ """JSONPath segments and selectors, as returned from `Parser.parse`."""
2
2
  from __future__ import annotations
3
3
 
4
4
  from abc import ABC
@@ -28,7 +28,7 @@ if TYPE_CHECKING:
28
28
 
29
29
 
30
30
  class JSONPathSelector(ABC):
31
- """Base class for all JSONPath selectors."""
31
+ """Base class for all JSONPath segments and selectors."""
32
32
 
33
33
  __slots__ = ("env", "token")
34
34
 
@@ -38,7 +38,17 @@ class JSONPathSelector(ABC):
38
38
 
39
39
  @abstractmethod
40
40
  def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
41
- """Expand matches from previous JSONPath selectors in to new matches."""
41
+ """Apply the segment/selector to each node in _matches_.
42
+
43
+ Arguments:
44
+ matches: Nodes matched by preceding segments/selectors. This is like
45
+ a lazy _NodeList_, as described in RFC 9535, but each match carries
46
+ more than the node's value and location.
47
+
48
+ Returns:
49
+ The `JSONPathMatch` instances created by applying this selector to each
50
+ preceding node.
51
+ """
42
52
 
43
53
  @abstractmethod
44
54
  def resolve_async(
@@ -48,7 +58,7 @@ class JSONPathSelector(ABC):
48
58
 
49
59
 
50
60
  class PropertySelector(JSONPathSelector):
51
- """A JSONPath property."""
61
+ """A shorthand or bracketed property selector."""
52
62
 
53
63
  __slots__ = ("name", "shorthand")
54
64
 
@@ -115,7 +125,12 @@ class PropertySelector(JSONPathSelector):
115
125
 
116
126
 
117
127
  class IndexSelector(JSONPathSelector):
118
- """Dotted and bracketed sequence access by index."""
128
+ """Select an element from an array by index.
129
+
130
+ Considering we don't require mapping (JSON object) keys/properties to
131
+ be quoted, and that we support mappings with numeric keys, we also check
132
+ to see if the "index" is a mapping key, which is non-standard.
133
+ """
119
134
 
120
135
  __slots__ = ("index", "_as_key")
121
136
 
@@ -213,7 +228,10 @@ class IndexSelector(JSONPathSelector):
213
228
 
214
229
 
215
230
  class KeysSelector(JSONPathSelector):
216
- """Select an mapping's keys/properties."""
231
+ """Select mapping/object keys/properties.
232
+
233
+ NOTE: This is a non-standard selector.
234
+ """
217
235
 
218
236
  __slots__ = ("shorthand",)
219
237
 
@@ -354,7 +372,7 @@ class SliceSelector(JSONPathSelector):
354
372
 
355
373
 
356
374
  class WildSelector(JSONPathSelector):
357
- """Wildcard expansion selector."""
375
+ """Select all items from a sequence/array or values from a mapping/object."""
358
376
 
359
377
  __slots__ = ("shorthand",)
360
378
 
@@ -433,7 +451,10 @@ class WildSelector(JSONPathSelector):
433
451
 
434
452
 
435
453
  class RecursiveDescentSelector(JSONPathSelector):
436
- """A JSONPath selector that visits all objects recursively."""
454
+ """A JSONPath selector that visits all nodes recursively.
455
+
456
+ NOTE: Strictly this is a "segment", not a "selector".
457
+ """
437
458
 
438
459
  def __str__(self) -> str:
439
460
  return ".."
@@ -504,7 +525,10 @@ async def _alist(it: List[T]) -> AsyncIterable[T]:
504
525
 
505
526
 
506
527
  class ListSelector(JSONPathSelector):
507
- """A JSONPath selector representing a list of properties, slices or indices."""
528
+ """A bracketed list of selectors, the results of which are concatenated together.
529
+
530
+ NOTE: Strictly this is a "segment", not a "selector".
531
+ """
508
532
 
509
533
  __slots__ = ("items",)
510
534
 
@@ -555,7 +579,7 @@ class ListSelector(JSONPathSelector):
555
579
 
556
580
 
557
581
  class Filter(JSONPathSelector):
558
- """A filter selector."""
582
+ """Filter sequence/array items or mapping/object values with a filter expression."""
559
583
 
560
584
  __slots__ = ("expression", "cacheable_nodes")
561
585
 
@@ -713,7 +737,7 @@ class Filter(JSONPathSelector):
713
737
 
714
738
 
715
739
  class FilterContext:
716
- """A filter expression context."""
740
+ """Contextual information and data for evaluating a filter expression."""
717
741
 
718
742
  __slots__ = (
719
743
  "current_key",
@@ -1,161 +0,0 @@
1
- """A fluent API for managing JSONPathMatch iterators."""
2
- from __future__ import annotations
3
-
4
- import collections
5
- import itertools
6
- from typing import TYPE_CHECKING
7
- from typing import Iterable
8
- from typing import Iterator
9
- from typing import Optional
10
- from typing import Tuple
11
-
12
- if TYPE_CHECKING:
13
- from jsonpath import JSONPathMatch
14
- from jsonpath import JSONPointer
15
-
16
-
17
- class Query:
18
- """A fluent API for managing `JSONPathMatch` iterators.
19
-
20
- Usually you'll want to use `jsonpath.query()` or `JSONPathEnvironment.query()`
21
- to create instances of `Query` rather than instantiating `Query` directly.
22
-
23
- Arguments:
24
- it: A `JSONPathMatch` iterable, as you'd get from `jsonpath.finditer()` or
25
- `JSONPathEnvironment.finditer()`.
26
-
27
- **New in version 1.1.0**
28
- """
29
-
30
- def __init__(self, it: Iterable[JSONPathMatch]) -> None:
31
- self._it = iter(it)
32
-
33
- def __iter__(self) -> Iterator[JSONPathMatch]:
34
- return self._it
35
-
36
- def limit(self, n: int) -> Query:
37
- """Limit the query iterator to at most _n_ matches.
38
-
39
- Raises:
40
- ValueError: If _n_ < 0.
41
- """
42
- if n < 0:
43
- raise ValueError("can't limit by a negative number of matches")
44
-
45
- self._it = itertools.islice(self._it, n)
46
- return self
47
-
48
- def head(self, n: int) -> Query:
49
- """Limit the query iterator to at most the first _n_ matches.
50
-
51
- `head()` is an alias for `limit()`.
52
-
53
- Raises:
54
- ValueError: If _n_ < 0.
55
- """
56
- return self.limit(n)
57
-
58
- def first(self, n: int) -> Query:
59
- """Limit the query iterator to at most the first _n_ matches.
60
-
61
- `first()` is an alias for `limit()`.
62
-
63
- Raises:
64
- ValueError: If _n_ < 0.
65
- """
66
- return self.limit(n)
67
-
68
- def drop(self, n: int) -> Query:
69
- """Skip up to _n_ matches from the query iterator.
70
-
71
- Raises:
72
- ValueError: If _n_ < 0.
73
- """
74
- if n < 0:
75
- raise ValueError("can't drop a negative number of matches")
76
-
77
- if n > 0:
78
- next(itertools.islice(self._it, n, n), None)
79
-
80
- return self
81
-
82
- def skip(self, n: int) -> Query:
83
- """Skip up to _n_ matches from the query iterator.
84
-
85
- Raises:
86
- ValueError: If _n_ < 0.
87
- """
88
- return self.drop(n)
89
-
90
- def tail(self, n: int) -> Query:
91
- """Drop matches up to the last _n_ matches from the iterator.
92
-
93
- Raises:
94
- ValueError: If _n_ < 0.
95
- """
96
- if n < 0:
97
- raise ValueError("can't select a negative number of matches")
98
-
99
- self._it = iter(collections.deque(self._it, maxlen=n))
100
- return self
101
-
102
- def last(self, n: int) -> Query:
103
- """Drop up to the last _n_ matches from the iterator.
104
-
105
- `last()` is an alias for `tail()`.
106
-
107
- Raises:
108
- ValueError: If _n_ < 0.
109
- """
110
- return self.tail(n)
111
-
112
- def values(self) -> Iterable[object]:
113
- """Return an iterable of objects associated with each match."""
114
- return (m.obj for m in self._it)
115
-
116
- def locations(self) -> Iterable[str]:
117
- """Return an iterable of normalized paths, one for each match."""
118
- return (m.path for m in self._it)
119
-
120
- def items(self) -> Iterable[Tuple[str, object]]:
121
- """Return an iterable of (object, path) tuples, one for each match."""
122
- return ((m.path, m.obj) for m in self._it)
123
-
124
- def pointers(self) -> Iterable[JSONPointer]:
125
- """Return an iterable of JSONPointers, one for each match."""
126
- return (m.pointer() for m in self._it)
127
-
128
- def first_one(self) -> Optional[JSONPathMatch]:
129
- """Return the first `JSONPathMatch` or `None` if there were no matches."""
130
- try:
131
- return next(self._it)
132
- except StopIteration:
133
- return None
134
-
135
- def one(self) -> Optional[JSONPathMatch]:
136
- """Return the first `JSONPathMatch` or `None` if there were no matches.
137
-
138
- `one()` is an alias for `first_one()`.
139
- """
140
- return self.first_one()
141
-
142
- def last_one(self) -> Optional[JSONPathMatch]:
143
- """Return the last `JSONPathMatch` or `None` if there were no matches."""
144
- try:
145
- return next(iter(self.tail(1)))
146
- except StopIteration:
147
- return None
148
-
149
- def tee(self, n: int = 2) -> Tuple[Query, ...]:
150
- """Return _n_ independent queries by teeing this query's iterator.
151
-
152
- It is not safe to use a `Query` instance after calling `tee()`.
153
- """
154
- return tuple(Query(it) for it in itertools.tee(self._it, n))
155
-
156
- def take(self, n: int) -> Query:
157
- """Return a new query iterating over the next _n_ matches.
158
-
159
- It is safe to continue using this query after calling take.
160
- """
161
- return Query(list(itertools.islice(self._it, n)))