Skip to content

Commit 09be2b3

Browse files
committed
add JsonParseError
1 parent 3b59df6 commit 09be2b3

File tree

7 files changed

+143
-21
lines changed

7 files changed

+143
-21
lines changed

crates/jiter-python/jiter.pyi

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,35 @@ class LosslessFloat:
6363
def __bytes__(self) -> bytes:
6464
"""Return the JSON bytes slice as bytes"""
6565

66-
def __str__(self):
66+
def __str__(self) -> str:
6767
"""Return the JSON bytes slice as a string"""
6868

69-
def __repr__(self):
69+
def __repr__(self) -> str:
70+
...
71+
72+
73+
class JsonParseError(ValueError):
74+
"""
75+
Represents details of failed JSON parsing.
76+
"""
77+
78+
def kind(self) -> str:
79+
...
80+
81+
def description(self) -> str:
82+
...
83+
84+
def index(self) -> int:
85+
...
86+
87+
def line(self) -> int:
88+
...
89+
90+
def column(self) -> int:
91+
...
92+
93+
def __str__(self) -> str:
94+
"""String summary of the error, combined description and position"""
95+
96+
def __repr__(self) -> str:
7097
...

crates/jiter-python/src/lib.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::sync::OnceLock;
22

33
use pyo3::prelude::*;
44

5-
use jiter::{map_json_error, LosslessFloat, PartialMode, PythonParse, StringCacheMode};
5+
use jiter::{JsonParseError, LosslessFloat, PartialMode, PythonParse, StringCacheMode};
66

77
#[allow(clippy::fn_params_excessive_bools)]
88
#[pyfunction(
@@ -33,9 +33,7 @@ pub fn from_json<'py>(
3333
catch_duplicate_keys,
3434
lossless_floats,
3535
};
36-
parse_builder
37-
.python_parse(py, json_data)
38-
.map_err(|e| map_json_error(json_data, &e))
36+
parse_builder.python_parse_exc(py, json_data)
3937
}
4038

4139
pub fn get_jiter_version() -> &'static str {
@@ -70,5 +68,6 @@ fn jiter_python(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
7068
m.add_function(wrap_pyfunction!(cache_clear, m)?)?;
7169
m.add_function(wrap_pyfunction!(cache_usage, m)?)?;
7270
m.add_class::<LosslessFloat>()?;
71+
m.add_class::<JsonParseError>()?;
7372
Ok(())
7473
}

crates/jiter-python/tests/test_jiter.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
from dirty_equals import IsFloatNan
88

99

10-
def test_python_parse_numeric():
10+
def test_parse_numeric():
1111
parsed = jiter.from_json(
1212
b' { "int": 1, "bigint": 123456789012345678901234567890, "float": 1.2} '
1313
)
1414
assert parsed == {"int": 1, "bigint": 123456789012345678901234567890, "float": 1.2}
1515

1616

17-
def test_python_parse_other_cached():
17+
def test_parse_other_cached():
1818
parsed = jiter.from_json(
1919
b'["string", true, false, null, NaN, Infinity, -Infinity]',
2020
allow_inf_nan=True,
@@ -23,27 +23,34 @@ def test_python_parse_other_cached():
2323
assert parsed == ["string", True, False, None, IsFloatNan(), inf, -inf]
2424

2525

26-
def test_python_parse_other_no_cache():
26+
def test_parse_other_no_cache():
2727
parsed = jiter.from_json(
2828
b'["string", true, false, null]',
2929
cache_mode=False,
3030
)
3131
assert parsed == ["string", True, False, None]
3232

3333

34-
def test_python_disallow_nan():
35-
with pytest.raises(ValueError, match="expected value at line 1 column 2"):
34+
def test_disallow_nan():
35+
with pytest.raises(jiter.JsonParseError, match="expected value at line 1 column 2"):
3636
jiter.from_json(b"[NaN]", allow_inf_nan=False)
3737

3838

3939
def test_error():
40-
with pytest.raises(ValueError, match="EOF while parsing a list at line 1 column 9"):
40+
with pytest.raises(jiter.JsonParseError, match="EOF while parsing a list at line 1 column 9") as exc_info:
4141
jiter.from_json(b'["string"')
4242

43+
assert exc_info.value.kind() == 'EofWhileParsingList'
44+
assert exc_info.value.description() == 'EOF while parsing a list'
45+
assert exc_info.value.index() == 9
46+
assert exc_info.value.line() == 1
47+
assert exc_info.value.column() == 9
48+
assert repr(exc_info.value) == 'JsonParseError("EOF while parsing a list at line 1 column 9")'
49+
4350

4451
def test_recursion_limit():
4552
with pytest.raises(
46-
ValueError, match="recursion limit exceeded at line 1 column 202"
53+
jiter.JsonParseError, match="recursion limit exceeded at line 1 column 202"
4754
):
4855
jiter.from_json(b"[" * 10_000)
4956

@@ -150,21 +157,21 @@ def test_partial_nested():
150157
assert isinstance(parsed, dict)
151158

152159

153-
def test_python_cache_usage_all():
160+
def test_cache_usage_all():
154161
jiter.cache_clear()
155162
parsed = jiter.from_json(b'{"foo": "bar", "spam": 3}', cache_mode="all")
156163
assert parsed == {"foo": "bar", "spam": 3}
157164
assert jiter.cache_usage() == 3
158165

159166

160-
def test_python_cache_usage_keys():
167+
def test_cache_usage_keys():
161168
jiter.cache_clear()
162169
parsed = jiter.from_json(b'{"foo": "bar", "spam": 3}', cache_mode="keys")
163170
assert parsed == {"foo": "bar", "spam": 3}
164171
assert jiter.cache_usage() == 2
165172

166173

167-
def test_python_cache_usage_none():
174+
def test_cache_usage_none():
168175
jiter.cache_clear()
169176
parsed = jiter.from_json(
170177
b'{"foo": "bar", "spam": 3}',

crates/jiter/src/errors.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,35 @@ impl std::fmt::Display for JsonErrorType {
107107
}
108108
}
109109

110+
impl JsonErrorType {
111+
pub fn kind(&self) -> &'static str {
112+
match self {
113+
Self::FloatExpectingInt => "FloatExpectingInt",
114+
Self::DuplicateKey(_) => "DuplicateKey",
115+
Self::EofWhileParsingList => "EofWhileParsingList",
116+
Self::EofWhileParsingObject => "EofWhileParsingObject",
117+
Self::EofWhileParsingString => "EofWhileParsingString",
118+
Self::EofWhileParsingValue => "EofWhileParsingValue",
119+
Self::ExpectedColon => "ExpectedColon",
120+
Self::ExpectedListCommaOrEnd => "ExpectedListCommaOrEnd",
121+
Self::ExpectedObjectCommaOrEnd => "ExpectedObjectCommaOrEnd",
122+
Self::ExpectedSomeIdent => "ExpectedSomeIdent",
123+
Self::ExpectedSomeValue => "ExpectedSomeValue",
124+
Self::InvalidEscape => "InvalidEscape",
125+
Self::InvalidNumber => "InvalidNumber",
126+
Self::NumberOutOfRange => "NumberOutOfRange",
127+
Self::InvalidUnicodeCodePoint => "InvalidUnicodeCodePoint",
128+
Self::ControlCharacterWhileParsingString => "ControlCharacterWhileParsingString",
129+
Self::KeyMustBeAString => "KeyMustBeAString",
130+
Self::LoneLeadingSurrogateInHexEscape => "LoneLeadingSurrogateInHexEscape",
131+
Self::TrailingComma => "TrailingComma",
132+
Self::TrailingCharacters => "TrailingCharacters",
133+
Self::UnexpectedEndOfHexEscape => "UnexpectedEndOfHexEscape",
134+
Self::RecursionLimitExceeded => "RecursionLimitExceeded",
135+
}
136+
}
137+
}
138+
110139
pub type JsonResult<T> = Result<T, JsonError>;
111140

112141
/// Represents an error from parsing JSON

crates/jiter/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ mod lazy_index_map;
66
mod number_decoder;
77
mod parse;
88
#[cfg(feature = "python")]
9+
mod py_error;
10+
#[cfg(feature = "python")]
911
mod py_lossless_float;
1012
#[cfg(feature = "python")]
1113
mod py_string_cache;
@@ -23,9 +25,11 @@ pub use number_decoder::{NumberAny, NumberInt};
2325
pub use parse::Peek;
2426
pub use value::{JsonArray, JsonObject, JsonValue};
2527

28+
#[cfg(feature = "python")]
29+
pub use py_error::JsonParseError;
2630
#[cfg(feature = "python")]
2731
pub use py_lossless_float::LosslessFloat;
2832
#[cfg(feature = "python")]
2933
pub use py_string_cache::{cache_clear, cache_usage, cached_py_string, pystring_fast_new, StringCacheMode};
3034
#[cfg(feature = "python")]
31-
pub use python::{map_json_error, PartialMode, PythonParse};
35+
pub use python::{PartialMode, PythonParse};

crates/jiter/src/py_error.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use pyo3::exceptions::PyValueError;
2+
use pyo3::prelude::*;
3+
4+
use crate::errors::{JsonError, LinePosition};
5+
6+
#[pyclass(extends=PyValueError, module="jiter")]
7+
#[derive(Debug, Clone)]
8+
pub struct JsonParseError {
9+
json_error: JsonError,
10+
position: LinePosition,
11+
}
12+
13+
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 };
17+
match Py::new(py, slf) {
18+
Ok(err) => PyErr::from_value_bound(err.into_bound(py).into_any()),
19+
Err(err) => err,
20+
}
21+
}
22+
}
23+
24+
#[pymethods]
25+
impl JsonParseError {
26+
fn kind(&self) -> &'static str {
27+
self.json_error.error_type.kind()
28+
}
29+
30+
fn description(&self) -> String {
31+
self.json_error.error_type.to_string()
32+
}
33+
34+
fn index(&self) -> usize {
35+
self.json_error.index
36+
}
37+
38+
fn line(&self) -> usize {
39+
self.position.line
40+
}
41+
42+
fn column(&self) -> usize {
43+
self.position.column
44+
}
45+
46+
fn __str__(&self) -> String {
47+
format!("{} at {}", self.json_error.error_type, self.position)
48+
}
49+
50+
fn __repr__(&self) -> String {
51+
format!("JsonParseError({:?})", self.__str__())
52+
}
53+
}

crates/jiter/src/python.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use smallvec::SmallVec;
1212
use crate::errors::{json_err, json_error, JsonError, JsonResult, DEFAULT_RECURSION_LIMIT};
1313
use crate::number_decoder::{AbstractNumberDecoder, NumberAny, NumberRange};
1414
use crate::parse::{Parser, Peek};
15+
use crate::py_error::JsonParseError;
1516
use crate::py_string_cache::{StringCacheAll, StringCacheKeys, StringCacheMode, StringMaybeCache, StringNoCache};
1617
use crate::string_decoder::{StringDecoder, Tape};
1718
use crate::{JsonErrorType, LosslessFloat};
@@ -71,11 +72,13 @@ impl PythonParse {
7172
StringCacheMode::None => ppp_group!(StringNoCache),
7273
}
7374
}
74-
}
7575

76-
/// Map a `JsonError` to a `PyErr` which can be raised as an exception in Python as a `ValueError`.
77-
pub fn map_json_error(json_data: &[u8], json_error: &JsonError) -> PyErr {
78-
PyValueError::new_err(json_error.description(json_data))
76+
/// Like `python_parse`, but maps [`JsonError`] to a `PyErr` which can be raised as an exception in
77+
/// Python as a [`JsonParseError`].
78+
pub fn python_parse_exc<'py>(self, py: Python<'py>, json_data: &[u8]) -> PyResult<Bound<'py, PyAny>> {
79+
self.python_parse(py, json_data)
80+
.map_err(|e| JsonParseError::new_err(py, e, json_data))
81+
}
7982
}
8083

8184
struct PythonParser<'j, StringCache, KeyCheck, ParseNumber> {

0 commit comments

Comments
 (0)