Skip to content

Commit d396bc5

Browse files
committed
feat: add update function to JSONPath class (#12)
1 parent a5e2973 commit d396bc5

File tree

2 files changed

+171
-2
lines changed

2 files changed

+171
-2
lines changed

jsonpath/jsonpath.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import re
44
import sys
55
from collections import defaultdict
6-
from typing import Union
6+
from typing import Any, Callable, Union
77

88

99
def create_logger(name: str = None, level: Union[int, str] = logging.INFO):
@@ -59,6 +59,7 @@ class JSONPath:
5959
REP_SLICE_CONTENT = re.compile(r"^(-?\d*)?:(-?\d*)?(:-?\d*)?$")
6060
REP_SELECT_CONTENT = re.compile(r"^([\w.']+)(, ?[\w.']+)+$")
6161
REP_FILTER_CONTENT = re.compile(r"@([.\[].*?)(?=<=|>=|==|!=|>|<| in| not| is|\s|\)|$)|len\(@([.\[].*?)\)")
62+
REP_PATH_SEGMENT = re.compile(r"(?:\.|^)(?P<dot>\w+)|\[['\"](?P<quote>.*?)['\"]\]|\[(?P<int>\d+)\]")
6263

6364
# annotations
6465
f: list
@@ -110,7 +111,9 @@ def _parse_expr(self, expr):
110111
expr = JSONPath.REP_PUT_PAREN.sub(self._put_paren, expr)
111112
expr = JSONPath.REP_PUT_BACKQUOTE.sub(self._put_backquote, expr)
112113
expr = JSONPath.REP_PUT_QUOTE.sub(self._put_quote, expr)
113-
if expr.startswith("$;"):
114+
if expr == "$":
115+
expr = ""
116+
elif expr.startswith("$;"):
114117
expr = expr[2:]
115118

116119
logger.debug(f"after expr : {expr}")
@@ -335,6 +338,46 @@ def _trace(self, obj, i: int, path):
335338

336339
return
337340

341+
def update(self, obj: Union[list, dict], value_or_func: Union[Any, Callable[[Any], Any]]) -> Any:
342+
paths = self.parse(obj, result_type="PATH")
343+
for path in paths:
344+
matches = list(JSONPath.REP_PATH_SEGMENT.finditer(path))
345+
if not matches:
346+
# Root object
347+
if isinstance(value_or_func, Callable):
348+
obj = value_or_func(obj)
349+
else:
350+
obj = value_or_func
351+
continue
352+
353+
target = obj
354+
# Traverse to parent
355+
for match in matches[:-1]:
356+
group = match.groupdict()
357+
if group["dot"]:
358+
target = target[group["dot"]]
359+
elif group["quote"]:
360+
target = target[group["quote"]]
361+
elif group["int"]:
362+
target = target[int(group["int"])]
363+
364+
# Update last segment
365+
last_match = matches[-1]
366+
group = last_match.groupdict()
367+
if group["dot"]:
368+
key = group["dot"]
369+
elif group["quote"]:
370+
key = group["quote"]
371+
elif group["int"]:
372+
key = int(group["int"])
373+
374+
if isinstance(value_or_func, Callable):
375+
target[key] = value_or_func(target[key])
376+
else:
377+
target[key] = value_or_func
378+
379+
return obj
380+
338381

339382
class RegexPattern:
340383
def __init__(self, pattern):

tests/test_update.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from jsonpath.jsonpath import JSONPath
2+
3+
4+
def test_update_value():
5+
data = {
6+
"store": {
7+
"book": [
8+
{"category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95}
9+
]
10+
}
11+
}
12+
jp = JSONPath("$.store.book[0].price")
13+
result = jp.update(data, 10.0)
14+
assert result["store"]["book"][0]["price"] == 10.0
15+
16+
17+
def test_update_function():
18+
data = {"count": 1}
19+
jp = JSONPath("$.count")
20+
result = jp.update(data, lambda x: x + 1)
21+
assert result["count"] == 2
22+
23+
24+
def test_update_root():
25+
data = {"a": 1}
26+
jp = JSONPath("$")
27+
result = jp.update(data, {"b": 2})
28+
assert result == {"b": 2}
29+
30+
31+
def test_update_list_index():
32+
data = [1, 2, 3]
33+
jp = JSONPath("$[1]")
34+
result = jp.update(data, 5)
35+
assert result == [1, 5, 3]
36+
37+
38+
def test_update_multiple():
39+
data = {"items": [{"value": 1}, {"value": 2}, {"value": 3}]}
40+
jp = JSONPath("$.items[*].value")
41+
result = jp.update(data, 0)
42+
for item in result["items"]:
43+
assert item["value"] == 0
44+
45+
46+
def test_update_multiple_func():
47+
data = {"items": [{"value": 1}, {"value": 2}, {"value": 3}]}
48+
jp = JSONPath("$.items[*].value")
49+
result = jp.update(data, lambda x: x * 2)
50+
assert result["items"][0]["value"] == 2
51+
assert result["items"][1]["value"] == 4
52+
assert result["items"][2]["value"] == 6
53+
54+
55+
def test_update_with_filter():
56+
data = {"books": [{"price": 10, "title": "A"}, {"price": 20, "title": "B"}, {"price": 30, "title": "C"}]}
57+
# Update price where price > 15
58+
jp = JSONPath("$.books[?(@.price > 15)].price")
59+
result = jp.update(data, 0)
60+
assert result["books"][0]["price"] == 10 # Unchanged
61+
assert result["books"][1]["price"] == 0 # Updated
62+
assert result["books"][2]["price"] == 0 # Updated
63+
64+
65+
def test_update_slice():
66+
data = [0, 1, 2, 3, 4]
67+
jp = JSONPath("$[1:4]") # Indices 1, 2, 3
68+
result = jp.update(data, 9)
69+
assert result == [0, 9, 9, 9, 4]
70+
71+
72+
def test_update_special_keys():
73+
data = {"complex.key": 1, "key with space": 2, "normal": 3}
74+
# Note: jsonpath-python might handle keys with dots using ['...'] syntax in path output
75+
jp = JSONPath("$['complex.key']")
76+
result = jp.update(data, 10)
77+
assert result["complex.key"] == 10
78+
79+
jp = JSONPath("$['key with space']")
80+
result = jp.update(data, 20)
81+
assert result["key with space"] == 20
82+
83+
84+
def test_update_no_match():
85+
data = {"a": 1}
86+
jp = JSONPath("$.b")
87+
result = jp.update(data, 2)
88+
assert result == {"a": 1}
89+
90+
91+
def test_update_nested_structure():
92+
data = {"a": [{"b": [1, 2]}, {"b": [3, 4]}]}
93+
jp = JSONPath("$.a[*].b[1]")
94+
result = jp.update(data, 99)
95+
assert result["a"][0]["b"][1] == 99
96+
assert result["a"][1]["b"][1] == 99
97+
98+
99+
def test_update_recursive():
100+
data = {
101+
"store": {
102+
"book": [{"category": "reference", "price": 8.95}, {"category": "fiction", "price": 12.99}],
103+
"bicycle": {"color": "red", "price": 19.95},
104+
}
105+
}
106+
jp = JSONPath("$..price")
107+
result = jp.update(data, 10.0)
108+
assert result["store"]["book"][0]["price"] == 10.0
109+
assert result["store"]["book"][1]["price"] == 10.0
110+
assert result["store"]["bicycle"]["price"] == 10.0
111+
112+
113+
def test_update_union():
114+
data = {"a": 1, "b": 2, "c": 3}
115+
jp = JSONPath("$[a,b]")
116+
result = jp.update(data, 0)
117+
assert result["a"] == 0
118+
assert result["b"] == 0
119+
assert result["c"] == 3
120+
121+
122+
def test_update_quote_keys():
123+
data = {'c"d': 1, "normal": 2}
124+
jp = JSONPath("$['c\"d']")
125+
result = jp.update(data, 99)
126+
assert result['c"d'] == 99

0 commit comments

Comments
 (0)