Skip to content

Commit d90518a

Browse files
committed
Add some test cases
1 parent 5c821ec commit d90518a

File tree

3 files changed

+149
-22
lines changed

3 files changed

+149
-22
lines changed

jsonpath/fluent_api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,12 @@ def select(self, *expressions: str) -> Iterable[object]:
157157
for match in self._env.finditer(expr, m.obj): # type: ignore
158158
_pointer = match.pointer()
159159
_patch_parents(_pointer.parent(), patch, m.obj) # type: ignore
160-
patch.add(_pointer, match.obj)
160+
patch.addap(_pointer, match.obj)
161161

162162
patch.apply(obj)
163-
yield obj
163+
164+
if obj:
165+
yield obj
164166

165167
def first_one(self) -> Optional[JSONPathMatch]:
166168
"""Return the first `JSONPathMatch` or `None` if there were no matches."""

jsonpath/patch.py

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""JSON Patch, as per RFC 6902."""
2+
23
from __future__ import annotations
34

45
import copy
@@ -85,16 +86,16 @@ def asdict(self) -> Dict[str, object]:
8586
return {"op": self.name, "path": str(self.path), "value": self.value}
8687

8788

88-
class OpAddNe(Op):
89-
"""A non-standard _add if not exists_ operation."""
89+
class OpAddNe(OpAdd):
90+
"""A non-standard _add if not exists_ operation.
9091
91-
__slots__ = ("path", "value")
92+
This is like _OpAdd_, but only adds object/dict keys/values if they key does
93+
not already exist.
94+
"""
9295

93-
name = "add"
96+
__slots__ = ("path", "value")
9497

95-
def __init__(self, path: JSONPointer, value: object) -> None:
96-
self.path = path
97-
self.value = value
98+
name = "addne"
9899

99100
def apply(
100101
self, data: Union[MutableSequence[object], MutableMapping[str, object]]
@@ -115,20 +116,45 @@ def apply(
115116
raise JSONPatchError("index out of range")
116117
else:
117118
parent.insert(int(target), self.value)
118-
elif (
119-
isinstance(parent, MutableMapping)
120-
and parent.get(target, UNDEFINED) == UNDEFINED
121-
):
119+
elif isinstance(parent, MutableMapping) and target not in parent:
122120
parent[target] = self.value
123-
# else:
124-
# raise JSONPatchError(
125-
# f"unexpected operation on {parent.__class__.__name__!r}"
126-
# )
127121
return data
128122

129-
def asdict(self) -> Dict[str, object]:
130-
"""Return a dictionary representation of this operation."""
131-
return {"op": self.name, "path": str(self.path), "value": self.value}
123+
124+
class OpAddAp(OpAdd):
125+
"""A non-standard add operation that appends to arrays/lists .
126+
127+
This is like _OpAdd_, but assumes an index of "-" if the path can not
128+
be resolved.
129+
"""
130+
131+
__slots__ = ("path", "value")
132+
133+
name = "addap"
134+
135+
def apply(
136+
self, data: Union[MutableSequence[object], MutableMapping[str, object]]
137+
) -> Union[MutableSequence[object], MutableMapping[str, object]]:
138+
"""Apply this patch operation to _data_."""
139+
parent, obj = self.path.resolve_parent(data)
140+
if parent is None:
141+
# Replace the root object.
142+
# The following op, if any, will raise a JSONPatchError if needed.
143+
return self.value # type: ignore
144+
145+
target = self.path.parts[-1]
146+
if isinstance(parent, MutableSequence):
147+
if obj is UNDEFINED:
148+
parent.append(self.value)
149+
else:
150+
parent.insert(int(target), self.value)
151+
elif isinstance(parent, MutableMapping):
152+
parent[target] = self.value
153+
else:
154+
raise JSONPatchError(
155+
f"unexpected operation on {parent.__class__.__name__!r}"
156+
)
157+
return data
132158

133159

134160
class OpRemove(Op):
@@ -388,8 +414,13 @@ def _build(self, patch: Iterable[Mapping[str, object]]) -> None:
388414
)
389415
elif op == "addne":
390416
self.addne(
391-
path=self._op_pointer(operation, "path", "add", i),
392-
value=self._op_value(operation, "value", "add", i),
417+
path=self._op_pointer(operation, "path", "addne", i),
418+
value=self._op_value(operation, "value", "addne", i),
419+
)
420+
elif op == "addap":
421+
self.addne(
422+
path=self._op_pointer(operation, "path", "addap", i),
423+
value=self._op_value(operation, "value", "addap", i),
393424
)
394425
elif op == "remove":
395426
self.remove(path=self._op_pointer(operation, "path", "add", i))
@@ -491,6 +522,22 @@ def addne(self: Self, path: Union[str, JSONPointer], value: object) -> Self:
491522
self.ops.append(OpAddNe(path=pointer, value=value))
492523
return self
493524

525+
def addap(self: Self, path: Union[str, JSONPointer], value: object) -> Self:
526+
"""Append an _addap_ operation to this patch.
527+
528+
Arguments:
529+
path: A string representation of a JSON Pointer, or one that has
530+
already been parsed.
531+
value: The object to add.
532+
533+
Returns:
534+
This `JSONPatch` instance, so we can build a JSON Patch by chaining
535+
calls to JSON Patch operation methods.
536+
"""
537+
pointer = self._ensure_pointer(path)
538+
self.ops.append(OpAddAp(path=pointer, value=value))
539+
return self
540+
494541
def remove(self: Self, path: Union[str, JSONPointer]) -> Self:
495542
"""Append a _remove_ operation to this patch.
496543

tests/test_query_project.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Test cases for the fluent API projection."""
2+
3+
from typing import Any
4+
from typing import List
5+
6+
import jsonpath
7+
8+
9+
def test_top_level_array() -> None:
10+
expr = "$.*"
11+
data = [{"a": 1, "b": 1}, {"a": 2, "b": 2}, {"b": 3, "a": 3}]
12+
projection = ("a",)
13+
it = jsonpath.query(expr, data).select(*projection)
14+
assert list(it) == [{"a": 1}, {"a": 2}, {"a": 3}]
15+
16+
17+
def test_top_level_array_partial_existence() -> None:
18+
expr = "$.*"
19+
data = [{"a": 1, "b": 1}, {"b": 2}, {"b": 3, "a": 3}]
20+
projection = ("a",)
21+
it = jsonpath.query(expr, data).select(*projection)
22+
assert list(it) == [{"a": 1}, {"a": 3}]
23+
24+
25+
def test_top_level_array_projection_does_not_existence() -> None:
26+
expr = "$.*"
27+
data = [{"a": 1, "b": 1}, {"b": 2}, {"b": 3, "a": 3}]
28+
projection = ("x",)
29+
it = jsonpath.query(expr, data).select(*projection)
30+
assert list(it) == []
31+
32+
33+
def test_empty_top_level_array() -> None:
34+
expr = "$.*"
35+
data: List[Any] = []
36+
projection = ("a",)
37+
it = jsonpath.query(expr, data).select(*projection)
38+
assert list(it) == []
39+
40+
41+
def test_top_level_array_select_many() -> None:
42+
expr = "$.*"
43+
data = [{"a": 1, "b": 1, "c": 1}, {"a": 2, "b": 2, "c": 2}, {"b": 3, "a": 3}]
44+
projection = ("a", "c")
45+
it = jsonpath.query(expr, data).select(*projection)
46+
assert list(it) == [{"a": 1, "c": 1}, {"a": 2, "c": 2}, {"a": 3}]
47+
48+
49+
def test_singular_query() -> None:
50+
expr = "$.a"
51+
data = {"a": {"foo": 42, "bar": 7}, "b": 1}
52+
projection = ("foo",)
53+
it = jsonpath.query(expr, data).select(*projection)
54+
assert list(it) == [{"foo": 42}]
55+
56+
57+
def test_select_array_element() -> None:
58+
expr = "$.a"
59+
data = {"a": {"foo": [42, 7], "bar": 7}, "b": 1}
60+
projection = ("foo[0]",)
61+
it = jsonpath.query(expr, data).select(*projection)
62+
assert list(it) == [{"foo": [42]}]
63+
64+
65+
def test_select_array_slice() -> None:
66+
expr = "$.a"
67+
data = {"a": {"foo": [1, 2, 42, 7, 3], "bar": 7}, "b": 1}
68+
projection = ("foo[2:4]",)
69+
it = jsonpath.query(expr, data).select(*projection)
70+
assert list(it) == [{"foo": [42, 7]}]
71+
72+
73+
def test_select_nested_objects() -> None:
74+
expr = "$.a"
75+
data = {"a": {"foo": {"bar": 42}, "bar": 7}, "b": 1}
76+
projection = ("foo.bar",)
77+
it = jsonpath.query(expr, data).select(*projection)
78+
assert list(it) == [{"foo": {"bar": 42}}]

0 commit comments

Comments
 (0)