Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show value of wrongly typed in serialization warning #1377

Merged
merged 5 commits into from
Aug 8, 2024
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
46 changes: 46 additions & 0 deletions src/errors/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use core::fmt;
use std::borrow::Cow;

use pyo3::prelude::*;

mod line_error;
Expand Down Expand Up @@ -30,3 +33,46 @@ pub fn py_err_string(py: Python, err: PyErr) -> String {
Err(_) => "Unknown Error".to_string(),
}
}

// TODO: is_utf8_char_boundary, floor_char_boundary and ceil_char_boundary
// with builtin methods once https://github.com/rust-lang/rust/issues/93743 is resolved
// These are just copy pasted from the current implementation
const fn is_utf8_char_boundary(value: u8) -> bool {
// This is bit magic equivalent to: b < 128 || b >= 192
(value as i8) >= -0x40
}

pub fn floor_char_boundary(value: &str, index: usize) -> usize {
if index >= value.len() {
value.len()
} else {
let lower_bound = index.saturating_sub(3);
let new_index = value.as_bytes()[lower_bound..=index]
.iter()
.rposition(|b| is_utf8_char_boundary(*b));

// SAFETY: we know that the character boundary will be within four bytes
unsafe { lower_bound + new_index.unwrap_unchecked() }
}
}

pub fn ceil_char_boundary(value: &str, index: usize) -> usize {
let upper_bound = Ord::min(index + 4, value.len());
value.as_bytes()[index..upper_bound]
.iter()
.position(|b| is_utf8_char_boundary(*b))
.map_or(upper_bound, |pos| pos + index)
}

pub fn write_truncated_to_50_bytes<F: fmt::Write>(f: &mut F, val: Cow<'_, str>) -> std::fmt::Result {
if val.len() > 50 {
write!(
f,
"{}...{}",
&val[0..floor_char_boundary(&val, 25)],
&val[ceil_char_boundary(&val, val.len() - 24)..]
)
} else {
write!(f, "{val}")
}
}
48 changes: 2 additions & 46 deletions src/errors/validation_exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,51 +386,6 @@ impl ValidationError {
}
}

// TODO: is_utf8_char_boundary, floor_char_boundary and ceil_char_boundary
// with builtin methods once https://github.com/rust-lang/rust/issues/93743 is resolved
// These are just copy pasted from the current implementation
const fn is_utf8_char_boundary(value: u8) -> bool {
// This is bit magic equivalent to: b < 128 || b >= 192
(value as i8) >= -0x40
}

fn floor_char_boundary(value: &str, index: usize) -> usize {
if index >= value.len() {
value.len()
} else {
let lower_bound = index.saturating_sub(3);
let new_index = value.as_bytes()[lower_bound..=index]
.iter()
.rposition(|b| is_utf8_char_boundary(*b));

// SAFETY: we know that the character boundary will be within four bytes
unsafe { lower_bound + new_index.unwrap_unchecked() }
}
}

pub fn ceil_char_boundary(value: &str, index: usize) -> usize {
let upper_bound = Ord::min(index + 4, value.len());
value.as_bytes()[index..upper_bound]
.iter()
.position(|b| is_utf8_char_boundary(*b))
.map_or(upper_bound, |pos| pos + index)
}

macro_rules! truncate_input_value {
($out:expr, $value:expr) => {
if $value.len() > 50 {
write!(
$out,
", input_value={}...{}",
&$value[0..floor_char_boundary($value, 25)],
&$value[ceil_char_boundary($value, $value.len() - 24)..]
)?;
} else {
write!($out, ", input_value={}", $value)?;
}
};
}

pub fn pretty_py_line_errors<'a>(
py: Python,
input_type: InputType,
Expand Down Expand Up @@ -570,7 +525,8 @@ impl PyLineError {
if !hide_input {
let input_value = self.input_value.bind(py);
let input_str = safe_repr(input_value);
truncate_input_value!(output, &input_str.to_cow());
write!(output, ", input_value=")?;
super::write_truncated_to_50_bytes(&mut output, input_str.to_cow())?;

if let Ok(type_) = input_value.get_type().qualname() {
write!(output, ", input_type={type_}")?;
Expand Down
11 changes: 10 additions & 1 deletion src/serializers/extra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::recursion_guard::ContainsRecursionState;
use crate::recursion_guard::RecursionError;
use crate::recursion_guard::RecursionGuard;
use crate::recursion_guard::RecursionState;
use crate::tools::safe_repr;
use crate::PydanticSerializationError;

/// this is ugly, would be much better if extra could be stored in `SerializationState`
Expand Down Expand Up @@ -424,8 +425,16 @@ impl CollectWarnings {
.get_type()
.qualname()
.unwrap_or_else(|_| PyString::new_bound(value.py(), "<unknown python object>"));

let input_str = safe_repr(value);
let mut value_str = String::with_capacity(100);
value_str.push_str("with value `");
crate::errors::write_truncated_to_50_bytes(&mut value_str, input_str.to_cow())
.expect("Writing to a `String` failed");
value_str.push('`');

self.add_warning(format!(
"Expected `{field_type}` but got `{type_name}` - serialized value may not be as expected"
"Expected `{field_type}` but got `{type_name}` {value_str} - serialized value may not be as expected"
));
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/serializers/test_any.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def test_any_with_date_serializer():
assert s.to_python(b'bang', mode='json') == 'bang'

assert [w.message.args[0] for w in warning_info.list] == [
'Pydantic serializer warnings:\n Expected `date` but got `bytes` - serialized value may not be as expected'
"Pydantic serializer warnings:\n Expected `date` but got `bytes` with value `b'bang'` - serialized value may not be as expected"
]


Expand All @@ -172,7 +172,7 @@ def test_any_with_timedelta_serializer():
assert s.to_python(b'bang', mode='json') == 'bang'

assert [w.message.args[0] for w in warning_info.list] == [
'Pydantic serializer warnings:\n Expected `timedelta` but got `bytes` - '
"Pydantic serializer warnings:\n Expected `timedelta` but got `bytes` with value `b'bang'` - "
'serialized value may not be as expected'
]

Expand Down
16 changes: 12 additions & 4 deletions tests/serializers/test_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,21 @@ def test_bytes_dict_key():

def test_bytes_fallback():
s = SchemaSerializer(core_schema.bytes_schema())
with pytest.warns(UserWarning, match='Expected `bytes` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `bytes` but got `int` with value `123` - serialized value may not be as expected'
):
assert s.to_python(123) == 123
with pytest.warns(UserWarning, match='Expected `bytes` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `bytes` but got `int` with value `123` - serialized value may not be as expected'
):
assert s.to_python(123, mode='json') == 123
with pytest.warns(UserWarning, match='Expected `bytes` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `bytes` but got `int` with value `123` - serialized value may not be as expected'
):
assert s.to_json(123) == b'123'
with pytest.warns(UserWarning, match='Expected `bytes` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `bytes` but got `str` with value `'foo'` - serialized value may not be as expected"
):
assert s.to_json('foo') == b'"foo"'


Expand Down
10 changes: 8 additions & 2 deletions tests/serializers/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ def test_datetime():
assert v.to_python(datetime(2022, 12, 2, 12, 13, 14), mode='json') == '2022-12-02T12:13:14'
assert v.to_json(datetime(2022, 12, 2, 12, 13, 14)) == b'"2022-12-02T12:13:14"'

with pytest.warns(UserWarning, match='Expected `datetime` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning,
match='Expected `datetime` but got `int` with value `123` - serialized value may not be as expected',
):
assert v.to_python(123, mode='json') == 123

with pytest.warns(UserWarning, match='Expected `datetime` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning,
match='Expected `datetime` but got `int` with value `123` - serialized value may not be as expected',
):
assert v.to_json(123) == b'123'


Expand Down
8 changes: 6 additions & 2 deletions tests/serializers/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ def test_decimal():
== b'"123456789123456789123456789.123456789123456789123456789"'
)

with pytest.warns(UserWarning, match='Expected `decimal` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `decimal` but got `int` with value `123` - serialized value may not be as expected'
):
assert v.to_python(123, mode='json') == 123

with pytest.warns(UserWarning, match='Expected `decimal` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `decimal` but got `int` with value `123` - serialized value may not be as expected'
):
assert v.to_json(123) == b'123'


Expand Down
40 changes: 30 additions & 10 deletions tests/serializers/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ class MyEnum(Enum):
assert v.to_python(MyEnum.a, mode='json') == 1
assert v.to_json(MyEnum.a) == b'1'

with pytest.warns(UserWarning, match='Expected `enum` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `enum` but got `int` with value `1` - serialized value may not be as expected'
):
assert v.to_python(1) == 1
with pytest.warns(UserWarning, match='Expected `enum` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `enum` but got `int` with value `1` - serialized value may not be as expected'
):
assert v.to_json(1) == b'1'


Expand All @@ -35,9 +39,13 @@ class MyEnum(int, Enum):
assert v.to_python(MyEnum.a, mode='json') == 1
assert v.to_json(MyEnum.a) == b'1'

with pytest.warns(UserWarning, match='Expected `enum` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `enum` but got `int` with value `1` - serialized value may not be as expected'
):
assert v.to_python(1) == 1
with pytest.warns(UserWarning, match='Expected `enum` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `enum` but got `int` with value `1` - serialized value may not be as expected'
):
assert v.to_json(1) == b'1'


Expand All @@ -53,9 +61,13 @@ class MyEnum(str, Enum):
assert v.to_python(MyEnum.a, mode='json') == 'a'
assert v.to_json(MyEnum.a) == b'"a"'

with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `enum` but got `str` with value `'a'` - serialized value may not be as expected"
):
assert v.to_python('a') == 'a'
with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `enum` but got `str` with value `'a'` - serialized value may not be as expected"
):
assert v.to_json('a') == b'"a"'


Expand All @@ -76,9 +88,13 @@ class MyEnum(Enum):
assert v.to_python({MyEnum.a: 'x'}, mode='json') == {'1': 'x'}
assert v.to_json({MyEnum.a: 'x'}) == b'{"1":"x"}'

with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `enum` but got `str` with value `'x'` - serialized value may not be as expected"
):
assert v.to_python({'x': 'x'}) == {'x': 'x'}
with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `enum` but got `str` with value `'x'` - serialized value may not be as expected"
):
assert v.to_json({'x': 'x'}) == b'{"x":"x"}'


Expand All @@ -99,7 +115,11 @@ class MyEnum(int, Enum):
assert v.to_python({MyEnum.a: 'x'}, mode='json') == {'1': 'x'}
assert v.to_json({MyEnum.a: 'x'}) == b'{"1":"x"}'

with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `enum` but got `str` with value `'x'` - serialized value may not be as expected"
):
assert v.to_python({'x': 'x'}) == {'x': 'x'}
with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `enum` but got `str` with value `'x'` - serialized value may not be as expected"
):
assert v.to_json({'x': 'x'}) == b'{"x":"x"}'
34 changes: 25 additions & 9 deletions tests/serializers/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def append_42(value, _info):
assert s.to_python([1, 2, 3], mode='json') == [1, 2, 3, 42]
assert s.to_json([1, 2, 3]) == b'[1,2,3,42]'

msg = r'Expected `list\[int\]` but got `str` - serialized value may not be as expected'
msg = r"Expected `list\[int\]` but got `str` with value `'abc'` - serialized value may not be as expected"
with pytest.warns(UserWarning, match=msg):
assert s.to_python('abc') == 'abc'

Expand Down Expand Up @@ -322,11 +322,17 @@ def test_wrong_return_type():
)
)
)
with pytest.warns(UserWarning, match='Expected `int` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `int` but got `str` with value `'123'` - serialized value may not be as expected"
):
assert s.to_python(123) == '123'
with pytest.warns(UserWarning, match='Expected `int` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `int` but got `str` with value `'123'` - serialized value may not be as expected"
):
assert s.to_python(123, mode='json') == '123'
with pytest.warns(UserWarning, match='Expected `int` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `int` but got `str` with value `'123'` - serialized value may not be as expected"
):
assert s.to_json(123) == b'"123"'


Expand Down Expand Up @@ -356,11 +362,17 @@ def f(value, serializer):
assert s.to_python(3) == 'result=3'
assert s.to_python(3, mode='json') == 'result=3'
assert s.to_json(3) == b'"result=3"'
with pytest.warns(UserWarning, match='Expected `str` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `str` but got `int` with value `42` - serialized value may not be as expected'
):
assert s.to_python(42) == 42
with pytest.warns(UserWarning, match='Expected `str` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `str` but got `int` with value `42` - serialized value may not be as expected'
):
assert s.to_python(42, mode='json') == 42
with pytest.warns(UserWarning, match='Expected `str` but got `int` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match='Expected `str` but got `int` with value `42` - serialized value may not be as expected'
):
assert s.to_json(42) == b'42'


Expand Down Expand Up @@ -611,7 +623,9 @@ def f(value, _info):
return value

s = SchemaSerializer(core_schema.with_info_after_validator_function(f, core_schema.int_schema()))
with pytest.warns(UserWarning, match='Expected `int` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `int` but got `str` with value `'abc'` - serialized value may not be as expected"
):
assert s.to_python('abc') == 'abc'


Expand All @@ -620,7 +634,9 @@ def f(value, handler, _info):
return handler(value)

s = SchemaSerializer(core_schema.with_info_wrap_validator_function(f, core_schema.int_schema()))
with pytest.warns(UserWarning, match='Expected `int` but got `str` - serialized value may not be as expected'):
with pytest.warns(
UserWarning, match="Expected `int` but got `str` with value `'abc'` - serialized value may not be as expected"
):
assert s.to_python('abc') == 'abc'


Expand Down
Loading
Loading