Skip to content

Commit 5e38031

Browse files
committed
fix: resolve issue #9 by filtering out missing keys in field extractor
1 parent 8ac2080 commit 5e38031

File tree

2 files changed

+60
-8
lines changed

2 files changed

+60
-8
lines changed

jsonpath/jsonpath.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class JSONPath:
4141

4242
# common patterns
4343
SEP = ";"
44+
_MISSING = object()
4445
REP_DOUBLEDOT = re.compile(r"\.\.")
4546
REP_DOT = re.compile(r"(?<!\.)\.(?!\.)")
4647

@@ -177,11 +178,14 @@ def _traverse(f, obj, i: int, path: str, *args):
177178
def _getattr(obj: dict, path: str, *, convert_number_str=False):
178179
r = obj
179180
for k in path.split("."):
180-
try:
181-
r = r.get(k)
182-
except (AttributeError, KeyError) as err:
183-
logger.error(err)
184-
return None
181+
if isinstance(r, dict):
182+
if k in r:
183+
r = r[k]
184+
else:
185+
return JSONPath._MISSING
186+
else:
187+
return JSONPath._MISSING
188+
185189
if convert_number_str and isinstance(r, str):
186190
try:
187191
if r.isdigit():
@@ -193,15 +197,19 @@ def _getattr(obj: dict, path: str, *, convert_number_str=False):
193197

194198
@staticmethod
195199
def _sorter(obj, sortbys):
200+
def key_func(t, k):
201+
v = JSONPath._getattr(t[1], k, convert_number_str=True)
202+
return v if v is not JSONPath._MISSING else None
203+
196204
try:
197205
for sortby in sortbys.split(",")[::-1]:
198206
if sortby.startswith("~"):
199207
obj.sort(
200-
key=lambda t, k=sortby: JSONPath._getattr(t[1], k[1:], convert_number_str=True),
208+
key=lambda t, k=sortby: key_func(t, k[1:]),
201209
reverse=True,
202210
)
203211
else:
204-
obj.sort(key=lambda t, k=sortby: JSONPath._getattr(t[1], k, convert_number_str=True))
212+
obj.sort(key=lambda t, k=sortby: key_func(t, k))
205213
except TypeError as e:
206214
raise JSONPathTypeError(f"not possible to compare str and int when sorting: {e}") from e
207215

@@ -314,7 +322,9 @@ def _trace(self, obj, i: int, path):
314322
if isinstance(obj, dict):
315323
obj_ = {}
316324
for k in step[1:-1].split(","):
317-
obj_[k] = self._getattr(obj, k)
325+
v = self._getattr(obj, k)
326+
if v is not JSONPath._MISSING:
327+
obj_[k] = v
318328
self._trace(obj_, i + 1, path)
319329
else:
320330
raise ExprSyntaxError("field-extractor must acting on dict")

tests/test_issues.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,45 @@ def test_issue_10_mixed_type_sorting():
101101
data6 = [{"code": 1}, {"other": 2}]
102102
with pytest.raises(JSONPathTypeError):
103103
JSONPath("$./(code)").parse(data6)
104+
105+
106+
def test_issue_9_filter_nulls_in_field_extractor():
107+
data = [
108+
{
109+
"author": [
110+
{"fullname": "some fullname", "rank": 3},
111+
{
112+
"fullname": "other fullname",
113+
"pid": {
114+
"id": {"scheme": "orcid", "value": "0000-0000-0000-0000"},
115+
"provenance": {"provenance": "Harvested", "trust": "0.91"},
116+
},
117+
"rank": 4,
118+
},
119+
]
120+
}
121+
]
122+
123+
expr = "$.*.author[*].(fullname,pid.id.value,pid.id.scheme)"
124+
result = JSONPath(expr).parse(data)
125+
126+
# First item should only have fullname, as pid.id... are missing
127+
assert result[0] == {"fullname": "some fullname"}
128+
129+
# Second item should have all fields
130+
assert result[1] == {"fullname": "other fullname", "pid.id.value": "0000-0000-0000-0000", "pid.id.scheme": "orcid"}
131+
132+
133+
def test_sorting_with_missing_keys():
134+
# Verify that missing keys are treated as None during sort (and thus come first or raise error if mixed)
135+
# Case 1: Missing keys vs Integers
136+
# None vs Int -> TypeError in Python 3. So this should raise JSONPathTypeError
137+
data = [{"val": 10}, {"other": 5}] # second item has missing "val" -> None
138+
139+
with pytest.raises(JSONPathTypeError):
140+
JSONPath("$./(val)").parse(data)
141+
142+
# Case 2: All missing keys (all None) -> Should raise JSONPathTypeError because None < None is not supported in Python 3
143+
data2 = [{"other": 1}, {"other": 2}]
144+
with pytest.raises(JSONPathTypeError):
145+
JSONPath("$./(val)").parse(data2)

0 commit comments

Comments
 (0)