From ee985db474589f6c62491efd8cf908784fa7061c Mon Sep 17 00:00:00 2001 From: Boxy Date: Tue, 30 Jul 2024 15:25:42 +0100 Subject: [PATCH 1/5] Change warning message --- src/errors/mod.rs | 46 ++++++++++++++++++++++++++++ src/errors/validation_exception.rs | 48 ++---------------------------- src/lib.rs | 3 +- src/serializers/extra.rs | 14 ++++++++- 4 files changed, 63 insertions(+), 48 deletions(-) diff --git a/src/errors/mod.rs b/src/errors/mod.rs index ffdda90e3..13323db06 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -1,3 +1,6 @@ +use core::fmt; +use std::borrow::Cow; + use pyo3::prelude::*; mod line_error; @@ -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 truncate_large_string(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}") + } +} diff --git a/src/errors/validation_exception.rs b/src/errors/validation_exception.rs index d41a10e65..fd36fc271 100644 --- a/src/errors/validation_exception.rs +++ b/src/errors/validation_exception.rs @@ -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, @@ -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::truncate_large_string(&mut output, input_str.to_cow())?; if let Ok(type_) = input_value.get_type().qualname() { write!(output, ", input_type={type_}")?; diff --git a/src/lib.rs b/src/lib.rs index eb598424b..94833b80a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,10 +14,11 @@ use validators::ValBytesMode; #[macro_use] mod py_gc; +pub mod errors; + mod argument_markers; mod build_tools; mod definitions; -mod errors; mod input; mod lookup_key; mod recursion_guard; diff --git a/src/serializers/extra.rs b/src/serializers/extra.rs index dbf6f7b31..f03f7b7f4 100644 --- a/src/serializers/extra.rs +++ b/src/serializers/extra.rs @@ -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` @@ -424,8 +425,19 @@ impl CollectWarnings { .get_type() .qualname() .unwrap_or_else(|_| PyString::new_bound(value.py(), "")); + + let input_str = safe_repr(value); + let mut value_str = String::with_capacity(100); + value_str.push_str("with value `"); + match crate::errors::truncate_large_string(&mut value_str, input_str.to_cow()) { + Err(_) => value_str.clear(), + Ok(()) => { + value_str.push_str("` "); + } + }; + 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" )); } } From 52af8cecbc95e3c3f57286e966df974e5d126183 Mon Sep 17 00:00:00 2001 From: Boxy Date: Tue, 30 Jul 2024 15:25:51 +0100 Subject: [PATCH 2/5] Update tests --- tests/serializers/test_any.py | 4 +-- tests/serializers/test_bytes.py | 16 ++++++--- tests/serializers/test_datetime.py | 10 ++++-- tests/serializers/test_decimal.py | 8 +++-- tests/serializers/test_enum.py | 40 ++++++++++++++++------ tests/serializers/test_functions.py | 34 ++++++++++++++----- tests/serializers/test_generator.py | 19 +++++++---- tests/serializers/test_list_tuple.py | 30 ++++++++--------- tests/serializers/test_model.py | 11 +++--- tests/serializers/test_nullable.py | 4 ++- tests/serializers/test_other.py | 4 ++- tests/serializers/test_set_frozenset.py | 14 +++++--- tests/serializers/test_simple.py | 9 +++-- tests/serializers/test_string.py | 45 ++++++++++++++++++------- tests/serializers/test_timedelta.py | 6 ++-- tests/serializers/test_union.py | 2 +- tests/serializers/test_url.py | 8 +++-- tests/serializers/test_uuid.py | 8 +++-- 18 files changed, 189 insertions(+), 83 deletions(-) diff --git a/tests/serializers/test_any.py b/tests/serializers/test_any.py index 7337d012e..12ac463e1 100644 --- a/tests/serializers/test_any.py +++ b/tests/serializers/test_any.py @@ -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" ] @@ -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' ] diff --git a/tests/serializers/test_bytes.py b/tests/serializers/test_bytes.py index cc2d44785..32c11fac9 100644 --- a/tests/serializers/test_bytes.py +++ b/tests/serializers/test_bytes.py @@ -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"' diff --git a/tests/serializers/test_datetime.py b/tests/serializers/test_datetime.py index 85fc51e95..19d5477f3 100644 --- a/tests/serializers/test_datetime.py +++ b/tests/serializers/test_datetime.py @@ -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' diff --git a/tests/serializers/test_decimal.py b/tests/serializers/test_decimal.py index 1e74c8eb7..277843aec 100644 --- a/tests/serializers/test_decimal.py +++ b/tests/serializers/test_decimal.py @@ -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' diff --git a/tests/serializers/test_enum.py b/tests/serializers/test_enum.py index 5161a2b39..66ab00691 100644 --- a/tests/serializers/test_enum.py +++ b/tests/serializers/test_enum.py @@ -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' @@ -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' @@ -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"' @@ -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"}' @@ -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"}' diff --git a/tests/serializers/test_functions.py b/tests/serializers/test_functions.py index c5b28ec31..a151b7454 100644 --- a/tests/serializers/test_functions.py +++ b/tests/serializers/test_functions.py @@ -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' @@ -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"' @@ -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' @@ -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' @@ -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' diff --git a/tests/serializers/test_generator.py b/tests/serializers/test_generator.py index 89b6b0350..cbd72ee8e 100644 --- a/tests/serializers/test_generator.py +++ b/tests/serializers/test_generator.py @@ -54,12 +54,12 @@ def test_generator_any(): assert s.to_json(iter(['a', b'b', 3])) == b'["a","b",3]' assert s.to_json(gen_ok('a', b'b', 3)) == b'["a","b",3]' - msg = 'Expected `generator` but got `int` - serialized value may not be as expected' + msg = 'Expected `generator` but got `int` with value `4` - serialized value may not be as expected' with pytest.warns(UserWarning, match=msg): assert s.to_python(4) == 4 - with pytest.warns(UserWarning, match='Expected `generator` but got `tuple`'): + with pytest.warns(UserWarning, match="Expected `generator` but got `tuple` with value `\\('a', b'b', 3\\)`"): assert s.to_python(('a', b'b', 3)) == ('a', b'b', 3) - with pytest.warns(UserWarning, match='Expected `generator` but got `str`'): + with pytest.warns(UserWarning, match="Expected `generator` but got `str` with value `'abc'`"): assert s.to_python('abc') == 'abc' with pytest.raises(ValueError, match='oops'): @@ -88,14 +88,21 @@ def test_generator_int(): with pytest.raises(ValueError, match='oops'): s.to_json(gen_error(1, 2)) - 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 `'a'` - serialized value may not be as expected" + ): s.to_json(gen_ok(1, 'a')) gen = s.to_python(gen_ok(1, 'a')) assert next(gen) == 1 - 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 `'a'` - serialized value may not be as expected" + ): assert next(gen) == 'a' - with pytest.warns(UserWarning, match='Expected `generator` but got `tuple` - serialized value may not.+'): + with pytest.warns( + UserWarning, + match='Expected `generator` but got `tuple` with value `\\(1, 2, 3\\)` - serialized value may not.+', + ): s.to_python((1, 2, 3)) diff --git a/tests/serializers/test_list_tuple.py b/tests/serializers/test_list_tuple.py index 3591ffc9d..b4bf1208b 100644 --- a/tests/serializers/test_list_tuple.py +++ b/tests/serializers/test_list_tuple.py @@ -18,26 +18,26 @@ def test_list_any(): def test_list_fallback(): v = SchemaSerializer(core_schema.list_schema(core_schema.any_schema())) - msg = 'Expected `list[any]` but got `str` - serialized value may not be as expected' + msg = "Expected `list[any]` but got `str` with value `'apple'` - serialized value may not be as expected" with pytest.warns(UserWarning, match=re.escape(msg)): assert v.to_python('apple') == 'apple' with pytest.warns(UserWarning) as warning_info: assert v.to_json('apple') == b'"apple"' assert [w.message.args[0] for w in warning_info.list] == [ - 'Pydantic serializer warnings:\n Expected `list[any]` but got `str` - serialized value may not be as expected' + "Pydantic serializer warnings:\n Expected `list[any]` but got `str` with value `'apple'` - serialized value may not be as expected" ] - msg = 'Expected `list[any]` but got `bytes` - serialized value may not be as expected' + msg = "Expected `list[any]` but got `bytes` with value `b'apple'` - serialized value may not be as expected" with pytest.warns(UserWarning, match=re.escape(msg)): assert v.to_json(b'apple') == b'"apple"' - msg = 'Expected `list[any]` but got `tuple` - serialized value may not be as expected' + msg = 'Expected `list[any]` but got `tuple` with value `(1, 2, 3)` - serialized value may not be as expected' with pytest.warns(UserWarning, match=re.escape(msg)): assert v.to_python((1, 2, 3)) == (1, 2, 3) # # even though we're in the fallback state, non JSON types should still be converted to JSON here - msg = 'Expected `list[any]` but got `tuple` - serialized value may not be as expected' + msg = 'Expected `list[any]` but got `tuple` with value `(1, 2, 3)` - serialized value may not be as expected' with pytest.warns(UserWarning, match=re.escape(msg)): assert v.to_python((1, 2, 3), mode='json') == [1, 2, 3] @@ -48,18 +48,18 @@ def test_list_str_fallback(): assert v.to_json([1, 2, 3]) == b'[1,2,3]' assert [w.message.args[0] for w in warning_info.list] == [ 'Pydantic serializer warnings:\n' - ' Expected `str` but got `int` - serialized value may not be as expected\n' - ' Expected `str` but got `int` - serialized value may not be as expected\n' - ' Expected `str` but got `int` - serialized value may not be as expected' + ' Expected `str` but got `int` with value `1` - serialized value may not be as expected\n' + ' Expected `str` but got `int` with value `2` - serialized value may not be as expected\n' + ' Expected `str` but got `int` with value `3` - serialized value may not be as expected' ] with pytest.raises(PydanticSerializationError) as warning_ex: v.to_json([1, 2, 3], warnings='error') assert str(warning_ex.value) == ''.join( [ 'Pydantic serializer warnings:\n' - ' Expected `str` but got `int` - serialized value may not be as expected\n' - ' Expected `str` but got `int` - serialized value may not be as expected\n' - ' Expected `str` but got `int` - serialized value may not be as expected' + ' Expected `str` but got `int` with value `1` - serialized value may not be as expected\n' + ' Expected `str` but got `int` with value `2` - serialized value may not be as expected\n' + ' Expected `str` but got `int` with value `3` - serialized value may not be as expected' ] ) @@ -243,25 +243,25 @@ def test_include_error_call_time(schema_func, seq_f, include, exclude): def test_tuple_fallback(): v = SchemaSerializer(core_schema.tuple_variable_schema(core_schema.any_schema())) - msg = 'Expected `tuple[any, ...]` but got `str` - serialized value may not be as expected' + msg = "Expected `tuple[any, ...]` but got `str` with value `'apple'` - serialized value may not be as expected" with pytest.warns(UserWarning, match=re.escape(msg)): assert v.to_python('apple') == 'apple' with pytest.warns(UserWarning) as warning_info: assert v.to_json([1, 2, 3]) == b'[1,2,3]' assert [w.message.args[0] for w in warning_info.list] == [ - 'Pydantic serializer warnings:\n Expected `tuple[any, ...]` but got `list` - ' + 'Pydantic serializer warnings:\n Expected `tuple[any, ...]` but got `list` with value `[1, 2, 3]` - ' 'serialized value may not be as expected' ] - msg = 'Expected `tuple[any, ...]` but got `bytes` - serialized value may not be as expected' + msg = "Expected `tuple[any, ...]` but got `bytes` with value `b'apple'` - serialized value may not be as expected" with pytest.warns(UserWarning, match=re.escape(msg)): assert v.to_json(b'apple') == b'"apple"' assert v.to_python((1, 2, 3)) == (1, 2, 3) # even though we're in the fallback state, non JSON types should still be converted to JSON here - msg = 'Expected `tuple[any, ...]` but got `list` - serialized value may not be as expected' + msg = 'Expected `tuple[any, ...]` but got `list` with value `[1, 2, 3]` - serialized value may not be as expected' with pytest.warns(UserWarning, match=re.escape(msg)): assert v.to_python([1, 2, 3], mode='json') == [1, 2, 3] diff --git a/tests/serializers/test_model.py b/tests/serializers/test_model.py index ddf9e5f97..207354946 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -230,14 +230,17 @@ def test_model_wrong_warn(): assert s.to_python(None, mode='json') is None assert s.to_json(None) == b'null' - with pytest.warns(UserWarning, match='Expected `MyModel` but got `int` - serialized value may.+'): + with pytest.warns(UserWarning, match='Expected `MyModel` but got `int` with value `123` - serialized value may.+'): assert s.to_python(123) == 123 - with pytest.warns(UserWarning, match='Expected `MyModel` but got `int` - serialized value may.+'): + with pytest.warns(UserWarning, match='Expected `MyModel` but got `int` with value `123` - serialized value may.+'): assert s.to_python(123, mode='json') == 123 - with pytest.warns(UserWarning, match='Expected `MyModel` but got `int` - serialized value may.+'): + with pytest.warns(UserWarning, match='Expected `MyModel` but got `int` with value `123` - serialized value may.+'): assert s.to_json(123) == b'123' - with pytest.warns(UserWarning, match='Expected `MyModel` but got `dict` - serialized value may.+'): + with pytest.warns( + UserWarning, + match="Expected `MyModel` but got `dict` with value `{'foo': 1, 'bar': b'more'}` - serialized value may.+", + ): assert s.to_python({'foo': 1, 'bar': b'more'}) == {'foo': 1, 'bar': b'more'} diff --git a/tests/serializers/test_nullable.py b/tests/serializers/test_nullable.py index 173fcb3ca..86b64a213 100644 --- a/tests/serializers/test_nullable.py +++ b/tests/serializers/test_nullable.py @@ -11,5 +11,7 @@ def test_nullable(): assert s.to_python(1, mode='json') == 1 assert s.to_json(1) == b'1' assert s.to_json(None) == b'null' - 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 `'aaa'` - serialized value may not be as expected" + ): assert s.to_json('aaa') == b'"aaa"' diff --git a/tests/serializers/test_other.py b/tests/serializers/test_other.py index 505c003d4..13877d096 100644 --- a/tests/serializers/test_other.py +++ b/tests/serializers/test_other.py @@ -44,7 +44,9 @@ def test_lax_or_strict(): assert plain_repr(s) == 'SchemaSerializer(serializer=Str(StrSerializer),definitions=[])' assert s.to_json('abc') == b'"abc"' - 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 `123` - serialized value may not be as expected' + ): assert s.to_json(123) == b'123' diff --git a/tests/serializers/test_set_frozenset.py b/tests/serializers/test_set_frozenset.py index 1c638a979..c2df80f0a 100644 --- a/tests/serializers/test_set_frozenset.py +++ b/tests/serializers/test_set_frozenset.py @@ -26,11 +26,15 @@ def test_frozenset_any(): @pytest.mark.parametrize( 'input_value,json_output,warning_type', [ - ('apple', 'apple', r'`set\[int\]` but got `str`'), - ([1, 2, 3], [1, 2, 3], r'`set\[int\]` but got `list`'), - ((1, 2, 3), [1, 2, 3], r'`set\[int\]` but got `tuple`'), - (frozenset([1, 2, 3]), IsList(1, 2, 3, check_order=False), r'`set\[int\]` but got `frozenset`'), - ({1, 2, 'a'}, IsList(1, 2, 'a', check_order=False), '`int` but got `str`'), + ('apple', 'apple', r"`set\[int\]` but got `str` with value `'apple'`"), + ([1, 2, 3], [1, 2, 3], r'`set\[int\]` but got `list` with value `\[1, 2, 3\]`'), + ((1, 2, 3), [1, 2, 3], r'`set\[int\]` but got `tuple` with value `\(1, 2, 3\)`'), + ( + frozenset([1, 2, 3]), + IsList(1, 2, 3, check_order=False), + r'`set\[int\]` but got `frozenset` with value `frozenset\({1, 2, 3}\)`', + ), + ({1, 2, 'a'}, IsList(1, 2, 'a', check_order=False), "`int` but got `str` with value `'a'`"), ], ) def test_set_fallback(input_value, json_output, warning_type): diff --git a/tests/serializers/test_simple.py b/tests/serializers/test_simple.py index b0fe7b836..19b3b4f55 100644 --- a/tests/serializers/test_simple.py +++ b/tests/serializers/test_simple.py @@ -109,17 +109,20 @@ def test_int_to_float_key(): def test_simple_serializers_fallback(schema_type): s = SchemaSerializer({'type': schema_type}) with pytest.warns( - UserWarning, match=f'Expected `{schema_type}` but got `list` - serialized value may not be as expected' + UserWarning, + match=f'Expected `{schema_type}` but got `list` with value `\\[1, 2, 3\\]` - serialized value may not be as expected', ): assert s.to_python([1, 2, 3]) == [1, 2, 3] with pytest.warns( - UserWarning, match=f'Expected `{schema_type}` but got `list` - serialized value may not be as expected' + UserWarning, + match=f"Expected `{schema_type}` but got `list` with value `\\[1, 2, b'bytes'\\]` - serialized value may not be as expected", ): assert s.to_python([1, 2, b'bytes'], mode='json') == [1, 2, 'bytes'] with pytest.warns( - UserWarning, match=f'Expected `{schema_type}` but got `list` - serialized value may not be as expected' + UserWarning, + match=f'Expected `{schema_type}` but got `list` with value `\\[1, 2, 3\\]` - serialized value may not be as expected', ): assert s.to_json([1, 2, 3]) == b'[1,2,3]' diff --git a/tests/serializers/test_string.py b/tests/serializers/test_string.py index 6038b6a67..ec14bfbe6 100644 --- a/tests/serializers/test_string.py +++ b/tests/serializers/test_string.py @@ -28,23 +28,41 @@ def test_str_fallback(): assert s.to_python(None) is None assert s.to_python(None, mode='json') is None assert s.to_json(None) == b'null' - 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 `123` - serialized value may not be as expected' + ): assert s.to_python(123) == 123 - 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 `123` - serialized value may not be as expected' + ): assert s.to_python(123, mode='json') == 123 - 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 `123` - serialized value may not be as expected' + ): assert s.to_json(123) == b'123' - 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 `123` - serialized value may not be as expected' + ): assert s.to_python(123, warnings='warn') == 123 - 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 `123` - serialized value may not be as expected' + ): assert s.to_python(123, mode='json', warnings='warn') == 123 - 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 `123` - serialized value may not be as expected' + ): assert s.to_json(123, warnings='warn') == b'123' - 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 `123` - serialized value may not be as expected' + ): assert s.to_python(123, warnings=True) == 123 - 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 `123` - serialized value may not be as expected' + ): assert s.to_python(123, mode='json', warnings=True) == 123 - 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 `123` - serialized value may not be as expected' + ): assert s.to_json(123, warnings=True) == b'123' @@ -61,15 +79,18 @@ def test_str_no_warnings(): def test_str_errors(): s = SchemaSerializer(core_schema.str_schema()) with pytest.raises( - PydanticSerializationError, match='Expected `str` but got `int` - serialized value may not be as expected' + PydanticSerializationError, + match='Expected `str` but got `int` with value `123` - serialized value may not be as expected', ): assert s.to_python(123, warnings='error') == 123 with pytest.raises( - PydanticSerializationError, match='Expected `str` but got `int` - serialized value may not be as expected' + PydanticSerializationError, + match='Expected `str` but got `int` with value `123` - serialized value may not be as expected', ): assert s.to_python(123, mode='json', warnings='error') == 123 with pytest.raises( - PydanticSerializationError, match='Expected `str` but got `int` - serialized value may not be as expected' + PydanticSerializationError, + match='Expected `str` but got `int` with value `123` - serialized value may not be as expected', ): assert s.to_json(123, warnings='error') == b'123' diff --git a/tests/serializers/test_timedelta.py b/tests/serializers/test_timedelta.py index 91b5716b0..19abb673d 100644 --- a/tests/serializers/test_timedelta.py +++ b/tests/serializers/test_timedelta.py @@ -18,12 +18,14 @@ def test_timedelta(): assert v.to_json(timedelta(days=2, hours=3, minutes=4)) == b'"P2DT3H4M"' with pytest.warns( - UserWarning, match='Expected `timedelta` but got `int` - serialized value may not be as expected' + UserWarning, + match='Expected `timedelta` 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 `timedelta` but got `int` - serialized value may not be as expected' + UserWarning, + match='Expected `timedelta` but got `int` with value `123` - serialized value may not be as expected', ): assert v.to_json(123) == b'123' diff --git a/tests/serializers/test_union.py b/tests/serializers/test_union.py index ee5ed3fc4..86811e560 100644 --- a/tests/serializers/test_union.py +++ b/tests/serializers/test_union.py @@ -32,7 +32,7 @@ def test_union_bool_int(input_value, expected_value, bool_case_label, int_case_l def test_union_error(): s = SchemaSerializer(core_schema.union_schema([core_schema.bool_schema(), core_schema.int_schema()])) - msg = 'Expected `Union[bool, int]` but got `str` - serialized value may not be as expected' + msg = "Expected `Union[bool, int]` but got `str` with value `'a string'` - serialized value may not be as expected" with pytest.warns(UserWarning, match=re.escape(msg)): assert s.to_python('a string') == 'a string' diff --git a/tests/serializers/test_url.py b/tests/serializers/test_url.py index 17cb70a80..be906c75b 100644 --- a/tests/serializers/test_url.py +++ b/tests/serializers/test_url.py @@ -18,7 +18,10 @@ def test_url(): assert s.to_python(url, mode='json') == 'https://example.com/' assert s.to_json(url) == b'"https://example.com/"' - with pytest.warns(UserWarning, match='Expected `url` but got `str` - serialized value may not be as expected'): + with pytest.warns( + UserWarning, + match="Expected `url` but got `str` with value `'https://example.com'` - serialized value may not be as expected", + ): assert s.to_python('https://example.com', mode='json') == 'https://example.com' @@ -36,7 +39,8 @@ def test_multi_host_url(): assert s.to_json(url) == b'"https://example.com,example.org/path"' with pytest.warns( - UserWarning, match='Expected `multi-host-url` but got `str` - serialized value may not be as expected' + UserWarning, + match="Expected `multi-host-url` but got `str` with value `'https://ex.com,ex.org/path'` - serialized value may not be as expected", ): assert s.to_python('https://ex.com,ex.org/path', mode='json') == 'https://ex.com,ex.org/path' diff --git a/tests/serializers/test_uuid.py b/tests/serializers/test_uuid.py index 9929891cb..fed9cfc98 100644 --- a/tests/serializers/test_uuid.py +++ b/tests/serializers/test_uuid.py @@ -14,10 +14,14 @@ def test_uuid(): ) assert v.to_json(UUID('12345678-1234-5678-1234-567812345678')) == b'"12345678-1234-5678-1234-567812345678"' - with pytest.warns(UserWarning, match='Expected `uuid` but got `int` - serialized value may not be as expected'): + with pytest.warns( + UserWarning, match='Expected `uuid` 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 `uuid` but got `int` - serialized value may not be as expected'): + with pytest.warns( + UserWarning, match='Expected `uuid` but got `int` with value `123` - serialized value may not be as expected' + ): assert v.to_json(123) == b'123' From 985d3595ddb7d288d3ac5ac6743580a38748a546 Mon Sep 17 00:00:00 2001 From: Boxy Date: Tue, 30 Jul 2024 15:30:01 +0100 Subject: [PATCH 3/5] Huge string test --- tests/serializers/test_string.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/serializers/test_string.py b/tests/serializers/test_string.py index ec14bfbe6..074179d8a 100644 --- a/tests/serializers/test_string.py +++ b/tests/serializers/test_string.py @@ -23,6 +23,18 @@ def test_str(): assert json.loads(json_emoji) == 'emoji 💩' +def test_huge_str(): + v = SchemaSerializer(core_schema.int_schema()) + msg = "Expected `int` but got `str` with value `'123456789012345678901234...89012345678901234567890'` - serialized value may not be as expected" + with pytest.warns(UserWarning, match=msg): + v.to_python( + '12345678901234567890123456789012345678901234567890123456789012345678901234567890\ + 12345678901234567890123456789012345678901234567890123456789012345678901234567890\ + 12345678901234567890123456789012345678901234567890123456789012345678901234567890\ + 12345678901234567890123456789012345678901234567890123456789012345678901234567890' + ) + + def test_str_fallback(): s = SchemaSerializer(core_schema.str_schema()) assert s.to_python(None) is None From 1254911bd397b533fe0bfc1161051a459a6660bd Mon Sep 17 00:00:00 2001 From: Boxy Date: Tue, 6 Aug 2024 16:01:54 +0100 Subject: [PATCH 4/5] Review --- src/errors/mod.rs | 2 +- src/errors/validation_exception.rs | 2 +- src/lib.rs | 3 +-- src/serializers/extra.rs | 11 ++++------- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 13323db06..255ea3cc8 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -64,7 +64,7 @@ pub fn ceil_char_boundary(value: &str, index: usize) -> usize { .map_or(upper_bound, |pos| pos + index) } -pub fn truncate_large_string(f: &mut F, val: Cow<'_, str>) -> std::fmt::Result { +pub fn write_truncated_to_50_bytes(f: &mut F, val: Cow<'_, str>) -> std::fmt::Result { if val.len() > 50 { write!( f, diff --git a/src/errors/validation_exception.rs b/src/errors/validation_exception.rs index fd36fc271..e7861fa9b 100644 --- a/src/errors/validation_exception.rs +++ b/src/errors/validation_exception.rs @@ -526,7 +526,7 @@ impl PyLineError { let input_value = self.input_value.bind(py); let input_str = safe_repr(input_value); write!(output, ", input_value=")?; - super::truncate_large_string(&mut output, input_str.to_cow())?; + 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_}")?; diff --git a/src/lib.rs b/src/lib.rs index 94833b80a..eb598424b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,11 +14,10 @@ use validators::ValBytesMode; #[macro_use] mod py_gc; -pub mod errors; - mod argument_markers; mod build_tools; mod definitions; +mod errors; mod input; mod lookup_key; mod recursion_guard; diff --git a/src/serializers/extra.rs b/src/serializers/extra.rs index f03f7b7f4..4ddfad1e3 100644 --- a/src/serializers/extra.rs +++ b/src/serializers/extra.rs @@ -429,15 +429,12 @@ impl CollectWarnings { let input_str = safe_repr(value); let mut value_str = String::with_capacity(100); value_str.push_str("with value `"); - match crate::errors::truncate_large_string(&mut value_str, input_str.to_cow()) { - Err(_) => value_str.clear(), - Ok(()) => { - value_str.push_str("` "); - } - }; + crate::errors::write_truncated_to_50_bytes(&mut value_str, input_str.to_cow()) + .expect("Writing to a `String` failed"); + value_str.push_str("`"); self.add_warning(format!( - "Expected `{field_type}` but got `{type_name}` {value_str}- serialized value may not be as expected" + "Expected `{field_type}` but got `{type_name}` {value_str} - serialized value may not be as expected" )); } } From f84e1b7a9df1c58641dfcb60d6c0fcea9f4fb71d Mon Sep 17 00:00:00 2001 From: Boxy Date: Tue, 6 Aug 2024 16:42:47 +0100 Subject: [PATCH 5/5] clippy --- src/serializers/extra.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serializers/extra.rs b/src/serializers/extra.rs index 4ddfad1e3..a62b55304 100644 --- a/src/serializers/extra.rs +++ b/src/serializers/extra.rs @@ -431,7 +431,7 @@ impl CollectWarnings { 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_str("`"); + value_str.push('`'); self.add_warning(format!( "Expected `{field_type}` but got `{type_name}` {value_str} - serialized value may not be as expected"