Skip to content

Commit e3bd7f5

Browse files
committed
support path on PythonJsonError
1 parent d15c046 commit e3bd7f5

File tree

5 files changed

+275
-35
lines changed

5 files changed

+275
-35
lines changed

crates/jiter-python/jiter.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def from_json(
1010
partial_mode: Literal[True, False, "off", "on", "trailing-strings"] = False,
1111
catch_duplicate_keys: bool = False,
1212
lossless_floats: bool = False,
13+
error_in_path: bool = False,
1314
) -> Any:
1415
"""
1516
Parse input bytes into a JSON object.
@@ -28,6 +29,7 @@ def from_json(
2829
- 'trailing-strings' - allow incomplete JSON, and include the last incomplete string in the output
2930
catch_duplicate_keys: if True, raise an exception if objects contain the same key multiple times
3031
lossless_floats: if True, preserve full detail on floats using `LosslessFloat`
32+
error_in_path: Whether to include the JSON path to the invalid JSON in `JsonParseError`
3133
3234
Returns:
3335
Python object built from the JSON input.
@@ -81,6 +83,9 @@ class JsonParseError(ValueError):
8183
def description(self) -> str:
8284
...
8385

86+
def path(self) -> list[str | int]:
87+
...
88+
8489
def index(self) -> int:
8590
...
8691

crates/jiter-python/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use pyo3::prelude::*;
55
use jiter::{JsonParseError, LosslessFloat, PartialMode, PythonParse, StringCacheMode};
66

77
#[allow(clippy::fn_params_excessive_bools)]
8+
#[allow(clippy::too_many_arguments)]
89
#[pyfunction(
910
signature = (
1011
json_data,
@@ -15,6 +16,7 @@ use jiter::{JsonParseError, LosslessFloat, PartialMode, PythonParse, StringCache
1516
partial_mode=PartialMode::Off,
1617
catch_duplicate_keys=false,
1718
lossless_floats=false,
19+
error_in_path=false,
1820
)
1921
)]
2022
pub fn from_json<'py>(
@@ -25,13 +27,15 @@ pub fn from_json<'py>(
2527
partial_mode: PartialMode,
2628
catch_duplicate_keys: bool,
2729
lossless_floats: bool,
30+
error_in_path: bool,
2831
) -> PyResult<Bound<'py, PyAny>> {
2932
let parse_builder = PythonParse {
3033
allow_inf_nan,
3134
cache_mode,
3235
partial_mode,
3336
catch_duplicate_keys,
3437
lossless_floats,
38+
error_in_path,
3539
};
3640
parse_builder.python_parse_exc(py, json_data)
3741
}

crates/jiter-python/tests/test_jiter.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,42 @@ def test_error():
4242

4343
assert exc_info.value.kind() == 'EofWhileParsingList'
4444
assert exc_info.value.description() == 'EOF while parsing a list'
45+
assert exc_info.value.path() == []
4546
assert exc_info.value.index() == 9
4647
assert exc_info.value.line() == 1
4748
assert exc_info.value.column() == 9
4849
assert repr(exc_info.value) == 'JsonParseError("EOF while parsing a list at line 1 column 9")'
4950

5051

52+
def test_error_path():
53+
with pytest.raises(jiter.JsonParseError, match="EOF while parsing a string at line 1 column 5") as exc_info:
54+
jiter.from_json(b'["str', error_in_path=True)
55+
56+
assert exc_info.value.kind() == 'EofWhileParsingString'
57+
assert exc_info.value.description() == 'EOF while parsing a string'
58+
assert exc_info.value.path() == [0]
59+
assert exc_info.value.index() == 5
60+
assert exc_info.value.line() == 1
61+
62+
63+
def test_error_path_empty():
64+
with pytest.raises(jiter.JsonParseError) as exc_info:
65+
jiter.from_json(b'"foo', error_in_path=True)
66+
67+
assert exc_info.value.kind() == 'EofWhileParsingString'
68+
assert exc_info.value.path() == []
69+
70+
71+
def test_error_path_object():
72+
with pytest.raises(jiter.JsonParseError) as exc_info:
73+
jiter.from_json(b'{"foo":\n[1,\n2, x', error_in_path=True)
74+
75+
assert exc_info.value.kind() == 'ExpectedSomeValue'
76+
assert exc_info.value.index() == 15
77+
assert exc_info.value.line() == 3
78+
assert exc_info.value.path() == ['foo', 2]
79+
80+
5181
def test_recursion_limit():
5282
with pytest.raises(
5383
jiter.JsonParseError, match="recursion limit exceeded at line 1 column 202"

crates/jiter/src/py_error.rs

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,24 @@ use crate::errors::{JsonError, LinePosition};
77
#[derive(Debug, Clone)]
88
pub struct JsonParseError {
99
json_error: JsonError,
10+
path: Vec<PathItem>,
1011
position: LinePosition,
1112
}
1213

1314
impl JsonParseError {
14-
pub fn new_err(py: Python, json_error: JsonError, json_data: &[u8]) -> PyErr {
15-
let position = json_error.get_position(json_data);
16-
let slf = Self { json_error, position };
15+
pub(crate) fn new_err(py: Python, py_json_error: PythonJsonError, json_data: &[u8]) -> PyErr {
16+
let position = py_json_error.error.get_position(json_data);
17+
let slf = Self {
18+
json_error: py_json_error.error,
19+
path: match py_json_error.path {
20+
Some(mut v) => {
21+
v.reverse();
22+
v
23+
}
24+
None => vec![],
25+
},
26+
position,
27+
};
1728
match Py::new(py, slf) {
1829
Ok(err) => PyErr::from_value_bound(err.into_bound(py).into_any()),
1930
Err(err) => err,
@@ -31,6 +42,10 @@ impl JsonParseError {
3142
self.json_error.error_type.to_string()
3243
}
3344

45+
fn path(&self, py: Python) -> PyObject {
46+
self.path.to_object(py)
47+
}
48+
3449
fn index(&self) -> usize {
3550
self.json_error.index
3651
}
@@ -51,3 +66,136 @@ impl JsonParseError {
5166
format!("JsonParseError({:?})", self.__str__())
5267
}
5368
}
69+
70+
pub(crate) trait MaybeBuildArrayPath: MaybeBuildPath {
71+
fn incr_index(&mut self);
72+
fn set_index_path(&self, err: PythonJsonError) -> PythonJsonError;
73+
}
74+
75+
pub(crate) trait MaybeBuildObjectPath: MaybeBuildPath {
76+
fn set_key(&mut self, key: &str);
77+
78+
fn set_key_path(&self, err: PythonJsonError) -> PythonJsonError;
79+
}
80+
81+
pub(crate) trait MaybeBuildPath {
82+
fn new_array() -> impl MaybeBuildArrayPath;
83+
fn new_object() -> impl MaybeBuildObjectPath;
84+
}
85+
86+
pub(crate) struct NoopBuildPath;
87+
88+
impl MaybeBuildPath for NoopBuildPath {
89+
fn new_array() -> NoopBuildPath {
90+
NoopBuildPath
91+
}
92+
93+
fn new_object() -> NoopBuildPath {
94+
NoopBuildPath
95+
}
96+
}
97+
98+
impl MaybeBuildArrayPath for NoopBuildPath {
99+
fn incr_index(&mut self) {}
100+
101+
fn set_index_path(&self, err: PythonJsonError) -> PythonJsonError {
102+
err
103+
}
104+
}
105+
106+
impl MaybeBuildObjectPath for NoopBuildPath {
107+
fn set_key(&mut self, _: &str) {}
108+
109+
fn set_key_path(&self, err: PythonJsonError) -> PythonJsonError {
110+
err
111+
}
112+
}
113+
114+
#[derive(Default)]
115+
pub(crate) struct ActiveBuildPath {
116+
index: usize,
117+
}
118+
119+
impl MaybeBuildPath for ActiveBuildPath {
120+
fn new_array() -> ActiveBuildPath {
121+
ActiveBuildPath::default()
122+
}
123+
124+
fn new_object() -> ActiveObjectBuildPath {
125+
ActiveObjectBuildPath::default()
126+
}
127+
}
128+
129+
impl MaybeBuildArrayPath for ActiveBuildPath {
130+
fn incr_index(&mut self) {
131+
self.index += 1;
132+
}
133+
134+
fn set_index_path(&self, mut err: PythonJsonError) -> PythonJsonError {
135+
err.add(PathItem::Index(self.index));
136+
err
137+
}
138+
}
139+
140+
#[derive(Default)]
141+
pub(crate) struct ActiveObjectBuildPath {
142+
key: String,
143+
}
144+
145+
impl MaybeBuildPath for ActiveObjectBuildPath {
146+
fn new_array() -> ActiveBuildPath {
147+
ActiveBuildPath::default()
148+
}
149+
150+
fn new_object() -> ActiveObjectBuildPath {
151+
ActiveObjectBuildPath::default()
152+
}
153+
}
154+
155+
impl MaybeBuildObjectPath for ActiveObjectBuildPath {
156+
fn set_key(&mut self, key: &str) {
157+
self.key = key.to_string();
158+
}
159+
160+
fn set_key_path(&self, mut err: PythonJsonError) -> PythonJsonError {
161+
err.add(PathItem::Key(self.key.clone()));
162+
err
163+
}
164+
}
165+
166+
#[derive(Debug, Clone)]
167+
enum PathItem {
168+
Index(usize),
169+
Key(String),
170+
}
171+
172+
impl ToPyObject for PathItem {
173+
fn to_object(&self, py: Python<'_>) -> PyObject {
174+
match self {
175+
Self::Index(index) => index.to_object(py),
176+
Self::Key(str) => str.to_object(py),
177+
}
178+
}
179+
}
180+
181+
pub struct PythonJsonError {
182+
pub error: JsonError,
183+
path: Option<Vec<PathItem>>,
184+
}
185+
186+
pub(crate) type PythonJsonResult<T> = Result<T, PythonJsonError>;
187+
188+
impl From<JsonError> for PythonJsonError {
189+
fn from(error: JsonError) -> Self {
190+
Self { error, path: None }
191+
}
192+
}
193+
194+
impl PythonJsonError {
195+
fn add(&mut self, path_item: PathItem) {
196+
match self.path.as_mut() {
197+
Some(path) => path.push(path_item),
198+
None => self.path = Some(vec![path_item]),
199+
}
200+
}
201+
}

0 commit comments

Comments
 (0)