python-jsonpath 1.0.0__tar.gz → 1.1.1__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 (35) hide show
  1. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/PKG-INFO +1 -1
  2. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/__about__.py +1 -1
  3. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/__init__.py +4 -0
  4. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/env.py +52 -7
  5. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/filter.py +8 -6
  6. python_jsonpath-1.1.1/jsonpath/fluent_api.py +161 -0
  7. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/match.py +5 -0
  8. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/parse.py +7 -5
  9. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/selectors.py +42 -18
  10. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/pyproject.toml +36 -33
  11. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/.gitignore +0 -0
  12. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/LICENSE.txt +0 -0
  13. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/README.md +0 -0
  14. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/__main__.py +0 -0
  15. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/_data.py +0 -0
  16. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/cli.py +0 -0
  17. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/exceptions.py +0 -0
  18. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/__init__.py +0 -0
  19. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/arguments.py +0 -0
  20. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/count.py +0 -0
  21. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/filter_function.py +0 -0
  22. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/is_instance.py +0 -0
  23. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/keys.py +0 -0
  24. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/length.py +0 -0
  25. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/match.py +0 -0
  26. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/search.py +0 -0
  27. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/typeof.py +0 -0
  28. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/function_extensions/value.py +0 -0
  29. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/lex.py +0 -0
  30. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/patch.py +0 -0
  31. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/path.py +0 -0
  32. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/pointer.py +0 -0
  33. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/py.typed +0 -0
  34. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/stream.py +0 -0
  35. {python_jsonpath-1.0.0 → python_jsonpath-1.1.1}/jsonpath/token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-jsonpath
3
- Version: 1.0.0
3
+ Version: 1.1.1
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
@@ -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.0.0"
4
+ __version__ = "1.1.1"
@@ -17,6 +17,7 @@ from .exceptions import RelativeJSONPointerError
17
17
  from .exceptions import RelativeJSONPointerIndexError
18
18
  from .exceptions import RelativeJSONPointerSyntaxError
19
19
  from .filter import UNDEFINED
20
+ from .fluent_api import Query
20
21
  from .lex import Lexer
21
22
  from .match import JSONPathMatch
22
23
  from .parse import Parser
@@ -52,6 +53,8 @@ __all__ = (
52
53
  "Lexer",
53
54
  "match",
54
55
  "Parser",
56
+ "query",
57
+ "Query",
55
58
  "RelativeJSONPointer",
56
59
  "RelativeJSONPointerError",
57
60
  "RelativeJSONPointerIndexError",
@@ -69,3 +72,4 @@ findall_async = DEFAULT_ENV.findall_async
69
72
  finditer = DEFAULT_ENV.finditer
70
73
  finditer_async = DEFAULT_ENV.finditer_async
71
74
  match = DEFAULT_ENV.match
75
+ query = DEFAULT_ENV.query
@@ -27,6 +27,7 @@ from .filter import FilterExpression
27
27
  from .filter import FunctionExtension
28
28
  from .filter import InfixExpression
29
29
  from .filter import Path
30
+ from .fluent_api import Query
30
31
  from .function_extensions import ExpressionType
31
32
  from .function_extensions import FilterFunction
32
33
  from .function_extensions import validate
@@ -76,8 +77,6 @@ class JSONPathEnvironment:
76
77
  - Hook in to mapping and sequence item getting by overriding `getitem()`.
77
78
  - Change filter comparison operator behavior by overriding `compare()`.
78
79
 
79
- ## Class attributes
80
-
81
80
  Arguments:
82
81
  filter_caching (bool): If `True`, filter expressions will be cached
83
82
  where possible.
@@ -89,6 +88,8 @@ class JSONPathEnvironment:
89
88
 
90
89
  **New in version 0.10.0**
91
90
 
91
+ ## Class attributes
92
+
92
93
  Attributes:
93
94
  fake_root_token (str): The pattern used to select a "fake" root node, one level
94
95
  above the real root node.
@@ -229,9 +230,9 @@ class JSONPathEnvironment:
229
230
  *,
230
231
  filter_context: Optional[FilterContextVars] = None,
231
232
  ) -> List[object]:
232
- """Find all objects in `data` matching the given JSONPath `path`.
233
+ """Find all objects in _data_ matching the JSONPath _path_.
233
234
 
234
- If `data` is a string or a file-like objects, it will be loaded
235
+ If _data_ is a string or a file-like objects, it will be loaded
235
236
  using `json.loads()` and the default `JSONDecoder`.
236
237
 
237
238
  Arguments:
@@ -259,10 +260,10 @@ class JSONPathEnvironment:
259
260
  *,
260
261
  filter_context: Optional[FilterContextVars] = None,
261
262
  ) -> Iterable[JSONPathMatch]:
262
- """Generate `JSONPathMatch` objects for each match.
263
+ """Generate `JSONPathMatch` objects for each match of _path_ in _data_.
263
264
 
264
- If `data` is a string or a file-like objects, it will be loaded
265
- using `json.loads()` and the default `JSONDecoder`.
265
+ If _data_ is a string or a file-like objects, it will be loaded using
266
+ `json.loads()` and the default `JSONDecoder`.
266
267
 
267
268
  Arguments:
268
269
  path: The JSONPath as a string.
@@ -310,6 +311,50 @@ class JSONPathEnvironment:
310
311
  """
311
312
  return self.compile(path).match(data, filter_context=filter_context)
312
313
 
314
+ def query(
315
+ self,
316
+ path: str,
317
+ data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
318
+ filter_context: Optional[FilterContextVars] = None,
319
+ ) -> Query:
320
+ """Return a `Query` object over matches found by applying _path_ to _data_.
321
+
322
+ `Query` objects are iterable.
323
+
324
+ ```
325
+ for match in jsonpath.query("$.foo..bar", data):
326
+ ...
327
+ ```
328
+
329
+ You can skip and limit results with `Query.skip()` and `Query.limit()`.
330
+
331
+ ```
332
+ matches = (
333
+ jsonpath.query("$.foo..bar", data)
334
+ .skip(5)
335
+ .limit(10)
336
+ )
337
+
338
+ for match in matches
339
+ ...
340
+ ```
341
+
342
+ `Query.tail()` will get the last _n_ results.
343
+
344
+ ```
345
+ for match in jsonpath.query("$.foo..bar", data).tail(5):
346
+ ...
347
+ ```
348
+
349
+ Get values for each match using `Query.values()`.
350
+
351
+ ```
352
+ for obj in jsonpath.query("$.foo..bar", data).limit(5).values():
353
+ ...
354
+ ```
355
+ """
356
+ return Query(self.finditer(path, data, filter_context=filter_context))
357
+
313
358
  async def findall_async(
314
359
  self,
315
360
  path: str,
@@ -1,4 +1,5 @@
1
1
  """Filter expression nodes."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import copy
@@ -317,7 +318,7 @@ class PrefixExpression(FilterExpression):
317
318
  class InfixExpression(FilterExpression):
318
319
  """A pair of expressions and a comparison or logical operator."""
319
320
 
320
- __slots__ = ("left", "operator", "right")
321
+ __slots__ = ("left", "operator", "right", "logical")
321
322
 
322
323
  def __init__(
323
324
  self,
@@ -328,10 +329,11 @@ class InfixExpression(FilterExpression):
328
329
  self.left = left
329
330
  self.operator = operator
330
331
  self.right = right
332
+ self.logical = operator in ("&&", "||")
331
333
  super().__init__()
332
334
 
333
335
  def __str__(self) -> str:
334
- if self.operator in ("&&", "||"):
336
+ if self.logical:
335
337
  return f"({self.left} {self.operator} {self.right})"
336
338
  return f"{self.left} {self.operator} {self.right}"
337
339
 
@@ -345,22 +347,22 @@ class InfixExpression(FilterExpression):
345
347
 
346
348
  def evaluate(self, context: FilterContext) -> bool:
347
349
  left = self.left.evaluate(context)
348
- if isinstance(left, NodeList) and len(left) == 1:
350
+ if not self.logical and isinstance(left, NodeList) and len(left) == 1:
349
351
  left = left[0].obj
350
352
 
351
353
  right = self.right.evaluate(context)
352
- if isinstance(right, NodeList) and len(right) == 1:
354
+ if not self.logical and isinstance(right, NodeList) and len(right) == 1:
353
355
  right = right[0].obj
354
356
 
355
357
  return context.env.compare(left, self.operator, right)
356
358
 
357
359
  async def evaluate_async(self, context: FilterContext) -> bool:
358
360
  left = await self.left.evaluate_async(context)
359
- if isinstance(left, NodeList) and len(left) == 1:
361
+ if not self.logical and isinstance(left, NodeList) and len(left) == 1:
360
362
  left = left[0].obj
361
363
 
362
364
  right = await self.right.evaluate_async(context)
363
- if isinstance(right, NodeList) and len(right) == 1:
365
+ if not self.logical and isinstance(right, NodeList) and len(right) == 1:
364
366
  right = right[0].obj
365
367
 
366
368
  return context.env.compare(left, self.operator, right)
@@ -0,0 +1,161 @@
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)))
@@ -76,6 +76,11 @@ class JSONPathMatch:
76
76
  """Return a `JSONPointer` pointing to this match's path."""
77
77
  return JSONPointer.from_match(self)
78
78
 
79
+ @property
80
+ def value(self) -> object:
81
+ """Return the value associated with this match/node."""
82
+ return self.obj
83
+
79
84
 
80
85
  def _truncate(val: str, num: int, end: str = "...") -> str:
81
86
  # Replaces consecutive whitespace with a single newline.
@@ -1,4 +1,5 @@
1
1
  """The default JSONPath parser."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import json
@@ -142,14 +143,15 @@ class Parser:
142
143
  """A JSONPath parser bound to a JSONPathEnvironment."""
143
144
 
144
145
  PRECEDENCE_LOWEST = 1
145
- PRECEDENCE_LOGICALRIGHT = 3
146
- PRECEDENCE_LOGICAL = 4
146
+ PRECEDENCE_LOGICALRIGHT = 2
147
+ PRECEDENCE_LOGICAL_OR = 3
148
+ PRECEDENCE_LOGICAL_AND = 4
147
149
  PRECEDENCE_RELATIONAL = 5
148
150
  PRECEDENCE_MEMBERSHIP = 6
149
151
  PRECEDENCE_PREFIX = 7
150
152
 
151
153
  PRECEDENCES = {
152
- TOKEN_AND: PRECEDENCE_LOGICAL,
154
+ TOKEN_AND: PRECEDENCE_LOGICAL_AND,
153
155
  TOKEN_CONTAINS: PRECEDENCE_MEMBERSHIP,
154
156
  TOKEN_EQ: PRECEDENCE_RELATIONAL,
155
157
  TOKEN_GE: PRECEDENCE_RELATIONAL,
@@ -160,7 +162,7 @@ class Parser:
160
162
  TOKEN_LT: PRECEDENCE_RELATIONAL,
161
163
  TOKEN_NE: PRECEDENCE_RELATIONAL,
162
164
  TOKEN_NOT: PRECEDENCE_PREFIX,
163
- TOKEN_OR: PRECEDENCE_LOGICAL,
165
+ TOKEN_OR: PRECEDENCE_LOGICAL_OR,
164
166
  TOKEN_RE: PRECEDENCE_RELATIONAL,
165
167
  TOKEN_RPAREN: PRECEDENCE_LOWEST,
166
168
  }
@@ -563,9 +565,9 @@ class Parser:
563
565
 
564
566
  def parse_regex(self, stream: TokenStream) -> FilterExpression:
565
567
  pattern = stream.current.value
568
+ flags = 0
566
569
  if stream.peek.kind == TOKEN_RE_FLAGS:
567
570
  stream.next_token()
568
- flags = 0
569
571
  for flag in set(stream.current.value):
570
572
  flags |= self.RE_FLAG_MAP[flag]
571
573
  return RegexLiteral(value=re.compile(pattern, flags))
@@ -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
 
@@ -541,21 +565,21 @@ class ListSelector(JSONPathSelector):
541
565
  return hash((self.items, self.token))
542
566
 
543
567
  def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
544
- _matches = list(matches)
545
- for item in self.items:
546
- yield from item.resolve(_matches)
568
+ for match_ in matches:
569
+ for item in self.items:
570
+ yield from item.resolve([match_])
547
571
 
548
572
  async def resolve_async(
549
573
  self, matches: AsyncIterable[JSONPathMatch]
550
574
  ) -> AsyncIterable[JSONPathMatch]:
551
- _matches = [m async for m in matches]
552
- for item in self.items:
553
- async for match in item.resolve_async(_alist(_matches)):
554
- yield match
575
+ async for match_ in matches:
576
+ for item in self.items:
577
+ async for m in item.resolve_async(_alist([match_])):
578
+ yield m
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",
@@ -105,6 +105,38 @@ warn_unreachable = true
105
105
 
106
106
 
107
107
  [tool.ruff]
108
+ # Exclude a variety of commonly ignored directories.
109
+ exclude = [
110
+ ".bzr",
111
+ ".direnv",
112
+ ".eggs",
113
+ ".git",
114
+ ".hg",
115
+ ".mypy_cache",
116
+ ".nox",
117
+ ".pants.d",
118
+ ".pytype",
119
+ ".ruff_cache",
120
+ ".svn",
121
+ ".tox",
122
+ ".venv",
123
+ "__pypackages__",
124
+ "_build",
125
+ "buck-out",
126
+ "build",
127
+ "dist",
128
+ "node_modules",
129
+ "venv",
130
+ ]
131
+
132
+ # Same as Black.
133
+ line-length = 88
134
+
135
+
136
+ # Assume Python 3.10.
137
+ target-version = "py310"
138
+
139
+ [tool.ruff.lint]
108
140
  select = [
109
141
  "A",
110
142
  "ARG",
@@ -134,52 +166,23 @@ select = [
134
166
  "TID",
135
167
  "YTT",
136
168
  ]
169
+
137
170
  ignore = ["S105", "S101", "D107", "D105", "PLR0913", "SIM108"]
138
171
 
139
172
  fixable = ["I"]
140
173
  unfixable = []
141
174
 
142
- # Exclude a variety of commonly ignored directories.
143
- exclude = [
144
- ".bzr",
145
- ".direnv",
146
- ".eggs",
147
- ".git",
148
- ".hg",
149
- ".mypy_cache",
150
- ".nox",
151
- ".pants.d",
152
- ".pytype",
153
- ".ruff_cache",
154
- ".svn",
155
- ".tox",
156
- ".venv",
157
- "__pypackages__",
158
- "_build",
159
- "buck-out",
160
- "build",
161
- "dist",
162
- "node_modules",
163
- "venv",
164
- ]
165
-
166
- # Same as Black.
167
- line-length = 88
168
-
169
175
  # Allow unused variables when underscore-prefixed.
170
176
  dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
171
177
 
172
- # Assume Python 3.10.
173
- target-version = "py310"
174
-
175
- [tool.ruff.isort]
178
+ [tool.ruff.lint.isort]
176
179
  force-single-line = true
177
180
 
178
- [tool.ruff.pydocstyle]
181
+ [tool.ruff.lint.pydocstyle]
179
182
  convention = "google"
180
183
 
181
184
 
182
- [tool.ruff.per-file-ignores]
185
+ [tool.ruff.lint.per-file-ignores]
183
186
  "jsonpath/__about__.py" = ["D100"]
184
187
  "jsonpath/__init__.py" = ["D104"]
185
188
  "tests/*" = ["D100", "D101", "D104", "D103"]