Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 76 additions & 13 deletions src/serializers/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ use std::fmt;

use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyString;

use crate::tools::truncate_safe_repr;

use serde::ser;

Expand Down Expand Up @@ -44,9 +47,9 @@ pub(super) fn se_err_py_err(error: PythonSerializerError) -> PyErr {
let s = error.to_string();
if let Some(msg) = s.strip_prefix(UNEXPECTED_TYPE_SER_MARKER) {
if msg.is_empty() {
PydanticSerializationUnexpectedValue::new_err(None)
PydanticSerializationUnexpectedValue::new_from_msg(None).to_py_err()
} else {
PydanticSerializationUnexpectedValue::new_err(Some(msg.to_string()))
PydanticSerializationUnexpectedValue::new_from_msg(Some(msg.to_string())).to_py_err()
}
} else if let Some(msg) = s.strip_prefix(SERIALIZATION_ERR_MARKER) {
PydanticSerializationError::new_err(msg.to_string())
Expand Down Expand Up @@ -94,30 +97,90 @@ impl PydanticSerializationError {
#[derive(Debug, Clone)]
pub struct PydanticSerializationUnexpectedValue {
message: Option<String>,
field_type: Option<String>,
input_value: Option<PyObject>,
}

impl PydanticSerializationUnexpectedValue {
pub(crate) fn new_err(msg: Option<String>) -> PyErr {
PyErr::new::<Self, Option<String>>(msg)
pub fn new_from_msg(message: Option<String>) -> Self {
Self {
message,
field_type: None,
input_value: None,
}
}

pub fn new_from_parts(field_type: Option<String>, input_value: Option<PyObject>) -> Self {
Self {
message: None,
field_type,
input_value,
}
}

pub fn new(message: Option<String>, field_type: Option<String>, input_value: Option<PyObject>) -> Self {
Self {
message,
field_type,
input_value,
}
}

pub fn to_py_err(&self) -> PyErr {
PyErr::new::<Self, (Option<String>, Option<String>, Option<PyObject>)>((
self.message.clone(),
self.field_type.clone(),
self.input_value.clone(),
))
}
}

#[pymethods]
impl PydanticSerializationUnexpectedValue {
#[new]
#[pyo3(signature = (message=None))]
fn py_new(message: Option<String>) -> Self {
Self { message }
#[pyo3(signature = (message=None, field_type=None, input_value=None))]
fn py_new(message: Option<String>, field_type: Option<String>, input_value: Option<PyObject>) -> Self {
Self {
message,
field_type,
input_value,
}
}

fn __str__(&self) -> &str {
match self.message {
Some(ref s) => s,
None => "Unexpected Value",
pub(crate) fn __str__(&self, py: Python) -> String {
let mut message = self.message.as_deref().unwrap_or("").to_string();

if let Some(field_type) = &self.field_type {
if !message.is_empty() {
message.push_str(": ");
}
message.push_str(&format!("Expected `{field_type}`"));
if self.input_value.is_some() {
message.push_str(" - serialized value may not be as expected");
}
}

if let Some(input_value) = &self.input_value {
let bound_input = input_value.bind(py);
let input_type = bound_input
.get_type()
.name()
.unwrap_or_else(|_| PyString::new(py, "<unknown python object>"))
.to_string();

let value_str = truncate_safe_repr(bound_input, None);

message.push_str(&format!(" [input_value={value_str}, input_type={input_type}]"));
}

if message.is_empty() {
message = "Unexpected Value".to_string();
}

message
}

pub(crate) fn __repr__(&self) -> String {
format!("PydanticSerializationUnexpectedValue({})", self.__str__())
pub(crate) fn __repr__(&self, py: Python) -> String {
format!("PydanticSerializationUnexpectedValue({})", self.__str__(py))
}
}
41 changes: 14 additions & 27 deletions src/serializers/extra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use crate::recursion_guard::ContainsRecursionState;
use crate::recursion_guard::RecursionError;
use crate::recursion_guard::RecursionGuard;
use crate::recursion_guard::RecursionState;
use crate::tools::truncate_safe_repr;
use crate::PydanticSerializationError;

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

impl Clone for CollectWarnings {
Expand All @@ -404,9 +403,9 @@ impl CollectWarnings {
}
}

pub fn custom_warning(&self, warning: String) {
pub fn register_warning(&self, warning: PydanticSerializationUnexpectedValue) {
if self.mode != WarningsMode::None {
self.add_warning(warning);
self.warnings.lock().expect("lock poisoned").push(warning);
}
}

Expand All @@ -415,15 +414,11 @@ impl CollectWarnings {
if value.is_none() {
Ok(())
} else if extra.check.enabled() {
let type_name = value
.get_type()
.qualname()
.unwrap_or_else(|_| PyString::new(value.py(), "<unknown python object>"));

let value_str = truncate_safe_repr(value, None);
Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
"Expected `{field_type}` but got `{type_name}` with value `{value_str}` - serialized value may not be as expected"
))))
Err(PydanticSerializationUnexpectedValue::new_from_parts(
Some(field_type.to_string()),
Some(value.clone().unbind()),
)
.to_py_err())
} else {
self.fallback_warning(field_type, value);
Ok(())
Expand Down Expand Up @@ -452,23 +447,13 @@ impl CollectWarnings {

fn fallback_warning(&self, field_type: &str, value: &Bound<'_, PyAny>) {
if self.mode != WarningsMode::None {
let type_name = value
.get_type()
.qualname()
.unwrap_or_else(|_| PyString::new(value.py(), "<unknown python object>"));

let value_str = truncate_safe_repr(value, None);

self.add_warning(format!(
"Expected `{field_type}` but got `{type_name}` with value `{value_str}` - serialized value may not be as expected"
self.register_warning(PydanticSerializationUnexpectedValue::new_from_parts(
Some(field_type.to_string()),
Some(value.clone().unbind()),
));
}
}

fn add_warning(&self, message: String) {
self.warnings.lock().expect("lock poisoned").push(message);
}

pub fn final_check(&self, py: Python) -> PyResult<()> {
if self.mode == WarningsMode::None {
return Ok(());
Expand All @@ -479,7 +464,9 @@ impl CollectWarnings {
return Ok(());
}

let message = format!("Pydantic serializer warnings:\n {}", warnings.join("\n "));
let formatted_warnings: Vec<String> = warnings.iter().map(|w| w.__repr__(py).to_string()).collect();

let message = format!("Pydantic serializer warnings:\n {}", formatted_warnings.join("\n "));
if self.mode == WarningsMode::Warn {
let user_warning_type = PyUserWarning::type_object(py);
PyErr::warn(py, &user_warning_type, &CString::new(message)?, 0)
Expand Down
31 changes: 12 additions & 19 deletions src/serializers/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use smallvec::SmallVec;

use crate::serializers::extra::SerCheck;
use crate::serializers::DuckTypingSerMode;
use crate::tools::truncate_safe_repr;
use crate::PydanticSerializationUnexpectedValue;

use super::computed_fields::ComputedFields;
Expand Down Expand Up @@ -201,15 +200,12 @@ impl GeneralFieldsSerializer {
};
output_dict.set_item(key, value)?;
} else if field_extra.check == SerCheck::Strict {
let type_name = field_extra.model_type_name();
return Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
"Unexpected field `{key}`{for_type_name}",
for_type_name = if let Some(type_name) = type_name {
format!(" for type `{type_name}`")
} else {
String::new()
},
))));
return Err(PydanticSerializationUnexpectedValue::new(
Some(format!("Unexpected field `{key}`")),
field_extra.model_type_name().map(|bound| bound.to_string()),
None,
)
.to_py_err());
}
}
}
Expand All @@ -221,16 +217,13 @@ impl GeneralFieldsSerializer {
&& self.required_fields > used_req_fields
{
let required_fields = self.required_fields;
let type_name = extra.model_type_name();
let field_value = match extra.model {
Some(model) => truncate_safe_repr(model, Some(100)),
None => "<unknown python object>".to_string(),
};

Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
"Expected {required_fields} fields but got {used_req_fields}{for_type_name} with value `{field_value}` - serialized value may not be as expected.",
for_type_name = if let Some(type_name) = type_name { format!(" for type `{type_name}`") } else { String::new() },
))))
Err(PydanticSerializationUnexpectedValue::new(
Some(format!("Expected {required_fields} fields but got {used_req_fields}").to_string()),
extra.model_type_name().map(|bound| bound.to_string()),
extra.model.map(|bound| bound.clone().unbind()),
)
.to_py_err())
} else {
Ok(output_dict)
}
Expand Down
2 changes: 1 addition & 1 deletion src/serializers/type_serializers/datetime_etc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fn downcast_date_reject_datetime<'a, 'py>(py_date: &'a Bound<'py, PyAny>) -> PyR
}
}

Err(PydanticSerializationUnexpectedValue::new_err(None))
Err(PydanticSerializationUnexpectedValue::new_from_msg(None).to_py_err())
}

macro_rules! build_serializer {
Expand Down
2 changes: 1 addition & 1 deletion src/serializers/type_serializers/float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl TypeSerializer for FloatSerializer {
match extra.ob_type_lookup.is_type(value, ObType::Float) {
IsType::Exact => Ok(value.clone().unbind()),
IsType::Subclass => match extra.check {
SerCheck::Strict => Err(PydanticSerializationUnexpectedValue::new_err(None)),
SerCheck::Strict => Err(PydanticSerializationUnexpectedValue::new_from_msg(None).to_py_err()),
SerCheck::Lax | SerCheck::None => match extra.mode {
SerMode::Json => value.extract::<f64>()?.into_py_any(py),
_ => infer_to_python(value, include, exclude, extra),
Expand Down
2 changes: 1 addition & 1 deletion src/serializers/type_serializers/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ fn on_error(py: Python, err: PyErr, function_name: &str, extra: &Extra) -> PyRes
if extra.check.enabled() {
Err(err)
} else {
extra.warnings.custom_warning(ser_err.__repr__());
extra.warnings.register_warning(ser_err);
Ok(())
}
} else if let Ok(err) = exception.extract::<PydanticSerializationError>() {
Expand Down
2 changes: 1 addition & 1 deletion src/serializers/type_serializers/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ impl TypeSerializer for ModelSerializer {
let py = value.py();
let root = value.getattr(intern!(py, ROOT_FIELD)).map_err(|original_err| {
if root_extra.check.enabled() {
PydanticSerializationUnexpectedValue::new_err(None)
PydanticSerializationUnexpectedValue::new_from_msg(None).to_py_err()
} else {
original_err
}
Expand Down
2 changes: 1 addition & 1 deletion src/serializers/type_serializers/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ macro_rules! build_simple_serializer {
match extra.ob_type_lookup.is_type(value, $ob_type) {
IsType::Exact => Ok(value.clone().unbind()),
IsType::Subclass => match extra.check {
SerCheck::Strict => Err(PydanticSerializationUnexpectedValue::new_err(None)),
SerCheck::Strict => Err(PydanticSerializationUnexpectedValue::new_from_msg(None).to_py_err()),
SerCheck::Lax | SerCheck::None => match extra.mode {
SerMode::Json => value.extract::<$rust_type>()?.into_py_any(py),
_ => infer_to_python(value, include, exclude, extra),
Expand Down
9 changes: 6 additions & 3 deletions src/serializers/type_serializers/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,19 +214,22 @@ impl TupleSerializer {
.chain(self.serializers[variadic_item_index + 1..].iter());
use_serializers!(serializers_iter);
} else if extra.check == SerCheck::Strict && n_items != self.serializers.len() {
return Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
return Err(PydanticSerializationUnexpectedValue::new_from_msg(Some(format!(
"Expected {} items, but got {}",
self.serializers.len(),
n_items
))));
)))
.to_py_err());
} else {
use_serializers!(self.serializers.iter());
let mut warned = false;
for (i, element) in py_tuple_iter.enumerate() {
if !warned {
extra
.warnings
.custom_warning("Unexpected extra items present in tuple".to_string());
.register_warning(PydanticSerializationUnexpectedValue::new_from_msg(Some(
"Unexpected extra items present in tuple".to_string(),
)));
warned = true;
}
let op_next = self
Expand Down
Loading
Loading