Skip to content

Commit 3e5f948

Browse files
committed
begin refactoring ValLineError collection
1 parent 89bdee1 commit 3e5f948

File tree

4 files changed

+155
-54
lines changed

4 files changed

+155
-54
lines changed

src/errors/line_error.rs

+105
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,108 @@ impl ToPyObject for InputValue {
162162
}
163163
}
164164
}
165+
166+
pub struct LineErrorCollector {
167+
errors: Vec<ValLineError>,
168+
capacity: usize,
169+
}
170+
171+
impl LineErrorCollector {
172+
pub fn new() -> Self {
173+
Self {
174+
errors: Vec::new(),
175+
capacity: 0,
176+
}
177+
}
178+
179+
pub fn with_capacity(capacity: usize) -> Self {
180+
Self {
181+
// as this is used on the error pathway, avoid allocating until the first error
182+
errors: Vec::new(),
183+
capacity,
184+
}
185+
}
186+
187+
pub fn ensure_empty(self) -> ValResult<()> {
188+
if self.errors.is_empty() {
189+
Ok(())
190+
} else {
191+
Err(ValError::LineErrors(self.errors))
192+
}
193+
}
194+
195+
pub fn push(&mut self, error: ValLineError) {
196+
self.allocate_if_needed();
197+
self.errors.push(error);
198+
}
199+
200+
fn collect(&mut self, errors: Vec<ValLineError>) {
201+
self.allocate_if_needed();
202+
self.errors.extend(errors);
203+
}
204+
205+
fn allocate_if_needed(&mut self) {
206+
if self.errors.is_empty() && self.capacity > 0 {
207+
self.errors.reserve(self.capacity);
208+
}
209+
}
210+
}
211+
212+
/// Helper trait only implemented for `ValResult` to allow chaining of `collect_line_error`
213+
pub trait ValResultExt<T> {
214+
/// If `self` is an `Err`, collect the line errors into the `collector` and return the error.
215+
fn collect_line_errors(
216+
self,
217+
collector: &mut LineErrorCollector,
218+
location: impl Into<LocItem>,
219+
) -> Result<Option<T>, ValidationControlFlow>;
220+
}
221+
222+
impl<T> ValResultExt<T> for ValResult<T> {
223+
#[inline]
224+
fn collect_line_errors(
225+
self,
226+
collector: &mut LineErrorCollector,
227+
location: impl Into<LocItem>,
228+
) -> Result<Option<T>, ValidationControlFlow> {
229+
match self {
230+
Ok(value) => Ok(Some(value)),
231+
Err(ValError::LineErrors(line_errors)) => {
232+
extend_collector(line_errors, collector, location.into());
233+
Ok(None)
234+
}
235+
Err(ValError::InternalErr(err)) => Err(ValidationControlFlow::InternalErr(err)),
236+
Err(ValError::Omit) => Err(ValidationControlFlow::Omit),
237+
Err(ValError::UseDefault) => Err(ValidationControlFlow::UseDefault),
238+
}
239+
}
240+
}
241+
242+
#[cold]
243+
fn extend_collector(line_errors: Vec<ValLineError>, collector: &mut LineErrorCollector, location: LocItem) {
244+
collector.collect(
245+
line_errors
246+
.into_iter()
247+
.map(|line_error| line_error.with_outer_location(location.clone()))
248+
.collect(),
249+
);
250+
}
251+
252+
/// ValError, minus the LineErrors variant.
253+
///
254+
/// TODO: maybe rework ValError to contain this.
255+
pub enum ValidationControlFlow {
256+
InternalErr(PyErr),
257+
Omit,
258+
UseDefault,
259+
}
260+
261+
impl From<ValidationControlFlow> for ValError {
262+
fn from(control_flow: ValidationControlFlow) -> Self {
263+
match control_flow {
264+
ValidationControlFlow::InternalErr(err) => ValError::InternalErr(err),
265+
ValidationControlFlow::Omit => ValError::Omit,
266+
ValidationControlFlow::UseDefault => ValError::UseDefault,
267+
}
268+
}
269+
}

src/errors/mod.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ mod types;
66
mod validation_exception;
77
mod value_exception;
88

9-
pub use self::line_error::{InputValue, ToErrorValue, ValError, ValLineError, ValResult};
9+
pub use self::line_error::{
10+
InputValue, LineErrorCollector, ToErrorValue, ValError, ValLineError, ValResult, ValResultExt,
11+
ValidationControlFlow,
12+
};
1013
pub use self::location::LocItem;
1114
pub use self::types::{list_all_errors, ErrorType, ErrorTypeDefaults, Number};
1215
pub use self::validation_exception::ValidationError;

src/validators/dict.rs

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

55
use crate::build_tools::is_strict;
6-
use crate::errors::{LocItem, ValError, ValLineError, ValResult};
6+
use crate::errors::LineErrorCollector;
7+
use crate::errors::ValResultExt;
8+
use crate::errors::ValidationControlFlow;
9+
use crate::errors::{LocItem, ValError, ValResult};
710
use crate::input::BorrowInput;
811
use crate::input::ConsumeIterator;
912
use crate::input::{Input, ValidatedDict};
@@ -108,7 +111,7 @@ where
108111
type Output = ValResult<PyObject>;
109112
fn consume_iterator(self, iterator: impl Iterator<Item = ValResult<(Key, Value)>>) -> ValResult<PyObject> {
110113
let output = PyDict::new_bound(self.py);
111-
let mut errors: Vec<ValLineError> = Vec::new();
114+
let mut errors = LineErrorCollector::new();
112115

113116
for item_result in iterator {
114117
let (key, value) = item_result?;
@@ -124,28 +127,24 @@ where
124127
Err(ValError::Omit) => continue,
125128
Err(err) => return Err(err),
126129
};
127-
let output_value = match self.value_validator.validate(self.py, value.borrow_input(), self.state) {
128-
Ok(value) => Some(value),
129-
Err(ValError::LineErrors(line_errors)) => {
130-
for err in line_errors {
131-
errors.push(err.with_outer_location(key.clone()));
132-
}
133-
None
134-
}
135-
Err(ValError::Omit) => continue,
136-
Err(err) => return Err(err),
130+
let output_value = match self
131+
.value_validator
132+
.validate(self.py, value.borrow_input(), self.state)
133+
.collect_line_errors(&mut errors, key)
134+
{
135+
Ok(output_value) => output_value,
136+
Err(ValidationControlFlow::Omit) => continue,
137+
Err(err) => return Err(err.into()),
137138
};
138139
if let (Some(key), Some(value)) = (output_key, output_value) {
139140
output.set_item(key, value)?;
140141
}
141142
}
142143

143-
if errors.is_empty() {
144-
let input = self.input;
145-
length_check!(input, "Dictionary", self.min_length, self.max_length, output);
146-
Ok(output.into())
147-
} else {
148-
Err(ValError::LineErrors(errors))
149-
}
144+
errors.ensure_empty()?;
145+
146+
let input = self.input;
147+
length_check!(input, "Dictionary", self.min_length, self.max_length, output);
148+
Ok(output.into())
150149
}
151150
}

src/validators/model_fields.rs

+28-34
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use ahash::AHashSet;
77

88
use crate::build_tools::py_schema_err;
99
use crate::build_tools::{is_strict, schema_or_config_same, ExtraBehavior};
10+
use crate::errors::LineErrorCollector;
1011
use crate::errors::LocItem;
12+
use crate::errors::ValResultExt;
1113
use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValLineError, ValResult};
1214
use crate::input::ConsumeIterator;
1315
use crate::input::{BorrowInput, Input, ValidatedDict, ValidationMatch};
@@ -148,7 +150,8 @@ impl Validator for ModelFieldsValidator {
148150

149151
let model_dict = PyDict::new_bound(py);
150152
let mut model_extra_dict_op: Option<Bound<PyDict>> = None;
151-
let mut errors: Vec<ValLineError> = Vec::with_capacity(self.fields.len());
153+
let mut errors = LineErrorCollector::with_capacity(self.fields.len());
154+
152155
let mut fields_set_vec: Vec<Py<PyString>> = Vec::with_capacity(self.fields.len());
153156
let mut fields_set_count: usize = 0;
154157

@@ -165,22 +168,20 @@ impl Validator for ModelFieldsValidator {
165168
let state = &mut state.rebind_extra(|extra| extra.data = Some(model_dict.clone()));
166169

167170
for field in &self.fields {
168-
let op_key_value = match dict.get_item(&field.lookup_key) {
169-
Ok(v) => v,
170-
Err(ValError::LineErrors(line_errors)) => {
171-
for err in line_errors {
172-
errors.push(err.with_outer_location(&field.name));
173-
}
174-
continue;
175-
}
176-
Err(err) => return Err(err),
171+
let Some(op_key_value) = dict
172+
.get_item(&field.lookup_key)
173+
.collect_line_errors(&mut errors, &field.name)?
174+
else {
175+
continue;
177176
};
177+
178178
if let Some((lookup_path, value)) = op_key_value {
179179
if let Some(ref mut used_keys) = used_keys {
180180
// key is "used" whether or not validation passes, since we want to skip this key in
181181
// extra logic either way
182182
used_keys.insert(lookup_path.first_key());
183183
}
184+
184185
match field.validator.validate(py, value.borrow_input(), state) {
185186
Ok(value) => {
186187
model_dict.set_item(&field.name_py, value)?;
@@ -231,7 +232,7 @@ impl Validator for ModelFieldsValidator {
231232
struct ValidateToModelExtra<'a, 's, 'py> {
232233
py: Python<'py>,
233234
used_keys: AHashSet<&'a str>,
234-
errors: &'a mut Vec<ValLineError>,
235+
errors: &'a mut LineErrorCollector,
235236
fields_set_vec: &'a mut Vec<Py<PyString>>,
236237
extra_behavior: ExtraBehavior,
237238
extras_validator: Option<&'a CombinedValidator>,
@@ -287,17 +288,12 @@ impl Validator for ModelFieldsValidator {
287288
ExtraBehavior::Allow => {
288289
let py_key = either_str.as_py_string(self.py, self.state.cache_str());
289290
if let Some(validator) = self.extras_validator {
290-
match validator.validate(self.py, value, self.state) {
291-
Ok(value) => {
292-
model_extra_dict.set_item(&py_key, value)?;
293-
self.fields_set_vec.push(py_key.into());
294-
}
295-
Err(ValError::LineErrors(line_errors)) => {
296-
for err in line_errors {
297-
self.errors.push(err.with_outer_location(raw_key.clone()));
298-
}
299-
}
300-
Err(err) => return Err(err),
291+
if let Some(value) = validator
292+
.validate(self.py, value, self.state)
293+
.collect_line_errors(self.errors, raw_key.clone())?
294+
{
295+
model_extra_dict.set_item(&py_key, value)?;
296+
self.fields_set_vec.push(py_key.into());
301297
}
302298
} else {
303299
model_extra_dict.set_item(&py_key, value.to_object(self.py))?;
@@ -325,20 +321,18 @@ impl Validator for ModelFieldsValidator {
325321
}
326322
}
327323

328-
if !errors.is_empty() {
329-
Err(ValError::LineErrors(errors))
330-
} else {
331-
let fields_set = PySet::new_bound(py, &fields_set_vec)?;
332-
state.add_fields_set(fields_set_count);
324+
errors.ensure_empty()?;
333325

334-
// if we have extra=allow, but we didn't create a dict because we were validating
335-
// from attributes, set it now so __pydantic_extra__ is always a dict if extra=allow
336-
if matches!(self.extra_behavior, ExtraBehavior::Allow) && model_extra_dict_op.is_none() {
337-
model_extra_dict_op = Some(PyDict::new_bound(py));
338-
};
326+
let fields_set = PySet::new_bound(py, &fields_set_vec)?;
327+
state.add_fields_set(fields_set_count);
339328

340-
Ok((model_dict, model_extra_dict_op, fields_set).to_object(py))
341-
}
329+
// if we have extra=allow, but we didn't create a dict because we were validating
330+
// from attributes, set it now so __pydantic_extra__ is always a dict if extra=allow
331+
if matches!(self.extra_behavior, ExtraBehavior::Allow) && model_extra_dict_op.is_none() {
332+
model_extra_dict_op = Some(PyDict::new_bound(py));
333+
};
334+
335+
Ok((model_dict, model_extra_dict_op, fields_set).to_object(py))
342336
}
343337

344338
fn validate_assignment<'py>(

0 commit comments

Comments
 (0)