Skip to content

Commit

Permalink
Show value of wrongly typed in serialization warning (#1377)
Browse files Browse the repository at this point in the history
  • Loading branch information
BoxyUwU committed Aug 8, 2024
1 parent 585f725 commit 863640b
Show file tree
Hide file tree
Showing 21 changed files with 259 additions and 130 deletions.
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

0 comments on commit 863640b

Please sign in to comment.