Skip to content

Commit 554ec40

Browse files
committed
tests for set,frozenset,list,dict,typed_dict
1 parent 2bf9d9b commit 554ec40

File tree

9 files changed

+281
-68
lines changed

9 files changed

+281
-68
lines changed

python/pydantic_core/core_schema.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2854,7 +2854,7 @@ def typed_dict_field(
28542854
28552855
Args:
28562856
schema: The schema to use for the field
2857-
required: Whether the field is required
2857+
required: Whether the field is required, otherwise uses the value from `total` on the typed dict
28582858
validation_alias: The alias(es) to use to find the field in the validation data
28592859
serialization_alias: The alias to use as a key when serializing
28602860
serialization_exclude: Whether to exclude the field when serializing
@@ -2930,7 +2930,7 @@ class MyTypedDict(TypedDict):
29302930
ref: optional unique identifier of the schema, used to reference the schema in other places
29312931
metadata: Any other information you want to include with the schema, not used by pydantic-core
29322932
extra_behavior: The extra behavior to use for the typed dict
2933-
total: Whether the typed dict is total
2933+
total: Whether the typed dict is total, otherwise uses `typed_dict_total` from config
29342934
populate_by_name: Whether the typed dict should populate by name
29352935
serialization: Custom serialization schema
29362936
"""

src/errors/line_error.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,11 @@ impl ValLineError {
146146
self
147147
}
148148

149-
pub fn last_loc_item(&self) -> Option<&LocItem> {
149+
pub fn first_loc_item(&self) -> Option<&LocItem> {
150150
match &self.location {
151151
Location::Empty => None,
152-
// first because order is reversed
153-
Location::List(loc_items) => loc_items.first(),
152+
// last because order is reversed
153+
Location::List(loc_items) => loc_items.last(),
154154
}
155155
}
156156
}

src/errors/mod.rs

-38
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::validators::ValidationState;
21
use pyo3::prelude::*;
32

43
mod line_error;
@@ -31,40 +30,3 @@ pub fn py_err_string(py: Python, err: PyErr) -> String {
3130
Err(_) => "Unknown Error".to_string(),
3231
}
3332
}
34-
35-
/// If we're in `allow_partial` mode, whether all errors occurred in the last element of the input.
36-
pub fn sequence_valid_as_partial(state: &ValidationState, input_length: usize, errors: &[ValLineError]) -> bool {
37-
if !state.extra().allow_partial {
38-
return false;
39-
}
40-
// for the error to be in the last element, the index of all errors must be `input_length - 1`
41-
let last_index = (input_length - 1) as i64;
42-
errors.iter().all(|error| {
43-
if let Some(LocItem::I(loc_index)) = error.last_loc_item() {
44-
*loc_index == last_index
45-
} else {
46-
false
47-
}
48-
})
49-
}
50-
51-
/// If we're in `allow_partial` mode, whether all errors occurred in the last value of the input.
52-
pub fn mapping_valid_as_partial(
53-
state: &ValidationState,
54-
opt_last_key: Option<impl Into<LocItem>>,
55-
errors: &[ValLineError],
56-
) -> bool {
57-
if !state.extra().allow_partial {
58-
return false;
59-
}
60-
let Some(last_key) = opt_last_key.map(Into::into) else {
61-
return false;
62-
};
63-
errors.iter().all(|error| {
64-
if let Some(loc_item) = error.last_loc_item() {
65-
loc_item == &last_key
66-
} else {
67-
false
68-
}
69-
})
70-
}

src/input/return_enums.rs

+20-4
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ use pyo3::types::{PyBytes, PyComplex, PyFloat, PyFrozenSet, PyIterator, PyMappin
1717
use serde::{ser::Error, Serialize, Serializer};
1818

1919
use crate::errors::{
20-
py_err_string, sequence_valid_as_partial, ErrorType, ErrorTypeDefaults, InputValue, ToErrorValue, ValError,
21-
ValLineError, ValResult,
20+
py_err_string, ErrorType, ErrorTypeDefaults, InputValue, LocItem, ToErrorValue, ValError, ValLineError, ValResult,
2221
};
2322
use crate::py_gc::PyGcTraverse;
2423
use crate::tools::{extract_i64, extract_int, new_py_string, py_err};
@@ -131,7 +130,6 @@ pub(crate) fn validate_iter_to_vec<'py>(
131130
let mut errors: Vec<ValLineError> = Vec::new();
132131
let mut index = 0;
133132
for item_result in iter {
134-
index += 1;
135133
let item = item_result.map_err(|e| any_next_error!(py, e, max_length_check.input, index))?;
136134
match validator.validate(py, item.borrow_input(), state) {
137135
Ok(item) => {
@@ -148,6 +146,7 @@ pub(crate) fn validate_iter_to_vec<'py>(
148146
Err(ValError::Omit) => (),
149147
Err(err) => return Err(err),
150148
}
149+
index += 1;
151150
}
152151

153152
if errors.is_empty() || sequence_valid_as_partial(state, index, &errors) {
@@ -157,6 +156,23 @@ pub(crate) fn validate_iter_to_vec<'py>(
157156
}
158157
}
159158

159+
/// If we're in `allow_partial` mode, whether all errors occurred in the last element of the input.
160+
pub fn sequence_valid_as_partial(state: &ValidationState, input_length: usize, errors: &[ValLineError]) -> bool {
161+
if !state.extra().allow_partial {
162+
false
163+
} else {
164+
// for the error to be in the last element, the index of all errors must be `input_length - 1`
165+
let last_index = (input_length - 1) as i64;
166+
errors.iter().all(|error| {
167+
if let Some(LocItem::I(loc_index)) = error.first_loc_item() {
168+
*loc_index == last_index
169+
} else {
170+
false
171+
}
172+
})
173+
}
174+
}
175+
160176
pub trait BuildSet {
161177
fn build_add(&self, item: PyObject) -> PyResult<()>;
162178

@@ -202,7 +218,6 @@ pub(crate) fn validate_iter_to_set<'py>(
202218
let mut errors: Vec<ValLineError> = Vec::new();
203219
let mut index = 0;
204220
for item_result in iter {
205-
index += 1;
206221
let item = item_result.map_err(|e| any_next_error!(py, e, input, index))?;
207222
match validator.validate(py, item.borrow_input(), state) {
208223
Ok(item) => {
@@ -233,6 +248,7 @@ pub(crate) fn validate_iter_to_set<'py>(
233248
if fail_fast && !errors.is_empty() {
234249
return Err(ValError::LineErrors(errors));
235250
}
251+
index += 1;
236252
}
237253

238254
if errors.is_empty() || sequence_valid_as_partial(state, index, &errors) {

src/validators/dict.rs

+12-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use pyo3::prelude::*;
33
use pyo3::types::PyDict;
44

55
use crate::build_tools::is_strict;
6-
use crate::errors::{sequence_valid_as_partial, LocItem, ValError, ValLineError, ValResult};
6+
use crate::errors::{LocItem, ValError, ValLineError, ValResult};
77
use crate::input::BorrowInput;
88
use crate::input::ConsumeIterator;
99
use crate::input::{Input, ValidatedDict};
@@ -109,10 +109,13 @@ where
109109
fn consume_iterator(self, iterator: impl Iterator<Item = ValResult<(Key, Value)>>) -> ValResult<PyObject> {
110110
let output = PyDict::new_bound(self.py);
111111
let mut errors: Vec<ValLineError> = Vec::new();
112-
let mut input_length = 0;
112+
// this should only be set to if:
113+
// we get errors in a value, there are no previous errors, and no items come after that
114+
// e.g. if we get errors just in the last value
115+
let mut errors_in_last = false;
113116

114117
for item_result in iterator {
115-
input_length += 1;
118+
errors_in_last = false;
116119
let (key, value) = item_result?;
117120
let output_key = match self.key_validator.validate(self.py, key.borrow_input(), self.state) {
118121
Ok(value) => Some(value),
@@ -127,22 +130,23 @@ where
127130
Err(err) => return Err(err),
128131
};
129132
let output_value = match self.value_validator.validate(self.py, value.borrow_input(), self.state) {
130-
Ok(value) => Some(value),
133+
Ok(value) => value,
131134
Err(ValError::LineErrors(line_errors)) => {
135+
errors_in_last = errors.is_empty();
132136
for err in line_errors {
133137
errors.push(err.with_outer_location(key.clone()));
134138
}
135-
None
139+
continue;
136140
}
137141
Err(ValError::Omit) => continue,
138142
Err(err) => return Err(err),
139143
};
140-
if let (Some(key), Some(value)) = (output_key, output_value) {
141-
output.set_item(key, value)?;
144+
if let Some(key) = output_key {
145+
output.set_item(key, output_value)?;
142146
}
143147
}
144148

145-
if errors.is_empty() || sequence_valid_as_partial(self.state, input_length, &errors) {
149+
if errors.is_empty() || (self.state.extra().allow_partial && errors_in_last) {
146150
let input = self.input;
147151
length_check!(input, "Dictionary", self.min_length, self.max_length, output);
148152
Ok(output.into())

src/validators/typed_dict.rs

+29-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use ahash::AHashSet;
66

77
use crate::build_tools::py_schema_err;
88
use crate::build_tools::{is_strict, schema_or_config, schema_or_config_same, ExtraBehavior};
9-
use crate::errors::{mapping_valid_as_partial, LocItem};
9+
use crate::errors::LocItem;
1010
use crate::errors::{ErrorTypeDefaults, ValError, ValLineError, ValResult};
1111
use crate::input::BorrowInput;
1212
use crate::input::ConsumeIterator;
@@ -35,6 +35,7 @@ pub struct TypedDictValidator {
3535
extras_validator: Option<Box<CombinedValidator>>,
3636
strict: bool,
3737
loc_by_alias: bool,
38+
allow_partial: bool,
3839
}
3940

4041
impl BuildValidator for TypedDictValidator {
@@ -124,13 +125,14 @@ impl BuildValidator for TypedDictValidator {
124125
required,
125126
});
126127
}
127-
128+
let allow_partial = fields.iter().all(|f| !f.required);
128129
Ok(Self {
129130
fields,
130131
extra_behavior,
131132
extras_validator,
132133
strict,
133134
loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true),
135+
allow_partial,
134136
}
135137
.into())
136138
}
@@ -322,7 +324,7 @@ impl Validator for TypedDictValidator {
322324
})??;
323325
}
324326

325-
if errors.is_empty() || mapping_valid_as_partial(state, dict.last_key(), &errors) {
327+
if errors.is_empty() || self.valid_as_partial(state, dict.last_key(), &errors) {
326328
Ok(output_dict.to_object(py))
327329
} else {
328330
Err(ValError::LineErrors(errors))
@@ -333,3 +335,27 @@ impl Validator for TypedDictValidator {
333335
Self::EXPECTED_TYPE
334336
}
335337
}
338+
339+
impl TypedDictValidator {
340+
/// If we're in `allow_partial` mode, whether all errors occurred in the last value of the dict.
341+
fn valid_as_partial(
342+
&self,
343+
state: &ValidationState,
344+
opt_last_key: Option<impl Into<LocItem>>,
345+
errors: &[ValLineError],
346+
) -> bool {
347+
if !state.extra().allow_partial || !self.allow_partial {
348+
false
349+
} else if let Some(last_key) = opt_last_key.map(Into::into) {
350+
errors.iter().all(|error| {
351+
if let Some(loc_item) = error.first_loc_item() {
352+
loc_item == &last_key
353+
} else {
354+
false
355+
}
356+
})
357+
} else {
358+
false
359+
}
360+
}
361+
}

tests/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
backports.zoneinfo==0.2.1;python_version<"3.9"
22
coverage==7.6.1
33
dirty-equals==0.8.0
4+
inline-snapshot==0.13.3
45
hypothesis==6.111.2
56
# pandas doesn't offer prebuilt wheels for all versions and platforms we test in CI e.g. aarch64 musllinux
67
pandas==2.1.3; python_version >= "3.9" and python_version < "3.13" and implementation_name == "cpython" and platform_machine == 'x86_64'

0 commit comments

Comments
 (0)