Skip to content

Commit 863640b

Browse files
authored
Show value of wrongly typed in serialization warning (#1377)
1 parent 585f725 commit 863640b

21 files changed

+259
-130
lines changed

src/errors/mod.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use core::fmt;
2+
use std::borrow::Cow;
3+
14
use pyo3::prelude::*;
25

36
mod line_error;
@@ -30,3 +33,46 @@ pub fn py_err_string(py: Python, err: PyErr) -> String {
3033
Err(_) => "Unknown Error".to_string(),
3134
}
3235
}
36+
37+
// TODO: is_utf8_char_boundary, floor_char_boundary and ceil_char_boundary
38+
// with builtin methods once https://github.com/rust-lang/rust/issues/93743 is resolved
39+
// These are just copy pasted from the current implementation
40+
const fn is_utf8_char_boundary(value: u8) -> bool {
41+
// This is bit magic equivalent to: b < 128 || b >= 192
42+
(value as i8) >= -0x40
43+
}
44+
45+
pub fn floor_char_boundary(value: &str, index: usize) -> usize {
46+
if index >= value.len() {
47+
value.len()
48+
} else {
49+
let lower_bound = index.saturating_sub(3);
50+
let new_index = value.as_bytes()[lower_bound..=index]
51+
.iter()
52+
.rposition(|b| is_utf8_char_boundary(*b));
53+
54+
// SAFETY: we know that the character boundary will be within four bytes
55+
unsafe { lower_bound + new_index.unwrap_unchecked() }
56+
}
57+
}
58+
59+
pub fn ceil_char_boundary(value: &str, index: usize) -> usize {
60+
let upper_bound = Ord::min(index + 4, value.len());
61+
value.as_bytes()[index..upper_bound]
62+
.iter()
63+
.position(|b| is_utf8_char_boundary(*b))
64+
.map_or(upper_bound, |pos| pos + index)
65+
}
66+
67+
pub fn write_truncated_to_50_bytes<F: fmt::Write>(f: &mut F, val: Cow<'_, str>) -> std::fmt::Result {
68+
if val.len() > 50 {
69+
write!(
70+
f,
71+
"{}...{}",
72+
&val[0..floor_char_boundary(&val, 25)],
73+
&val[ceil_char_boundary(&val, val.len() - 24)..]
74+
)
75+
} else {
76+
write!(f, "{val}")
77+
}
78+
}

src/errors/validation_exception.rs

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -386,51 +386,6 @@ impl ValidationError {
386386
}
387387
}
388388

389-
// TODO: is_utf8_char_boundary, floor_char_boundary and ceil_char_boundary
390-
// with builtin methods once https://github.com/rust-lang/rust/issues/93743 is resolved
391-
// These are just copy pasted from the current implementation
392-
const fn is_utf8_char_boundary(value: u8) -> bool {
393-
// This is bit magic equivalent to: b < 128 || b >= 192
394-
(value as i8) >= -0x40
395-
}
396-
397-
fn floor_char_boundary(value: &str, index: usize) -> usize {
398-
if index >= value.len() {
399-
value.len()
400-
} else {
401-
let lower_bound = index.saturating_sub(3);
402-
let new_index = value.as_bytes()[lower_bound..=index]
403-
.iter()
404-
.rposition(|b| is_utf8_char_boundary(*b));
405-
406-
// SAFETY: we know that the character boundary will be within four bytes
407-
unsafe { lower_bound + new_index.unwrap_unchecked() }
408-
}
409-
}
410-
411-
pub fn ceil_char_boundary(value: &str, index: usize) -> usize {
412-
let upper_bound = Ord::min(index + 4, value.len());
413-
value.as_bytes()[index..upper_bound]
414-
.iter()
415-
.position(|b| is_utf8_char_boundary(*b))
416-
.map_or(upper_bound, |pos| pos + index)
417-
}
418-
419-
macro_rules! truncate_input_value {
420-
($out:expr, $value:expr) => {
421-
if $value.len() > 50 {
422-
write!(
423-
$out,
424-
", input_value={}...{}",
425-
&$value[0..floor_char_boundary($value, 25)],
426-
&$value[ceil_char_boundary($value, $value.len() - 24)..]
427-
)?;
428-
} else {
429-
write!($out, ", input_value={}", $value)?;
430-
}
431-
};
432-
}
433-
434389
pub fn pretty_py_line_errors<'a>(
435390
py: Python,
436391
input_type: InputType,
@@ -570,7 +525,8 @@ impl PyLineError {
570525
if !hide_input {
571526
let input_value = self.input_value.bind(py);
572527
let input_str = safe_repr(input_value);
573-
truncate_input_value!(output, &input_str.to_cow());
528+
write!(output, ", input_value=")?;
529+
super::write_truncated_to_50_bytes(&mut output, input_str.to_cow())?;
574530

575531
if let Ok(type_) = input_value.get_type().qualname() {
576532
write!(output, ", input_type={type_}")?;

src/serializers/extra.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::recursion_guard::ContainsRecursionState;
1515
use crate::recursion_guard::RecursionError;
1616
use crate::recursion_guard::RecursionGuard;
1717
use crate::recursion_guard::RecursionState;
18+
use crate::tools::safe_repr;
1819
use crate::PydanticSerializationError;
1920

2021
/// this is ugly, would be much better if extra could be stored in `SerializationState`
@@ -424,8 +425,16 @@ impl CollectWarnings {
424425
.get_type()
425426
.qualname()
426427
.unwrap_or_else(|_| PyString::new_bound(value.py(), "<unknown python object>"));
428+
429+
let input_str = safe_repr(value);
430+
let mut value_str = String::with_capacity(100);
431+
value_str.push_str("with value `");
432+
crate::errors::write_truncated_to_50_bytes(&mut value_str, input_str.to_cow())
433+
.expect("Writing to a `String` failed");
434+
value_str.push('`');
435+
427436
self.add_warning(format!(
428-
"Expected `{field_type}` but got `{type_name}` - serialized value may not be as expected"
437+
"Expected `{field_type}` but got `{type_name}` {value_str} - serialized value may not be as expected"
429438
));
430439
}
431440
}

tests/serializers/test_any.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_any_with_date_serializer():
158158
assert s.to_python(b'bang', mode='json') == 'bang'
159159

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

164164

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

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

tests/serializers/test_bytes.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,21 @@ def test_bytes_dict_key():
4646

4747
def test_bytes_fallback():
4848
s = SchemaSerializer(core_schema.bytes_schema())
49-
with pytest.warns(UserWarning, match='Expected `bytes` but got `int` - serialized value may not be as expected'):
49+
with pytest.warns(
50+
UserWarning, match='Expected `bytes` but got `int` with value `123` - serialized value may not be as expected'
51+
):
5052
assert s.to_python(123) == 123
51-
with pytest.warns(UserWarning, match='Expected `bytes` but got `int` - serialized value may not be as expected'):
53+
with pytest.warns(
54+
UserWarning, match='Expected `bytes` but got `int` with value `123` - serialized value may not be as expected'
55+
):
5256
assert s.to_python(123, mode='json') == 123
53-
with pytest.warns(UserWarning, match='Expected `bytes` but got `int` - serialized value may not be as expected'):
57+
with pytest.warns(
58+
UserWarning, match='Expected `bytes` but got `int` with value `123` - serialized value may not be as expected'
59+
):
5460
assert s.to_json(123) == b'123'
55-
with pytest.warns(UserWarning, match='Expected `bytes` but got `str` - serialized value may not be as expected'):
61+
with pytest.warns(
62+
UserWarning, match="Expected `bytes` but got `str` with value `'foo'` - serialized value may not be as expected"
63+
):
5664
assert s.to_json('foo') == b'"foo"'
5765

5866

tests/serializers/test_datetime.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@ def test_datetime():
1212
assert v.to_python(datetime(2022, 12, 2, 12, 13, 14), mode='json') == '2022-12-02T12:13:14'
1313
assert v.to_json(datetime(2022, 12, 2, 12, 13, 14)) == b'"2022-12-02T12:13:14"'
1414

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

18-
with pytest.warns(UserWarning, match='Expected `datetime` but got `int` - serialized value may not be as expected'):
21+
with pytest.warns(
22+
UserWarning,
23+
match='Expected `datetime` but got `int` with value `123` - serialized value may not be as expected',
24+
):
1925
assert v.to_json(123) == b'123'
2026

2127

tests/serializers/test_decimal.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ def test_decimal():
2020
== b'"123456789123456789123456789.123456789123456789123456789"'
2121
)
2222

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

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

2933

tests/serializers/test_enum.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ class MyEnum(Enum):
1717
assert v.to_python(MyEnum.a, mode='json') == 1
1818
assert v.to_json(MyEnum.a) == b'1'
1919

20-
with pytest.warns(UserWarning, match='Expected `enum` but got `int` - serialized value may not be as expected'):
20+
with pytest.warns(
21+
UserWarning, match='Expected `enum` but got `int` with value `1` - serialized value may not be as expected'
22+
):
2123
assert v.to_python(1) == 1
22-
with pytest.warns(UserWarning, match='Expected `enum` but got `int` - serialized value may not be as expected'):
24+
with pytest.warns(
25+
UserWarning, match='Expected `enum` but got `int` with value `1` - serialized value may not be as expected'
26+
):
2327
assert v.to_json(1) == b'1'
2428

2529

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

38-
with pytest.warns(UserWarning, match='Expected `enum` but got `int` - serialized value may not be as expected'):
42+
with pytest.warns(
43+
UserWarning, match='Expected `enum` but got `int` with value `1` - serialized value may not be as expected'
44+
):
3945
assert v.to_python(1) == 1
40-
with pytest.warns(UserWarning, match='Expected `enum` but got `int` - serialized value may not be as expected'):
46+
with pytest.warns(
47+
UserWarning, match='Expected `enum` but got `int` with value `1` - serialized value may not be as expected'
48+
):
4149
assert v.to_json(1) == b'1'
4250

4351

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

56-
with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
64+
with pytest.warns(
65+
UserWarning, match="Expected `enum` but got `str` with value `'a'` - serialized value may not be as expected"
66+
):
5767
assert v.to_python('a') == 'a'
58-
with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
68+
with pytest.warns(
69+
UserWarning, match="Expected `enum` but got `str` with value `'a'` - serialized value may not be as expected"
70+
):
5971
assert v.to_json('a') == b'"a"'
6072

6173

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

79-
with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
91+
with pytest.warns(
92+
UserWarning, match="Expected `enum` but got `str` with value `'x'` - serialized value may not be as expected"
93+
):
8094
assert v.to_python({'x': 'x'}) == {'x': 'x'}
81-
with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
95+
with pytest.warns(
96+
UserWarning, match="Expected `enum` but got `str` with value `'x'` - serialized value may not be as expected"
97+
):
8298
assert v.to_json({'x': 'x'}) == b'{"x":"x"}'
8399

84100

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

102-
with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
118+
with pytest.warns(
119+
UserWarning, match="Expected `enum` but got `str` with value `'x'` - serialized value may not be as expected"
120+
):
103121
assert v.to_python({'x': 'x'}) == {'x': 'x'}
104-
with pytest.warns(UserWarning, match='Expected `enum` but got `str` - serialized value may not be as expected'):
122+
with pytest.warns(
123+
UserWarning, match="Expected `enum` but got `str` with value `'x'` - serialized value may not be as expected"
124+
):
105125
assert v.to_json({'x': 'x'}) == b'{"x":"x"}'

tests/serializers/test_functions.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def append_42(value, _info):
207207
assert s.to_python([1, 2, 3], mode='json') == [1, 2, 3, 42]
208208
assert s.to_json([1, 2, 3]) == b'[1,2,3,42]'
209209

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

@@ -322,11 +322,17 @@ def test_wrong_return_type():
322322
)
323323
)
324324
)
325-
with pytest.warns(UserWarning, match='Expected `int` but got `str` - serialized value may not be as expected'):
325+
with pytest.warns(
326+
UserWarning, match="Expected `int` but got `str` with value `'123'` - serialized value may not be as expected"
327+
):
326328
assert s.to_python(123) == '123'
327-
with pytest.warns(UserWarning, match='Expected `int` but got `str` - serialized value may not be as expected'):
329+
with pytest.warns(
330+
UserWarning, match="Expected `int` but got `str` with value `'123'` - serialized value may not be as expected"
331+
):
328332
assert s.to_python(123, mode='json') == '123'
329-
with pytest.warns(UserWarning, match='Expected `int` but got `str` - serialized value may not be as expected'):
333+
with pytest.warns(
334+
UserWarning, match="Expected `int` but got `str` with value `'123'` - serialized value may not be as expected"
335+
):
330336
assert s.to_json(123) == b'"123"'
331337

332338

@@ -356,11 +362,17 @@ def f(value, serializer):
356362
assert s.to_python(3) == 'result=3'
357363
assert s.to_python(3, mode='json') == 'result=3'
358364
assert s.to_json(3) == b'"result=3"'
359-
with pytest.warns(UserWarning, match='Expected `str` but got `int` - serialized value may not be as expected'):
365+
with pytest.warns(
366+
UserWarning, match='Expected `str` but got `int` with value `42` - serialized value may not be as expected'
367+
):
360368
assert s.to_python(42) == 42
361-
with pytest.warns(UserWarning, match='Expected `str` but got `int` - serialized value may not be as expected'):
369+
with pytest.warns(
370+
UserWarning, match='Expected `str` but got `int` with value `42` - serialized value may not be as expected'
371+
):
362372
assert s.to_python(42, mode='json') == 42
363-
with pytest.warns(UserWarning, match='Expected `str` but got `int` - serialized value may not be as expected'):
373+
with pytest.warns(
374+
UserWarning, match='Expected `str` but got `int` with value `42` - serialized value may not be as expected'
375+
):
364376
assert s.to_json(42) == b'42'
365377

366378

@@ -611,7 +623,9 @@ def f(value, _info):
611623
return value
612624

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

617631

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

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

626642

0 commit comments

Comments
 (0)