-
Notifications
You must be signed in to change notification settings - Fork 280
Do not call default factories taking the data argument if a validation error already occurred #1623
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -191,13 +191,18 @@ impl Validator for ModelFieldsValidator { | |
fields_set_vec.push(field.name_py.clone_ref(py)); | ||
fields_set_count += 1; | ||
} | ||
Err(ValError::Omit) => continue, | ||
Err(ValError::LineErrors(line_errors)) => { | ||
for err in line_errors { | ||
errors.push(lookup_path.apply_error_loc(err, self.loc_by_alias, &field.name)); | ||
Err(e) => { | ||
state.has_field_error = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to track this on the state? Should we adjust the algorithm to just never call (We could go further and rearrange things a bit so that all default values only get filled in after the whole input is processed, there is some overlap with #1620 🤔) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It's only relevant for default factories taking the data argument, so if you just have a "static" default it shouldn't matter. And because the logic to call the default value is nested under the current field validator, I can't really "influence" the logic from the model field. This state approach doesn't look right, but I couldn't find a better way to do so 🤔
Would this allow using other field values independently from the field order? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I see. I missed the default factories guarantee field order; this makes a lot of sense. Because of the existence of ... but I do worry about when this flag is reset. Currently once you set it it's true for the rest of the validation. This seems problematic in unions, for example. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed this is one of the reasons it doesn't look right; however, for unions I think it should be alright, as there's a boundary in the validation error. The error we are handling here is the validation error of the field. If this field is defined like: class Model(BaseModel):
a: int | str And a validation error happens on Edit: Actually it might be wrong if we have: class Model1(BaseModel):
a: int
class Model2(BaseModel): ...
class Model(BaseModel):
m: Model1 | Model2 if validation fails for Now the question is: are unions the only concern? If so, we can make sure we somehow reset this state value on union validation, but there might be other concerns apart from unions :/ |
||
match e { | ||
ValError::Omit => continue, | ||
ValError::LineErrors(line_errors) => { | ||
for err in line_errors { | ||
errors.push(lookup_path.apply_error_loc(err, self.loc_by_alias, &field.name)); | ||
} | ||
} | ||
err => return Err(err), | ||
} | ||
} | ||
Err(err) => return Err(err), | ||
} | ||
continue; | ||
} | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -9,7 +9,7 @@ use pyo3::PyVisit; | |||||||||||||||||||||||||||
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; | ||||||||||||||||||||||||||||
use crate::build_tools::py_schema_err; | ||||||||||||||||||||||||||||
use crate::build_tools::schema_or_config_same; | ||||||||||||||||||||||||||||
use crate::errors::{LocItem, ValError, ValResult}; | ||||||||||||||||||||||||||||
use crate::errors::{ErrorTypeDefaults, LocItem, ValError, ValResult}; | ||||||||||||||||||||||||||||
use crate::input::Input; | ||||||||||||||||||||||||||||
use crate::py_gc::PyGcTraverse; | ||||||||||||||||||||||||||||
use crate::tools::SchemaDict; | ||||||||||||||||||||||||||||
|
@@ -180,6 +180,11 @@ impl Validator for WithDefaultValidator { | |||||||||||||||||||||||||||
outer_loc: Option<impl Into<LocItem>>, | ||||||||||||||||||||||||||||
state: &mut ValidationState<'_, 'py>, | ||||||||||||||||||||||||||||
) -> ValResult<Option<PyObject>> { | ||||||||||||||||||||||||||||
if matches!(self.default, DefaultType::DefaultFactory(_, true)) && state.has_field_error { | ||||||||||||||||||||||||||||
// The default factory might use data from fields that failed to validate, and this results | ||||||||||||||||||||||||||||
// in an unhelpul error. | ||||||||||||||||||||||||||||
return Err(ValError::new(ErrorTypeDefaults::DefaultFactoryNotCalled, input)); | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit annoying because I don't have access to any input inside the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, the current behavior is a bit weird: class Model(BaseModel):
a: int
b: int
Model(b="fails")
"""
pydantic_core._pydantic_core.ValidationError: 2 validation errors for Model
a
Field required [type=missing, input_value={'b': 'fails'}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.11/v/missing
b
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='fails', input_type=str]
For further information visit https://errors.pydantic.dev/2.11/v/int_parsing
""" For pydantic-core/src/validators/model_fields.rs Lines 205 to 217 in 164b9ff
So perhaps we could just use Err(ValError::new(ErrorTypeDefaults::DefaultFactoryNotCalled, &self.undefined)) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm interesting, I wonder if the error for maybe we could special-case
? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll see if this can be done without too much special casing, I agree it would be better. |
||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
match self.default.default_value(py, state.extra().data.as_ref())? { | ||||||||||||||||||||||||||||
Some(stored_dft) => { | ||||||||||||||||||||||||||||
let dft: Py<PyAny> = if self.copy_default { | ||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same needs to be done for DCs/TDs