Skip to content

Commit 3e2bcda

Browse files
committed
feat: add output mode: PATH
1 parent 9b0b033 commit 3e2bcda

File tree

4 files changed

+90
-42
lines changed

4 files changed

+90
-42
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
- [jsonpath-python](#jsonpath-python)
55
- [Features](#features)
6-
- [Issues](#issues)
76
- [Examples](#examples)
87
- [Filter](#filter)
98
- [Sort](#sort)

jsonpath/__init__.py

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
Author : zhangxianbing1
33
Date : 2020-12-27 09:22:14
44
LastEditors : zhangxianbing1
5-
LastEditTime : 2021-01-04 11:01:24
5+
LastEditTime : 2021-01-04 12:40:59
66
Description : JSONPath
77
"""
88
__version__ = "0.0.3"
99
__author__ = "zhangxianbing"
1010

1111
import json
1212
import re
13-
from typing import Union
1413
from collections import defaultdict
14+
from typing import Union
1515

1616
RESULT_TYPE = {
1717
"VALUE": "A list of specific values.",
@@ -39,10 +39,6 @@
3939
# pylint: disable=invalid-name,missing-function-docstring,missing-class-docstring,eval-used
4040

4141

42-
def concat(x, y, con=SEP):
43-
return f"{x}{con}{y}"
44-
45-
4642
def _getattr(obj: dict, path: str):
4743
r = obj
4844
for k in path.split("."):
@@ -61,18 +57,13 @@ class ExprSyntaxError(Exception):
6157

6258
class JSONPath:
6359
# annotations
64-
result: Union[list, dict]
65-
result_type: str
66-
subx = defaultdict(list)
6760
steps: list
6861
lpath: int
62+
subx = defaultdict(list)
63+
result: Union[list, dict]
64+
result_type: str
6965

70-
def __init__(self, expr: str, *, result_type="VALUE"):
71-
if result_type not in RESULT_TYPE:
72-
raise ValueError(f"result_type must be one of {tuple(RESULT_TYPE.keys())}")
73-
self.result_type = result_type
74-
75-
# parse expression
66+
def __init__(self, expr: str):
7667
expr = self._parse_expr(expr)
7768
self.steps = expr.split(SEP)
7869
self.lpath = len(self.steps)
@@ -118,38 +109,40 @@ def _f_brackets(m):
118109
ret += '["%s"]' % e
119110
return ret
120111

121-
def parse(self, obj):
112+
def parse(self, obj, result_type="VALUE"):
122113
if not isinstance(obj, (list, dict)):
123114
raise TypeError("obj must be a list or a dict.")
124-
115+
if result_type not in RESULT_TYPE:
116+
raise ValueError(f"result_type must be one of {tuple(RESULT_TYPE.keys())}")
117+
self.result_type = result_type
125118
if self.result_type == "FIELD":
126119
self.result = {}
127120
else:
128121
self.result = []
129122

130-
self._trace(obj, 0)
123+
self._trace(obj, 0, "$")
131124

132125
return self.result
133126

134127
@staticmethod
135-
def _traverse(f, obj, i: int, *args):
128+
def _traverse(f, obj, i: int, path: str, *args):
136129
if isinstance(obj, list):
137-
for v in obj:
138-
f(v, i, *args)
130+
for idx, v in enumerate(obj):
131+
f(v, i, f"{path}{SEP}{idx}", *args)
139132
elif isinstance(obj, dict):
140-
for v in obj.values():
141-
f(v, i, *args)
133+
for k, v in obj.items():
134+
f(v, i, f"{path}{SEP}{k}", *args)
142135

143-
def _filter(self, obj, i: int, step: str):
136+
def _filter(self, obj, i: int, path: str, step: str):
144137
r = False
145138
try:
146139
r = eval(step, None, {"__obj": obj})
147140
except Exception as err:
148141
print(err)
149142
if r:
150-
self._trace(obj, i)
143+
self._trace(obj, i, path)
151144

152-
def _trace(self, obj, i: int):
145+
def _trace(self, obj, i: int, path):
153146
"""Perform operation on object.
154147
155148
Args:
@@ -159,54 +152,59 @@ def _trace(self, obj, i: int):
159152

160153
# store
161154
if i >= self.lpath:
162-
self.result.append(obj)
155+
if self.result_type == "VALUE":
156+
self.result.append(obj)
157+
elif self.result_type == "PATH":
158+
self.result.append(path)
159+
elif self.result_type == "FIELD":
160+
pass
163161
print(obj)
164162
return
165163

166164
step = self.steps[i]
167165

168166
# wildcard
169167
if step == "*":
170-
self._traverse(self._trace, obj, i + 1)
168+
self._traverse(self._trace, obj, i + 1, path)
171169
return
172170

173171
# recursive descent
174172
if step == "..":
175-
self._trace(obj, i + 1)
176-
self._traverse(self._trace, obj, i)
173+
self._trace(obj, i + 1, path)
174+
self._traverse(self._trace, obj, i, path)
177175
return
178176

179177
# get value from list
180178
if isinstance(obj, list) and step.isdigit():
181179
ikey = int(step)
182180
if ikey < len(obj):
183-
self._trace(obj[ikey], i + 1)
181+
self._trace(obj[ikey], i + 1, f"{path}{SEP}{step}")
184182
return
185183

186184
# get value from dict
187185
if isinstance(obj, dict) and step in obj:
188-
self._trace(obj[step], i + 1)
186+
self._trace(obj[step], i + 1, f"{path}{SEP}{step}")
189187
return
190188

191189
# slice
192190
if isinstance(obj, list) and REP_SLICE_CONTENT.fullmatch(step):
193191
vals = eval(f"obj[{step}]")
194-
for v in vals:
195-
self._trace(v, i + 1)
192+
for idx, v in enumerate(vals):
193+
self._trace(v, i + 1, f"{path}{SEP}{idx}")
196194
return
197195

198196
# select
199197
if isinstance(obj, dict) and REP_SELECT_CONTENT.fullmatch(step):
200198
for k in step.split(","):
201199
if k in obj:
202-
self._trace(obj[k], i + 1)
200+
self._trace(obj[k], i + 1, f"{path}{SEP}{k}")
203201
return
204202

205203
# filter
206204
if step.startswith("?(") and step.endswith(")"):
207205
step = step[2:-1]
208206
step = REP_FILTER_CONTENT.sub(self._f_brackets, step)
209-
self._traverse(self._filter, obj, i + 1, step)
207+
self._traverse(self._filter, obj, i + 1, path, step)
210208
return
211209

212210
# sort
@@ -229,11 +227,12 @@ def _trace(self, obj, i: int):
229227
else:
230228
obj.sort(key=lambda t, k=sortby: _getattr(t[1], k))
231229
obj = {k: v for k, v in obj}
232-
self._traverse(self._trace, obj, i + 1)
230+
self._traverse(self._trace, obj, i + 1, path)
233231
return
234232

235233

236234
if __name__ == "__main__":
237235
with open("test/data/2.json", "rb") as f:
238236
d = json.load(f)
239-
JSONPath("$.scores[/(score)].score").parse(d)
237+
D = JSONPath("$.scores[/(score)].score").parse(d, "PATH")
238+
print(D)

test/conftest.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,50 @@
5050
TestCase("$.scores[/(score)].score", data, [60, 85, 90, 95, 100]),
5151
]
5252
)
53-
def cases(request):
53+
def value_cases(request):
54+
return request.param
55+
56+
57+
@pytest.fixture(
58+
params=[
59+
TestCase("$.*", data, list(data.values())),
60+
TestCase("$.book", data, [data["book"]]),
61+
TestCase("$[book]", data, [data["book"]]),
62+
TestCase("$.'a.b c'", data, [data["a.b c"]]),
63+
TestCase("$['a.b c']", data, [data["a.b c"]]),
64+
# recursive descent
65+
TestCase("$..price", data, [8.95, 12.99, 8.99, 22.99, 19.95]),
66+
# slice
67+
TestCase("$.book[1:3]", data, data["book"][1:3]),
68+
TestCase("$.book[1:-1]", data, data["book"][1:-1]),
69+
TestCase("$.book[0:-1:2]", data, data["book"][0:-1:2]),
70+
TestCase("$.book[-1:1]", data, data["book"][-1:1]),
71+
TestCase("$.book[-1:-11:3]", data, data["book"][-1:-11:3]),
72+
TestCase("$.book[:]", data, data["book"][:]),
73+
# filter
74+
TestCase("$.book[?(@.price>8 and @.price<9)].price", data, [8.95, 8.99]),
75+
TestCase('$.book[?(@.category=="reference")].category', data, ["reference"]),
76+
TestCase(
77+
'$.book[?(@.category!="reference" and @.price<9)].title',
78+
data,
79+
["Moby Dick"],
80+
),
81+
TestCase(
82+
'$.book[?(@.author=="Herman Melville" or @.author=="Evelyn Waugh")].author',
83+
data,
84+
["Evelyn Waugh", "Herman Melville"],
85+
),
86+
# sort
87+
TestCase("$.book[/(price)].price", data, [8.95, 8.99, 12.99, 22.99]),
88+
TestCase("$.book[/(~price)].price", data, [22.99, 12.99, 8.99, 8.95]),
89+
TestCase("$.book[/(category,price)].price", data, [8.99, 12.99, 22.99, 8.95]),
90+
TestCase(
91+
"$.book[/(brand.version)].brand.version",
92+
data,
93+
["v0.0.1", "v1.0.0", "v1.0.2", "v1.0.3"],
94+
),
95+
TestCase("$.scores[/(score)].score", data, [60, 85, 90, 95, 100]),
96+
]
97+
)
98+
def path_cases(request):
5499
return request.param

test/test_jsonpath.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from jsonpath import JSONPath
22

33

4-
def test_cases(cases):
5-
assert cases.result == JSONPath(cases.expr).parse(cases.data)
4+
def test_value_cases(value_cases):
5+
assert value_cases.result == JSONPath(value_cases.expr).parse(value_cases.data)
6+
7+
8+
def test_path_cases(path_cases):
9+
print(JSONPath(path_cases.expr).parse(path_cases.data, "PATH"))
10+
# assert path_cases.result == JSONPath(path_cases.expr).parse(path_cases.data, "PATH")

0 commit comments

Comments
 (0)