Skip to content

Commit 37efc68

Browse files
committed
Defer pyobject introspection until warnings are actually raised, leading to perf boost for union serialization
1 parent 9975030 commit 37efc68

File tree

10 files changed

+57
-32
lines changed

10 files changed

+57
-32
lines changed

src/serializers/errors.rs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ use std::fmt;
22

33
use pyo3::exceptions::PyValueError;
44
use pyo3::prelude::*;
5+
use pyo3::types::PyString;
6+
7+
use crate::tools::truncate_safe_repr;
58

69
use serde::ser;
710

@@ -44,9 +47,9 @@ pub(super) fn se_err_py_err(error: PythonSerializerError) -> PyErr {
4447
let s = error.to_string();
4548
if let Some(msg) = s.strip_prefix(UNEXPECTED_TYPE_SER_MARKER) {
4649
if msg.is_empty() {
47-
PydanticSerializationUnexpectedValue::new_err(None)
50+
PydanticSerializationUnexpectedValue::new_from_msg(None)
4851
} else {
49-
PydanticSerializationUnexpectedValue::new_err(Some(msg.to_string()))
52+
PydanticSerializationUnexpectedValue::new_from_msg(Some(msg.to_string()))
5053
}
5154
} else if let Some(msg) = s.strip_prefix(SERIALIZATION_ERR_MARKER) {
5255
PydanticSerializationError::new_err(msg.to_string())
@@ -94,30 +97,57 @@ impl PydanticSerializationError {
9497
#[derive(Debug, Clone)]
9598
pub struct PydanticSerializationUnexpectedValue {
9699
message: Option<String>,
100+
field_type: Option<String>,
101+
input_value: Option<PyObject>,
97102
}
98103

99104
impl PydanticSerializationUnexpectedValue {
100-
pub(crate) fn new_err(msg: Option<String>) -> PyErr {
101-
PyErr::new::<Self, Option<String>>(msg)
105+
pub fn new_from_msg(message: Option<String>) -> PyErr {
106+
PyErr::new::<Self, (Option<String>, Option<String>, Option<PyObject>)>((message, None, None))
107+
}
108+
109+
pub fn new_from_parts(field_type: String, input_value: PyObject) -> PyErr {
110+
PyErr::new::<Self, (Option<String>, Option<String>, Option<PyObject>)>((
111+
None,
112+
Some(field_type),
113+
Some(input_value),
114+
))
102115
}
103116
}
104117

105118
#[pymethods]
106119
impl PydanticSerializationUnexpectedValue {
107120
#[new]
108-
#[pyo3(signature = (message=None))]
109-
fn py_new(message: Option<String>) -> Self {
110-
Self { message }
121+
#[pyo3(signature = (message=None, field_type=None, input_value=None))]
122+
fn py_new(message: Option<String>, field_type: Option<String>, input_value: Option<PyObject>) -> Self {
123+
Self {
124+
message,
125+
field_type,
126+
input_value,
127+
}
111128
}
112129

113-
fn __str__(&self) -> &str {
114-
match self.message {
115-
Some(ref s) => s,
116-
None => "Unexpected Value",
130+
fn __str__(&self, py: Python) -> String {
131+
match &self.message {
132+
Some(s) => s.to_string(),
133+
None => match (&self.field_type, &self.input_value) {
134+
(Some(ref field_type), Some(ref input_value)) => {
135+
let bound_input = input_value.bind(py);
136+
137+
let type_name = bound_input
138+
.get_type()
139+
.qualname()
140+
.unwrap_or_else(|_| PyString::new(py, "<unknown python object>"));
141+
142+
let value_str = truncate_safe_repr(bound_input, None);
143+
format!("Expected `{field_type}` but got `{type_name}` with value `{value_str}` - serialized value may not be as expected")
144+
}
145+
_ => "Unexpected value - serialized value may not be as expected".to_string(),
146+
},
117147
}
118148
}
119149

120-
pub(crate) fn __repr__(&self) -> String {
121-
format!("PydanticSerializationUnexpectedValue({})", self.__str__())
150+
pub(crate) fn __repr__(&self, py: Python) -> String {
151+
format!("PydanticSerializationUnexpectedValue({})", self.__str__(py))
122152
}
123153
}

src/serializers/extra.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::ffi::CString;
33
use std::fmt;
44
use std::sync::Mutex;
55

6+
use crate::tools::truncate_safe_repr;
67
use pyo3::exceptions::{PyTypeError, PyUserWarning, PyValueError};
78
use pyo3::prelude::*;
89
use pyo3::types::{PyBool, PyString};
@@ -17,7 +18,6 @@ use crate::recursion_guard::ContainsRecursionState;
1718
use crate::recursion_guard::RecursionError;
1819
use crate::recursion_guard::RecursionGuard;
1920
use crate::recursion_guard::RecursionState;
20-
use crate::tools::truncate_safe_repr;
2121
use crate::PydanticSerializationError;
2222

2323
/// this is ugly, would be much better if extra could be stored in `SerializationState`
@@ -415,15 +415,10 @@ impl CollectWarnings {
415415
if value.is_none() {
416416
Ok(())
417417
} else if extra.check.enabled() {
418-
let type_name = value
419-
.get_type()
420-
.qualname()
421-
.unwrap_or_else(|_| PyString::new(value.py(), "<unknown python object>"));
422-
423-
let value_str = truncate_safe_repr(value, None);
424-
Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
425-
"Expected `{field_type}` but got `{type_name}` with value `{value_str}` - serialized value may not be as expected"
426-
))))
418+
Err(PydanticSerializationUnexpectedValue::new_from_parts(
419+
field_type.to_string(),
420+
value.clone().unbind(),
421+
))
427422
} else {
428423
self.fallback_warning(field_type, value);
429424
Ok(())

src/serializers/fields.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ impl GeneralFieldsSerializer {
202202
output_dict.set_item(key, value)?;
203203
} else if field_extra.check == SerCheck::Strict {
204204
let type_name = field_extra.model_type_name();
205-
return Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
205+
return Err(PydanticSerializationUnexpectedValue::new_from_msg(Some(format!(
206206
"Unexpected field `{key}`{for_type_name}",
207207
for_type_name = if let Some(type_name) = type_name {
208208
format!(" for type `{type_name}`")
@@ -227,7 +227,7 @@ impl GeneralFieldsSerializer {
227227
None => "<unknown python object>".to_string(),
228228
};
229229

230-
Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
230+
Err(PydanticSerializationUnexpectedValue::new_from_msg(Some(format!(
231231
"Expected {required_fields} fields but got {used_req_fields}{for_type_name} with value `{field_value}` - serialized value may not be as expected.",
232232
for_type_name = if let Some(type_name) = type_name { format!(" for type `{type_name}`") } else { String::new() },
233233
))))

src/serializers/type_serializers/datetime_etc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fn downcast_date_reject_datetime<'a, 'py>(py_date: &'a Bound<'py, PyAny>) -> PyR
3434
}
3535
}
3636

37-
Err(PydanticSerializationUnexpectedValue::new_err(None))
37+
Err(PydanticSerializationUnexpectedValue::new_from_msg(None))
3838
}
3939

4040
macro_rules! build_serializer {

src/serializers/type_serializers/float.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ impl TypeSerializer for FloatSerializer {
7575
match extra.ob_type_lookup.is_type(value, ObType::Float) {
7676
IsType::Exact => Ok(value.clone().unbind()),
7777
IsType::Subclass => match extra.check {
78-
SerCheck::Strict => Err(PydanticSerializationUnexpectedValue::new_err(None)),
78+
SerCheck::Strict => Err(PydanticSerializationUnexpectedValue::new_from_msg(None)),
7979
SerCheck::Lax | SerCheck::None => match extra.mode {
8080
SerMode::Json => value.extract::<f64>()?.into_py_any(py),
8181
_ => infer_to_python(value, include, exclude, extra),

src/serializers/type_serializers/function.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ fn on_error(py: Python, err: PyErr, function_name: &str, extra: &Extra) -> PyRes
194194
if extra.check.enabled() {
195195
Err(err)
196196
} else {
197-
extra.warnings.custom_warning(ser_err.__repr__());
197+
extra.warnings.custom_warning(ser_err.__repr__(py));
198198
Ok(())
199199
}
200200
} else if let Ok(err) = exception.extract::<PydanticSerializationError>() {

src/serializers/type_serializers/model.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ impl TypeSerializer for ModelSerializer {
190190
let py = value.py();
191191
let root = value.getattr(intern!(py, ROOT_FIELD)).map_err(|original_err| {
192192
if root_extra.check.enabled() {
193-
PydanticSerializationUnexpectedValue::new_err(None)
193+
PydanticSerializationUnexpectedValue::new_from_msg(None)
194194
} else {
195195
original_err
196196
}

src/serializers/type_serializers/simple.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ macro_rules! build_simple_serializer {
123123
match extra.ob_type_lookup.is_type(value, $ob_type) {
124124
IsType::Exact => Ok(value.clone().unbind()),
125125
IsType::Subclass => match extra.check {
126-
SerCheck::Strict => Err(PydanticSerializationUnexpectedValue::new_err(None)),
126+
SerCheck::Strict => Err(PydanticSerializationUnexpectedValue::new_from_msg(None)),
127127
SerCheck::Lax | SerCheck::None => match extra.mode {
128128
SerMode::Json => value.extract::<$rust_type>()?.into_py_any(py),
129129
_ => infer_to_python(value, include, exclude, extra),

src/serializers/type_serializers/tuple.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ impl TupleSerializer {
214214
.chain(self.serializers[variadic_item_index + 1..].iter());
215215
use_serializers!(serializers_iter);
216216
} else if extra.check == SerCheck::Strict && n_items != self.serializers.len() {
217-
return Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
217+
return Err(PydanticSerializationUnexpectedValue::new_from_msg(Some(format!(
218218
"Expected {} items, but got {}",
219219
self.serializers.len(),
220220
n_items

src/serializers/type_serializers/union.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ fn union_serialize<S>(
111111
// care of the formatting for us
112112
else if !errors.is_empty() {
113113
let message = errors.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n");
114-
return Err(PydanticSerializationUnexpectedValue::new_err(Some(message)));
114+
return Err(PydanticSerializationUnexpectedValue::new_from_msg(Some(message)));
115115
}
116116

117117
Ok(None)

0 commit comments

Comments
 (0)