Skip to content

Commit ca83f0a

Browse files
committed
nicer error when hitting recursion error while serializing, fix pydantic/pydantic#5348
1 parent c9a83c8 commit ca83f0a

File tree

3 files changed

+72
-52
lines changed

3 files changed

+72
-52
lines changed

src/serializers/errors.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ pub struct PydanticSerializationError {
3737
message: String,
3838
}
3939

40+
impl fmt::Display for PydanticSerializationError {
41+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42+
write!(f, "{}", self.message)
43+
}
44+
}
45+
4046
impl PydanticSerializationError {
4147
pub(crate) fn new_err(msg: String) -> PyErr {
4248
PyErr::new::<Self, String>(msg)

src/serializers/type_serializers/function.rs

Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
use std::borrow::Cow;
22
use std::str::FromStr;
33

4-
use pyo3::exceptions::{PyAttributeError, PyRuntimeError};
4+
use pyo3::exceptions::{PyAttributeError, PyRecursionError, PyRuntimeError};
55
use pyo3::intern;
66
use pyo3::prelude::*;
77
use pyo3::types::PyDict;
88

99
use pyo3::types::PyString;
10-
use serde::ser::Error;
1110

12-
use crate::build_tools::{function_name, py_error_type, SchemaDict};
11+
use crate::build_tools::{function_name, py_err, py_error_type, SchemaDict};
1312
use crate::definitions::DefinitionsBuilder;
1413
use crate::{PydanticOmit, PydanticSerializationUnexpectedValue};
1514

@@ -154,6 +153,26 @@ impl FunctionPlainSerializer {
154153
}
155154
}
156155

156+
fn on_error(py: Python, err: PyErr, function_name: &str, extra: &Extra) -> PyResult<()> {
157+
let exception = err.value(py);
158+
if let Ok(ser_err) = exception.extract::<PydanticSerializationUnexpectedValue>() {
159+
if extra.check.enabled() {
160+
Err(err)
161+
} else {
162+
extra.warnings.custom_warning(ser_err.__repr__());
163+
Ok(())
164+
}
165+
} else if let Ok(err) = exception.extract::<PydanticSerializationError>() {
166+
py_err!(PydanticSerializationError; "{}", err)
167+
} else if exception.is_instance_of::<PyRecursionError>().unwrap_or(false) {
168+
py_err!(PydanticSerializationError; "Error calling function `{}`: RecursionError", function_name)
169+
} else {
170+
let new_err = py_error_type!(PydanticSerializationError; "Error calling function `{}`: {}", function_name, err);
171+
new_err.set_cause(py, Some(err));
172+
Err(new_err)
173+
}
174+
}
175+
157176
macro_rules! function_type_serializer {
158177
($name:ident) => {
159178
impl TypeSerializer for $name {
@@ -177,21 +196,10 @@ macro_rules! function_type_serializer {
177196
_ => Ok(next_value.to_object(py)),
178197
}
179198
}
180-
Err(err) => match err.value(py).extract::<PydanticSerializationUnexpectedValue>() {
181-
Ok(ser_err) => {
182-
if extra.check.enabled() {
183-
Err(err)
184-
} else {
185-
extra.warnings.custom_warning(ser_err.__repr__());
186-
infer_to_python(value, include, exclude, extra)
187-
}
188-
}
189-
Err(_) => {
190-
let new_err = py_error_type!(PydanticSerializationError; "Error calling function `{}`: {}", self.function_name, err);
191-
new_err.set_cause(py, Some(err));
192-
Err(new_err)
193-
}
194-
},
199+
Err(err) => {
200+
on_error(py, err, &self.function_name, extra)?;
201+
infer_to_python(value, include, exclude, extra)
202+
}
195203
}
196204
}
197205

@@ -205,21 +213,10 @@ macro_rules! function_type_serializer {
205213
None => infer_json_key(next_key, extra),
206214
}
207215
}
208-
Err(err) => match err.value(py).extract::<PydanticSerializationUnexpectedValue>() {
209-
Ok(ser_err) => {
210-
if extra.check.enabled() {
211-
Err(err)
212-
} else {
213-
extra.warnings.custom_warning(ser_err.__repr__());
214-
infer_json_key(key, extra)
215-
}
216-
}
217-
Err(_) => {
218-
let new_err = py_error_type!(PydanticSerializationError; "Error calling function `{}`: {}", self.function_name, err);
219-
new_err.set_cause(py, Some(err));
220-
Err(new_err)
221-
}
222-
},
216+
Err(err) => {
217+
on_error(py, err, &self.function_name, extra)?;
218+
infer_json_key(key, extra)
219+
}
223220
}
224221
}
225222

@@ -235,28 +232,18 @@ macro_rules! function_type_serializer {
235232
match self.call(value, include, exclude, extra) {
236233
Ok(v) => {
237234
let next_value = v.as_ref(py);
238-
// None for include/exclude here, as filtering should be done
235+
// None for include/exclude here, as filtering should be done
239236
match self.json_return_ob_type {
240237
Some(ref ob_type) => {
241238
infer_serialize_known(ob_type, next_value, serializer, None, None, extra)
242239
}
243240
None => infer_serialize(next_value, serializer, None, None, extra),
244241
}
245242
}
246-
Err(err) => match err.value(py).extract::<PydanticSerializationUnexpectedValue>() {
247-
Ok(ser_err) => {
248-
if extra.check.enabled() {
249-
Err(py_err_se_err(err))
250-
} else {
251-
extra.warnings.custom_warning(ser_err.__repr__());
252-
infer_serialize(value, serializer, include, exclude, extra)
253-
}
254-
}
255-
Err(_) => Err(Error::custom(format!(
256-
"Error calling function `{}`: {}",
257-
self.function_name, err
258-
))),
259-
},
243+
Err(err) => {
244+
on_error(py, err, &self.function_name, extra).map_err(py_err_se_err)?;
245+
infer_serialize(value, serializer, include, exclude, extra)
246+
}
260247
}
261248
}
262249

tests/serializers/test_functions.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,10 @@ def fallback(v):
406406
assert s.to_json('foo') == b'"result=foo"'
407407

408408
assert s.to_python(Foobar()) == 'result=foobar!'
409-
with pytest.raises(PydanticSerializationError, match='Error calling function `f`'):
410-
assert s.to_python(Foobar(), mode='json') == 'result=foobar!'
411-
with pytest.raises(PydanticSerializationError, match='Error calling function `f`'):
412-
assert s.to_json(Foobar()) == b'"result=foobar!"'
409+
with pytest.raises(PydanticSerializationError, match='Unable to serialize unknown type:'):
410+
s.to_python(Foobar(), mode='json')
411+
with pytest.raises(PydanticSerializationError, match='Unable to serialize unknown type:'):
412+
s.to_json(Foobar())
413413

414414
assert s.to_python(Foobar(), fallback=fallback) == 'result=fallback:foobar!'
415415
assert s.to_python(Foobar(), mode='json', fallback=fallback) == 'result=fallback:foobar!'
@@ -630,3 +630,30 @@ def f(value, handler, _info):
630630
s = SchemaSerializer(core_schema.general_wrap_validator_function(f, core_schema.int_schema()))
631631
with pytest.warns(UserWarning, match='Expected `int` but got `str` - serialized value may not be as expected'):
632632
assert s.to_python('abc') == 'abc'
633+
634+
635+
def test_recursive_call():
636+
def bad_recursive(value):
637+
return s.to_python(value)
638+
639+
s = SchemaSerializer(
640+
core_schema.any_schema(serialization=core_schema.plain_serializer_function_ser_schema(bad_recursive))
641+
)
642+
with pytest.raises(PydanticSerializationError) as exc_info:
643+
s.to_python(42)
644+
# insert_assert(str(exc_info.value))
645+
assert str(exc_info.value) == 'Error calling function `bad_recursive`: RecursionError'
646+
647+
with pytest.raises(PydanticSerializationError) as exc_info:
648+
s.to_python(42, mode='json')
649+
# insert_assert(str(exc_info.value))
650+
assert str(exc_info.value) == 'Error calling function `bad_recursive`: RecursionError'
651+
652+
with pytest.raises(PydanticSerializationError) as exc_info:
653+
s.to_json(42)
654+
# insert_assert(str(exc_info.value))
655+
assert str(exc_info.value) == (
656+
'Error serializing to JSON: '
657+
'PydanticSerializationError: Error calling function `bad_recursive`: '
658+
'RuntimeError: Already mutably borrowed'
659+
)

0 commit comments

Comments
 (0)