Skip to content

Commit 66c8c58

Browse files
Clean up PydanticUnexpectedValueError (#1652)
1 parent 8b7bda6 commit 66c8c58

29 files changed

+346
-203
lines changed

src/serializers/errors.rs

Lines changed: 76 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).to_py_err()
4851
} else {
49-
PydanticSerializationUnexpectedValue::new_err(Some(msg.to_string()))
52+
PydanticSerializationUnexpectedValue::new_from_msg(Some(msg.to_string())).to_py_err()
5053
}
5154
} else if let Some(msg) = s.strip_prefix(SERIALIZATION_ERR_MARKER) {
5255
PydanticSerializationError::new_err(msg.to_string())
@@ -94,30 +97,90 @@ 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>) -> Self {
106+
Self {
107+
message,
108+
field_type: None,
109+
input_value: None,
110+
}
111+
}
112+
113+
pub fn new_from_parts(field_type: Option<String>, input_value: Option<PyObject>) -> Self {
114+
Self {
115+
message: None,
116+
field_type,
117+
input_value,
118+
}
119+
}
120+
121+
pub fn new(message: Option<String>, field_type: Option<String>, input_value: Option<PyObject>) -> Self {
122+
Self {
123+
message,
124+
field_type,
125+
input_value,
126+
}
127+
}
128+
129+
pub fn to_py_err(&self) -> PyErr {
130+
PyErr::new::<Self, (Option<String>, Option<String>, Option<PyObject>)>((
131+
self.message.clone(),
132+
self.field_type.clone(),
133+
self.input_value.clone(),
134+
))
102135
}
103136
}
104137

105138
#[pymethods]
106139
impl PydanticSerializationUnexpectedValue {
107140
#[new]
108-
#[pyo3(signature = (message=None))]
109-
fn py_new(message: Option<String>) -> Self {
110-
Self { message }
141+
#[pyo3(signature = (message=None, field_type=None, input_value=None))]
142+
fn py_new(message: Option<String>, field_type: Option<String>, input_value: Option<PyObject>) -> Self {
143+
Self {
144+
message,
145+
field_type,
146+
input_value,
147+
}
111148
}
112149

113-
fn __str__(&self) -> &str {
114-
match self.message {
115-
Some(ref s) => s,
116-
None => "Unexpected Value",
150+
pub(crate) fn __str__(&self, py: Python) -> String {
151+
let mut message = self.message.as_deref().unwrap_or("").to_string();
152+
153+
if let Some(field_type) = &self.field_type {
154+
if !message.is_empty() {
155+
message.push_str(": ");
156+
}
157+
message.push_str(&format!("Expected `{field_type}`"));
158+
if self.input_value.is_some() {
159+
message.push_str(" - serialized value may not be as expected");
160+
}
161+
}
162+
163+
if let Some(input_value) = &self.input_value {
164+
let bound_input = input_value.bind(py);
165+
let input_type = bound_input
166+
.get_type()
167+
.name()
168+
.unwrap_or_else(|_| PyString::new(py, "<unknown python object>"))
169+
.to_string();
170+
171+
let value_str = truncate_safe_repr(bound_input, None);
172+
173+
message.push_str(&format!(" [input_value={value_str}, input_type={input_type}]"));
117174
}
175+
176+
if message.is_empty() {
177+
message = "Unexpected Value".to_string();
178+
}
179+
180+
message
118181
}
119182

120-
pub(crate) fn __repr__(&self) -> String {
121-
format!("PydanticSerializationUnexpectedValue({})", self.__str__())
183+
pub(crate) fn __repr__(&self, py: Python) -> String {
184+
format!("PydanticSerializationUnexpectedValue({})", self.__str__(py))
122185
}
123186
}

src/serializers/extra.rs

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ use crate::recursion_guard::ContainsRecursionState;
1717
use crate::recursion_guard::RecursionError;
1818
use crate::recursion_guard::RecursionGuard;
1919
use crate::recursion_guard::RecursionState;
20-
use crate::tools::truncate_safe_repr;
2120
use crate::PydanticSerializationError;
2221

2322
/// this is ugly, would be much better if extra could be stored in `SerializationState`
@@ -384,7 +383,7 @@ impl From<bool> for WarningsMode {
384383
pub(crate) struct CollectWarnings {
385384
mode: WarningsMode,
386385
// FIXME: mutex is to satisfy PyO3 0.23, we should be able to refactor this away
387-
warnings: Mutex<Vec<String>>,
386+
warnings: Mutex<Vec<PydanticSerializationUnexpectedValue>>,
388387
}
389388

390389
impl Clone for CollectWarnings {
@@ -404,9 +403,9 @@ impl CollectWarnings {
404403
}
405404
}
406405

407-
pub fn custom_warning(&self, warning: String) {
406+
pub fn register_warning(&self, warning: PydanticSerializationUnexpectedValue) {
408407
if self.mode != WarningsMode::None {
409-
self.add_warning(warning);
408+
self.warnings.lock().expect("lock poisoned").push(warning);
410409
}
411410
}
412411

@@ -415,15 +414,11 @@ impl CollectWarnings {
415414
if value.is_none() {
416415
Ok(())
417416
} 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-
))))
417+
Err(PydanticSerializationUnexpectedValue::new_from_parts(
418+
Some(field_type.to_string()),
419+
Some(value.clone().unbind()),
420+
)
421+
.to_py_err())
427422
} else {
428423
self.fallback_warning(field_type, value);
429424
Ok(())
@@ -452,23 +447,13 @@ impl CollectWarnings {
452447

453448
fn fallback_warning(&self, field_type: &str, value: &Bound<'_, PyAny>) {
454449
if self.mode != WarningsMode::None {
455-
let type_name = value
456-
.get_type()
457-
.qualname()
458-
.unwrap_or_else(|_| PyString::new(value.py(), "<unknown python object>"));
459-
460-
let value_str = truncate_safe_repr(value, None);
461-
462-
self.add_warning(format!(
463-
"Expected `{field_type}` but got `{type_name}` with value `{value_str}` - serialized value may not be as expected"
450+
self.register_warning(PydanticSerializationUnexpectedValue::new_from_parts(
451+
Some(field_type.to_string()),
452+
Some(value.clone().unbind()),
464453
));
465454
}
466455
}
467456

468-
fn add_warning(&self, message: String) {
469-
self.warnings.lock().expect("lock poisoned").push(message);
470-
}
471-
472457
pub fn final_check(&self, py: Python) -> PyResult<()> {
473458
if self.mode == WarningsMode::None {
474459
return Ok(());
@@ -479,7 +464,9 @@ impl CollectWarnings {
479464
return Ok(());
480465
}
481466

482-
let message = format!("Pydantic serializer warnings:\n {}", warnings.join("\n "));
467+
let formatted_warnings: Vec<String> = warnings.iter().map(|w| w.__repr__(py).to_string()).collect();
468+
469+
let message = format!("Pydantic serializer warnings:\n {}", formatted_warnings.join("\n "));
483470
if self.mode == WarningsMode::Warn {
484471
let user_warning_type = PyUserWarning::type_object(py);
485472
PyErr::warn(py, &user_warning_type, &CString::new(message)?, 0)

src/serializers/fields.rs

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use smallvec::SmallVec;
99

1010
use crate::serializers::extra::SerCheck;
1111
use crate::serializers::DuckTypingSerMode;
12-
use crate::tools::truncate_safe_repr;
1312
use crate::PydanticSerializationUnexpectedValue;
1413

1514
use super::computed_fields::ComputedFields;
@@ -201,15 +200,12 @@ impl GeneralFieldsSerializer {
201200
};
202201
output_dict.set_item(key, value)?;
203202
} else if field_extra.check == SerCheck::Strict {
204-
let type_name = field_extra.model_type_name();
205-
return Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
206-
"Unexpected field `{key}`{for_type_name}",
207-
for_type_name = if let Some(type_name) = type_name {
208-
format!(" for type `{type_name}`")
209-
} else {
210-
String::new()
211-
},
212-
))));
203+
return Err(PydanticSerializationUnexpectedValue::new(
204+
Some(format!("Unexpected field `{key}`")),
205+
field_extra.model_type_name().map(|bound| bound.to_string()),
206+
None,
207+
)
208+
.to_py_err());
213209
}
214210
}
215211
}
@@ -221,16 +217,13 @@ impl GeneralFieldsSerializer {
221217
&& self.required_fields > used_req_fields
222218
{
223219
let required_fields = self.required_fields;
224-
let type_name = extra.model_type_name();
225-
let field_value = match extra.model {
226-
Some(model) => truncate_safe_repr(model, Some(100)),
227-
None => "<unknown python object>".to_string(),
228-
};
229220

230-
Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
231-
"Expected {required_fields} fields but got {used_req_fields}{for_type_name} with value `{field_value}` - serialized value may not be as expected.",
232-
for_type_name = if let Some(type_name) = type_name { format!(" for type `{type_name}`") } else { String::new() },
233-
))))
221+
Err(PydanticSerializationUnexpectedValue::new(
222+
Some(format!("Expected {required_fields} fields but got {used_req_fields}").to_string()),
223+
extra.model_type_name().map(|bound| bound.to_string()),
224+
extra.model.map(|bound| bound.clone().unbind()),
225+
)
226+
.to_py_err())
234227
} else {
235228
Ok(output_dict)
236229
}

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).to_py_err())
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).to_py_err()),
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.register_warning(ser_err);
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).to_py_err()
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).to_py_err()),
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: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,19 +214,22 @@ 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
221-
))));
221+
)))
222+
.to_py_err());
222223
} else {
223224
use_serializers!(self.serializers.iter());
224225
let mut warned = false;
225226
for (i, element) in py_tuple_iter.enumerate() {
226227
if !warned {
227228
extra
228229
.warnings
229-
.custom_warning("Unexpected extra items present in tuple".to_string());
230+
.register_warning(PydanticSerializationUnexpectedValue::new_from_msg(Some(
231+
"Unexpected extra items present in tuple".to_string(),
232+
)));
230233
warned = true;
231234
}
232235
let op_next = self

0 commit comments

Comments
 (0)