Skip to content

Commit ab4b9e4

Browse files
authored
Merge pull request #5 from zhangxianbing/dev
feat: support child operator in field-selector
2 parents 122fa54 + f636dab commit ab4b9e4

File tree

3 files changed

+64
-30
lines changed

3 files changed

+64
-30
lines changed

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,23 @@ A more powerful JSONPath implementation in modern python.
1818

1919
## Features
2020

21-
- [x] Light. (No need to install third-party dependencies.)
21+
- [x] **Light. (No need to install third-party dependencies.)**
22+
- [x] **Support filter operator, including multi-selection, inverse-selection filtering.**
23+
- [x] **Support sorter operator, including sorting by multiple fields, ascending and descending order.**
2224
- [x] Support basic semantics of JSONPath.
2325
- [x] Support output modes: VALUE, PATH.
24-
- [x] Support filter operator, including multi-selection, inverse-selection filtering.
25-
- [x] Support sorter operator, including sorting by multiple fields, ascending and descending order.
26-
- [ ] Support parent operator.
26+
- [ ] Support embedded syntax.
2727
- [ ] Support user-defined function.
28+
- [ ] Support parent operator.
29+
30+
## Installation
31+
32+
```bash
33+
pip install jsonpath-python
34+
35+
# import
36+
>>> from jsonpath import JSONPath
37+
```
2838

2939
## JSONPath Syntax
3040

jsonpath/__init__.py

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Author : zhangxianbing
33
Date : 2020-12-27 09:22:14
44
LastEditors : zhangxianbing
5-
LastEditTime : 2021-02-07 10:10:55
5+
LastEditTime : 2021-03-02 15:18:06
66
Description : JSONPath
77
"""
88
__version__ = "1.0.3"
@@ -36,7 +36,7 @@ def create_logger(name: str = None, level: Union[int, str] = logging.INFO):
3636
return logger
3737

3838

39-
LOG = create_logger("jsonpath", os.getenv("PYLOGLEVEL", "INFO"))
39+
logger = create_logger("jsonpath", os.getenv("PYLOGLEVEL", "INFO"))
4040

4141

4242
class ExprSyntaxError(Exception):
@@ -49,15 +49,19 @@ class JSONPath:
4949
"PATH": "All path of specific values.",
5050
}
5151

52+
# common patterns
5253
SEP = ";"
53-
# regex patterns
54-
REP_PICKUP_QUOTE = re.compile(r"['](.*?)[']")
55-
REP_PICKUP_BRACKET = re.compile(r"[\[](.*?)[\]]")
56-
REP_PUTBACK_QUOTE = re.compile(r"#Q(\d+)")
57-
REP_PUTBACK_BRACKET = re.compile(r"#B(\d+)")
5854
REP_DOUBLEDOT = re.compile(r"\.\.")
5955
REP_DOT = re.compile(r"(?<!\.)\.(?!\.)")
6056

57+
# save special patterns
58+
REP_GET_QUOTE = re.compile(r"['](.*?)[']")
59+
REP_PUT_QUOTE = re.compile(r"#Q(\d+)")
60+
REP_GET_BRACKET = re.compile(r"[\[](.*?)[\]]")
61+
REP_PUT_BRACKET = re.compile(r"#B(\d+)")
62+
REP_GET_PAREN = re.compile(r"[\(](.*?)[\)]")
63+
REP_PUT_PAREN = re.compile(r"#P(\d+)")
64+
6165
# operators
6266
REP_SLICE_CONTENT = re.compile(r"^(-?\d*)?:(-?\d*)?(:-?\d*)?$")
6367
REP_SELECT_CONTENT = re.compile(r"^([\w.']+)(, ?[\w.']+)+$")
@@ -76,7 +80,7 @@ def __init__(self, expr: str):
7680
expr = self._parse_expr(expr)
7781
self.segments = expr.split(JSONPath.SEP)
7882
self.lpath = len(self.segments)
79-
LOG.debug(f"segments : {self.segments}")
83+
logger.debug(f"segments : {self.segments}")
8084

8185
def parse(self, obj, result_type="VALUE"):
8286
if not isinstance(obj, (list, dict)):
@@ -94,36 +98,46 @@ def parse(self, obj, result_type="VALUE"):
9498
return self.result
9599

96100
def _parse_expr(self, expr):
97-
LOG.debug(f"before expr : {expr}")
101+
logger.debug(f"before expr : {expr}")
98102

99-
expr = JSONPath.REP_PICKUP_QUOTE.sub(self._f_pickup_quote, expr)
100-
expr = JSONPath.REP_PICKUP_BRACKET.sub(self._f_pickup_bracket, expr)
103+
expr = JSONPath.REP_GET_QUOTE.sub(self._get_quote, expr)
104+
expr = JSONPath.REP_GET_BRACKET.sub(self._get_bracket, expr)
105+
expr = JSONPath.REP_GET_PAREN.sub(self._get_paren, expr)
101106
expr = JSONPath.REP_DOUBLEDOT.sub(f"{JSONPath.SEP}..{JSONPath.SEP}", expr)
102107
expr = JSONPath.REP_DOT.sub(JSONPath.SEP, expr)
103-
expr = JSONPath.REP_PUTBACK_BRACKET.sub(self._f_putback_bracket, expr)
104-
expr = JSONPath.REP_PUTBACK_QUOTE.sub(self._f_putback_quote, expr)
108+
expr = JSONPath.REP_PUT_PAREN.sub(self._put_paren, expr)
109+
expr = JSONPath.REP_PUT_BRACKET.sub(self._put_bracket, expr)
110+
expr = JSONPath.REP_PUT_QUOTE.sub(self._put_quote, expr)
105111
if expr.startswith("$;"):
106112
expr = expr[2:]
107113

108-
LOG.debug(f"after expr : {expr}")
114+
logger.debug(f"after expr : {expr}")
109115
return expr
110116

111-
def _f_pickup_quote(self, m):
117+
def _get_quote(self, m):
112118
n = len(self.subx["#Q"])
113119
self.subx["#Q"].append(m.group(1))
114120
return f"#Q{n}"
115121

116-
def _f_pickup_bracket(self, m):
122+
def _put_quote(self, m):
123+
return self.subx["#Q"][int(m.group(1))]
124+
125+
def _get_bracket(self, m):
117126
n = len(self.subx["#B"])
118127
self.subx["#B"].append(m.group(1))
119128
return f".#B{n}"
120129

121-
def _f_putback_quote(self, m):
122-
return self.subx["#Q"][int(m.group(1))]
123-
124-
def _f_putback_bracket(self, m):
130+
def _put_bracket(self, m):
125131
return self.subx["#B"][int(m.group(1))]
126132

133+
def _get_paren(self, m):
134+
n = len(self.subx["#P"])
135+
self.subx["#P"].append(m.group(1))
136+
return f"(#P{n})"
137+
138+
def _put_paren(self, m):
139+
return self.subx["#P"][int(m.group(1))]
140+
127141
@staticmethod
128142
def _f_brackets(m):
129143
ret = "__obj"
@@ -147,7 +161,7 @@ def _getattr(obj: dict, path: str):
147161
try:
148162
r = r.get(k)
149163
except (AttributeError, KeyError) as err:
150-
LOG.error(err)
164+
logger.error(err)
151165
return None
152166

153167
return r
@@ -167,7 +181,7 @@ def _filter(self, obj, i: int, path: str, step: str):
167181
try:
168182
r = eval(step, None, {"__obj": obj})
169183
except Exception as err:
170-
LOG.error(err)
184+
logger.error(err)
171185
if r:
172186
self._trace(obj, i, path)
173187

@@ -185,7 +199,7 @@ def _trace(self, obj, i: int, path):
185199
self.result.append(obj)
186200
elif self.result_type == "PATH":
187201
self.result.append(path)
188-
LOG.debug(f"path: {path} | value: {obj}")
202+
logger.debug(f"path: {path} | value: {obj}")
189203
return
190204

191205
step = self.segments[i]
@@ -256,18 +270,18 @@ def _trace(self, obj, i: int, path):
256270
if isinstance(obj, dict):
257271
obj_ = {}
258272
for k in step[1:-1].split(","):
259-
obj_[k] = obj.get(k)
273+
obj_[k] = self._getattr(obj, k)
260274
self._trace(obj_, i + 1, path)
261275
else:
262-
raise ExprSyntaxError("field-extractor must acting on list or dict")
276+
raise ExprSyntaxError("field-extractor must acting on dict")
263277

264278
return
265279

266280

267281
if __name__ == "__main__":
268282
with open("test/data/2.json", "rb") as f:
269283
d = json.load(f)
270-
D = JSONPath("$[bicycle, scores]").parse(d, "VALUE")
284+
D = JSONPath("$.scores[/(score)].(score)").parse(d, "VALUE")
271285
print(D)
272286
for v in D:
273287
print(v)

test/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@
7979
{"title": "Sayings of the Century", "price": 8.95},
8080
],
8181
),
82+
TestCase(
83+
"$.book[*].(title,brand.version)",
84+
data,
85+
[
86+
{"title": "Sayings of the Century", "brand.version": "v1.0.0"},
87+
{"title": "Sword of Honour", "brand.version": "v0.0.1"},
88+
{"title": "Moby Dick", "brand.version": "v1.0.2"},
89+
{"title": "The Lord of the Rings", "brand.version": "v1.0.3"},
90+
],
91+
),
8292
]
8393
)
8494
def value_cases(request):

0 commit comments

Comments
 (0)