Skip to content

Commit dcaf63e

Browse files
authored
Fix warning for tuple of wrong size in union (#1174)
1 parent 758bc51 commit dcaf63e

File tree

2 files changed

+144
-126
lines changed

2 files changed

+144
-126
lines changed

src/serializers/type_serializers/tuple.rs

+118-126
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ use std::iter;
77
use serde::ser::SerializeSeq;
88

99
use crate::definitions::DefinitionsBuilder;
10+
use crate::serializers::extra::SerCheck;
1011
use crate::serializers::type_serializers::any::AnySerializer;
1112
use crate::tools::SchemaDict;
13+
use crate::PydanticSerializationUnexpectedValue;
1214

1315
use super::{
1416
infer_json_key, infer_serialize, infer_to_python, py_err_se_err, BuildSerializer, CombinedSerializer, Extra,
@@ -70,52 +72,14 @@ impl TypeSerializer for TupleSerializer {
7072
let py = value.py();
7173

7274
let n_items = py_tuple.len();
73-
let mut py_tuple_iter = py_tuple.iter();
7475
let mut items = Vec::with_capacity(n_items);
7576

76-
macro_rules! use_serializers {
77-
($serializers_iter:expr) => {
78-
for (index, serializer) in $serializers_iter.enumerate() {
79-
let element = match py_tuple_iter.next() {
80-
Some(value) => value,
81-
None => break,
82-
};
83-
let op_next = self
84-
.filter
85-
.index_filter(index, include, exclude, Some(n_items))?;
86-
if let Some((next_include, next_exclude)) = op_next {
87-
items.push(serializer.to_python(element, next_include, next_exclude, extra)?);
88-
}
89-
}
90-
};
91-
}
92-
93-
if let Some(variadic_item_index) = self.variadic_item_index {
94-
// Need `saturating_sub` to handle items with too few elements without panicking
95-
let n_variadic_items = (n_items + 1).saturating_sub(self.serializers.len());
96-
let serializers_iter = self.serializers[..variadic_item_index]
97-
.iter()
98-
.chain(iter::repeat(&self.serializers[variadic_item_index]).take(n_variadic_items))
99-
.chain(self.serializers[variadic_item_index + 1..].iter());
100-
use_serializers!(serializers_iter);
101-
} else {
102-
use_serializers!(self.serializers.iter());
103-
let mut warned = false;
104-
for (i, element) in py_tuple_iter.enumerate() {
105-
if !warned {
106-
extra
107-
.warnings
108-
.custom_warning("Unexpected extra items present in tuple".to_string());
109-
warned = true;
110-
}
111-
let op_next =
112-
self.filter
113-
.index_filter(i + self.serializers.len(), include, exclude, Some(n_items))?;
114-
if let Some((next_include, next_exclude)) = op_next {
115-
items.push(AnySerializer.to_python(element, next_include, next_exclude, extra)?);
116-
}
117-
}
118-
};
77+
self.for_each_tuple_item_and_serializer(py_tuple, include, exclude, extra, |entry| {
78+
entry
79+
.serializer
80+
.to_python(entry.item, entry.include, entry.exclude, extra)
81+
.map(|item| items.push(item))
82+
})??;
11983

12084
match extra.mode {
12185
SerMode::Json => Ok(PyList::new(py, items).into_py(py)),
@@ -132,35 +96,14 @@ impl TypeSerializer for TupleSerializer {
13296
fn json_key<'py>(&self, key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
13397
match key.downcast::<PyTuple>() {
13498
Ok(py_tuple) => {
135-
let mut py_tuple_iter = py_tuple.iter();
136-
13799
let mut key_builder = KeyBuilder::new();
138100

139-
let n_items = py_tuple.len();
140-
141-
macro_rules! use_serializers {
142-
($serializers_iter:expr) => {
143-
for serializer in $serializers_iter {
144-
let element = match py_tuple_iter.next() {
145-
Some(value) => value,
146-
None => break,
147-
};
148-
key_builder.push(&serializer.json_key(element, extra)?);
149-
}
150-
};
151-
}
152-
153-
if let Some(variadic_item_index) = self.variadic_item_index {
154-
// Need `saturating_sub` to handle items with too few elements without panicking
155-
let n_variadic_items = (n_items + 1).saturating_sub(self.serializers.len());
156-
let serializers_iter = self.serializers[..variadic_item_index]
157-
.iter()
158-
.chain(iter::repeat(&self.serializers[variadic_item_index]).take(n_variadic_items))
159-
.chain(self.serializers[variadic_item_index + 1..].iter());
160-
use_serializers!(serializers_iter);
161-
} else {
162-
use_serializers!(self.serializers.iter());
163-
};
101+
self.for_each_tuple_item_and_serializer(py_tuple, None, None, extra, |entry| {
102+
entry
103+
.serializer
104+
.json_key(entry.item, extra)
105+
.map(|key| key_builder.push(&key))
106+
})??;
164107

165108
Ok(Cow::Owned(key_builder.finish()))
166109
}
@@ -184,63 +127,18 @@ impl TypeSerializer for TupleSerializer {
184127
let py_tuple: &PyTuple = py_tuple.downcast().map_err(py_err_se_err)?;
185128

186129
let n_items = py_tuple.len();
187-
let mut py_tuple_iter = py_tuple.iter();
188130
let mut seq = serializer.serialize_seq(Some(n_items))?;
189131

190-
macro_rules! use_serializers {
191-
($serializers_iter:expr) => {
192-
for (index, serializer) in $serializers_iter.enumerate() {
193-
let element = match py_tuple_iter.next() {
194-
Some(value) => value,
195-
None => break,
196-
};
197-
let op_next = self
198-
.filter
199-
.index_filter(index, include, exclude, Some(n_items))
200-
.map_err(py_err_se_err)?;
201-
if let Some((next_include, next_exclude)) = op_next {
202-
let item_serialize =
203-
PydanticSerializer::new(element, serializer, next_include, next_exclude, extra);
204-
seq.serialize_element(&item_serialize)?;
205-
}
206-
}
207-
};
208-
}
209-
210-
if let Some(variadic_item_index) = self.variadic_item_index {
211-
// Need `saturating_sub` to handle items with too few elements without panicking
212-
let n_variadic_items = (n_items + 1).saturating_sub(self.serializers.len());
213-
let serializers_iter = self.serializers[..variadic_item_index]
214-
.iter()
215-
.chain(iter::repeat(&self.serializers[variadic_item_index]).take(n_variadic_items))
216-
.chain(self.serializers[variadic_item_index + 1..].iter());
217-
use_serializers!(serializers_iter);
218-
} else {
219-
use_serializers!(self.serializers.iter());
220-
let mut warned = false;
221-
for (i, element) in py_tuple_iter.enumerate() {
222-
if !warned {
223-
extra
224-
.warnings
225-
.custom_warning("Unexpected extra items present in tuple".to_string());
226-
warned = true;
227-
}
228-
let op_next = self
229-
.filter
230-
.index_filter(i + self.serializers.len(), include, exclude, Some(n_items))
231-
.map_err(py_err_se_err)?;
232-
if let Some((next_include, next_exclude)) = op_next {
233-
let item_serialize = PydanticSerializer::new(
234-
element,
235-
&CombinedSerializer::Any(AnySerializer),
236-
next_include,
237-
next_exclude,
238-
extra,
239-
);
240-
seq.serialize_element(&item_serialize)?;
241-
}
242-
}
243-
};
132+
self.for_each_tuple_item_and_serializer(py_tuple, include, exclude, extra, |entry| {
133+
seq.serialize_element(&PydanticSerializer::new(
134+
entry.item,
135+
entry.serializer,
136+
entry.include,
137+
entry.exclude,
138+
extra,
139+
))
140+
})
141+
.map_err(py_err_se_err)??;
244142

245143
seq.end()
246144
}
@@ -254,6 +152,100 @@ impl TypeSerializer for TupleSerializer {
254152
fn get_name(&self) -> &str {
255153
&self.name
256154
}
155+
156+
fn retry_with_lax_check(&self) -> bool {
157+
true
158+
}
159+
}
160+
161+
struct TupleSerializerEntry<'a, 'py> {
162+
item: &'py PyAny,
163+
include: Option<&'py PyAny>,
164+
exclude: Option<&'py PyAny>,
165+
serializer: &'a CombinedSerializer,
166+
}
167+
168+
impl TupleSerializer {
169+
/// Try to serialize each item in the tuple with the corresponding serializer.
170+
///
171+
/// If the tuple doesn't match the length of the serializer, in strict mode, an error is returned.
172+
///
173+
/// The error type E is the type of the error returned by the closure, which is why there are two
174+
/// levels of `Result`.
175+
fn for_each_tuple_item_and_serializer<E>(
176+
&self,
177+
tuple: &PyTuple,
178+
include: Option<&PyAny>,
179+
exclude: Option<&PyAny>,
180+
extra: &Extra,
181+
mut f: impl for<'a, 'py> FnMut(TupleSerializerEntry<'a, 'py>) -> Result<(), E>,
182+
) -> PyResult<Result<(), E>> {
183+
let n_items = tuple.len();
184+
let mut py_tuple_iter = tuple.iter();
185+
186+
macro_rules! use_serializers {
187+
($serializers_iter:expr) => {
188+
for (index, serializer) in $serializers_iter.enumerate() {
189+
let element = match py_tuple_iter.next() {
190+
Some(value) => value,
191+
None => break,
192+
};
193+
let op_next = self.filter.index_filter(index, include, exclude, Some(n_items))?;
194+
if let Some((next_include, next_exclude)) = op_next {
195+
if let Err(e) = f(TupleSerializerEntry {
196+
item: element,
197+
include: next_include,
198+
exclude: next_exclude,
199+
serializer,
200+
}) {
201+
return Ok(Err(e));
202+
};
203+
}
204+
}
205+
};
206+
}
207+
208+
if let Some(variadic_item_index) = self.variadic_item_index {
209+
// Need `saturating_sub` to handle items with too few elements without panicking
210+
let n_variadic_items = (n_items + 1).saturating_sub(self.serializers.len());
211+
let serializers_iter = self.serializers[..variadic_item_index]
212+
.iter()
213+
.chain(iter::repeat(&self.serializers[variadic_item_index]).take(n_variadic_items))
214+
.chain(self.serializers[variadic_item_index + 1..].iter());
215+
use_serializers!(serializers_iter);
216+
} else if extra.check == SerCheck::Strict && n_items != self.serializers.len() {
217+
return Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
218+
"Expected {} items, but got {}",
219+
self.serializers.len(),
220+
n_items
221+
))));
222+
} else {
223+
use_serializers!(self.serializers.iter());
224+
let mut warned = false;
225+
for (i, element) in py_tuple_iter.enumerate() {
226+
if !warned {
227+
extra
228+
.warnings
229+
.custom_warning("Unexpected extra items present in tuple".to_string());
230+
warned = true;
231+
}
232+
let op_next = self
233+
.filter
234+
.index_filter(i + self.serializers.len(), include, exclude, Some(n_items))?;
235+
if let Some((next_include, next_exclude)) = op_next {
236+
if let Err(e) = f(TupleSerializerEntry {
237+
item: element,
238+
include: next_include,
239+
exclude: next_exclude,
240+
serializer: &CombinedSerializer::Any(AnySerializer),
241+
}) {
242+
return Ok(Err(e));
243+
};
244+
}
245+
}
246+
};
247+
Ok(Ok(()))
248+
}
257249
}
258250

259251
pub(crate) struct KeyBuilder {

tests/serializers/test_list_tuple.py

+26
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,29 @@ def test_tuple_pos_dict_key():
411411
assert s.to_python({(1, 'a', 2): 1}, mode='json') == {'1,a,2': 1}
412412
assert s.to_json({(1, 'a'): 1}) == b'{"1,a":1}'
413413
assert s.to_json({(1, 'a', 2): 1}) == b'{"1,a,2":1}'
414+
415+
416+
def test_tuple_wrong_size_union():
417+
# See https://github.com/pydantic/pydantic/issues/8677
418+
419+
f = core_schema.float_schema()
420+
s = SchemaSerializer(
421+
core_schema.union_schema([core_schema.tuple_schema([f, f]), core_schema.tuple_schema([f, f, f])])
422+
)
423+
assert s.to_python((1.0, 2.0)) == (1.0, 2.0)
424+
assert s.to_python((1.0, 2.0, 3.0)) == (1.0, 2.0, 3.0)
425+
426+
with pytest.warns(UserWarning, match='Unexpected extra items present in tuple'):
427+
s.to_python((1.0, 2.0, 3.0, 4.0))
428+
429+
assert s.to_python((1.0, 2.0), mode='json') == [1.0, 2.0]
430+
assert s.to_python((1.0, 2.0, 3.0), mode='json') == [1.0, 2.0, 3.0]
431+
432+
with pytest.warns(UserWarning, match='Unexpected extra items present in tuple'):
433+
s.to_python((1.0, 2.0, 3.0, 4.0), mode='json')
434+
435+
assert s.to_json((1.0, 2.0)) == b'[1.0,2.0]'
436+
assert s.to_json((1.0, 2.0, 3.0)) == b'[1.0,2.0,3.0]'
437+
438+
with pytest.warns(UserWarning, match='Unexpected extra items present in tuple'):
439+
s.to_json((1.0, 2.0, 3.0, 4.0))

0 commit comments

Comments
 (0)