Skip to content

Commit aa1cd23

Browse files
committed
Implement find_one and JSONPathNodeList.items
1 parent ade1134 commit aa1cd23

File tree

6 files changed

+120
-8
lines changed

6 files changed

+120
-8
lines changed

README.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Each `JSONPathNode` has:
4141
The returned list is a subclass of `list` with some helper methods.
4242

4343
- `values()` returns a list of values, one for each node.
44-
- `values_or_singular()` returns a scalar value if the list has exactly one node, or a list of values otherwise.
44+
- `items()` returns a list of `(normalized path, value)` tuples, one tuple for each node in the list.
4545

4646
**Example:**
4747

@@ -65,10 +65,56 @@ for node in jsonpath.find("$.users[?@.score > 85]", value):
6565
# {'name': 'John', 'score': 86, 'admin': True} at '$['users'][1]'
6666
```
6767

68-
### finditer(query, value)
68+
### `finditer(query, value)`
6969

7070
`finditer()` accepts the same arguments as [`find()`](#findquery-value), but returns an iterator over `JSONPathNode` instances rather than a list. This could be useful if you're expecting a large number of results that you don't want to load into memory all at once.
7171

72+
### `find_one(query, value)`
73+
74+
`find_one()` accepts the same arguments as [`find()`](#findquery-value), but returns the first available `JSONPathNode`, or `None` if there were no matches.
75+
76+
`find_one()` is equivalent to:
77+
78+
```python
79+
def find_one(query, value):
80+
try:
81+
return next(iter(jsonpath.finditer(query, value)))
82+
except StopIteration:
83+
return None
84+
```
85+
86+
### `compile(query)`
87+
88+
`find(query, value)` is a convenience function for `JSONPathEnvironment().compile(query).apply(value)`. Use `compile(query)` to obtain a `JSONPathQuery` instance which can be applied to difference JSON-like values repeatedly.
89+
90+
```python
91+
import jsonpath_rfc9535 as jsonpath
92+
93+
value = {
94+
"users": [
95+
{"name": "Sue", "score": 100},
96+
{"name": "John", "score": 86, "admin": True},
97+
{"name": "Sally", "score": 84, "admin": False},
98+
{"name": "Jane", "score": 55},
99+
],
100+
"moderator": "John",
101+
}
102+
103+
query = jsonpath.compile("$.users[?@.score > 85]")
104+
105+
for node in query.apply(value):
106+
print(f"{node.value} at '{node.path()}'")
107+
108+
# {'name': 'Sue', 'score': 100} at '$['users'][0]'
109+
# {'name': 'John', 'score': 86, 'admin': True} at '$['users'][1]'
110+
```
111+
112+
A `JSONPathQuery` has a `finditer(value)` method too, and `find(value)` is an alias for `apply(value)`.
113+
72114
## License
73115

74116
`python-jsonpath-rfc9535` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
117+
118+
```
119+
120+
```

jsonpath_rfc9535/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"Parser",
3030
"JSONPathQuery",
3131
"find",
32+
"find_one",
3233
"finditer",
3334
"compile",
3435
)
@@ -38,3 +39,4 @@
3839
compile = DEFAULT_ENV.compile # noqa: A001
3940
finditer = DEFAULT_ENV.finditer
4041
find = DEFAULT_ENV.find
42+
find_one = DEFAULT_ENV.find_one

jsonpath_rfc9535/environment.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,28 @@ def find(
144144
"""
145145
return self.compile(query).find(value)
146146

147+
def find_one(
148+
self,
149+
query: str,
150+
value: JSONValue,
151+
) -> Optional[JSONPathNode]:
152+
"""Return the first available node from applying _query_ to _value_.
153+
154+
Arguments:
155+
query: A JSONPath expression.
156+
value: JSON-like data to query, as you'd get from `json.load`.
157+
158+
Returns:
159+
The first available `JSONPathNode` instance, or `None` if there
160+
are no matches.
161+
162+
Raises:
163+
JSONPathSyntaxError: If the query is invalid.
164+
JSONPathTypeError: If a filter expression attempts to use types in
165+
an incompatible way.
166+
"""
167+
return self.compile(query).find_one(value)
168+
147169
def setup_function_extensions(self) -> None:
148170
"""Initialize function extensions."""
149171
self.function_extensions["length"] = function_extensions.Length()

jsonpath_rfc9535/node.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def path(self) -> str:
4343
)
4444

4545
def __str__(self) -> str:
46-
return f"JSONPathNode({self.path()})"
46+
return f"JSONPathNode({self.path()!r})"
4747

4848

4949
class JSONPathNodeList(List[JSONPathNode]):
@@ -56,11 +56,9 @@ def values(self) -> List[object]:
5656
"""Return the values from this node list."""
5757
return [node.value for node in self]
5858

59-
def values_or_singular(self) -> object:
60-
"""Return values from this node list, or a single value if there's one node."""
61-
if len(self) == 1:
62-
return self[0].value
63-
return [node.value for node in self]
59+
def items(self) -> List[Tuple[str, object]]:
60+
"""Return a list of (path, value) pairs, one for each node in the list."""
61+
return [(node.path(), node.value) for node in self]
6462

6563
def empty(self) -> bool:
6664
"""Return `True` if this node list is empty."""

jsonpath_rfc9535/query.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from typing import TYPE_CHECKING
66
from typing import Iterable
7+
from typing import Optional
78
from typing import Tuple
89

910
from .node import JSONPathNode
@@ -97,6 +98,28 @@ def find(
9798
"""
9899
return JSONPathNodeList(self.finditer(value))
99100

101+
apply = find
102+
103+
def find_one(self, value: JSONValue) -> Optional[JSONPathNode]:
104+
"""Return the first node from applying this query to _value_.
105+
106+
Arguments:
107+
value: JSON-like data to query, as you'd get from `json.load`.
108+
109+
Returns:
110+
The first available `JSONPathNode` instance, or `None` if there
111+
are no matches.
112+
113+
Raises:
114+
JSONPathSyntaxError: If the query is invalid.
115+
JSONPathTypeError: If a filter expression attempts to use types in
116+
an incompatible way.
117+
"""
118+
try:
119+
return next(iter(self.finditer(value)))
120+
except StopIteration:
121+
return None
122+
100123
def singular_query(self) -> bool:
101124
"""Return `True` if this JSONPath expression is a singular query."""
102125
for segment in self.segments:

tests/test_env.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pytest
2+
3+
from jsonpath_rfc9535 import JSONPathEnvironment
4+
5+
6+
@pytest.fixture()
7+
def env() -> JSONPathEnvironment:
8+
return JSONPathEnvironment()
9+
10+
11+
def test_find_one(env: JSONPathEnvironment) -> None:
12+
"""Test that we can get the first node from a node iterator."""
13+
match = env.find_one("$.some", {"some": 1, "thing": 2})
14+
assert match is not None
15+
assert match.value == 1
16+
17+
18+
def test_find_on_no_match(env: JSONPathEnvironment) -> None:
19+
"""Test that we get `None` if there are no matches."""
20+
match = env.find_one("$.other", {"some": 1, "thing": 2})
21+
assert match is None

0 commit comments

Comments
 (0)