Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit e87ba01

Browse files
Viicosdavidhewitt
andauthored
Do not call default factories taking the data argument if a validation error already occurred (#1623)
Co-authored-by: David Hewitt <mail@davidhewitt.dev>
1 parent 70bd6f9 commit e87ba01

File tree

9 files changed

+98
-8
lines changed

9 files changed

+98
-8
lines changed

‎python/pydantic_core/core_schema.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4236,6 +4236,7 @@ def definition_reference_schema(
42364236
'model_attributes_type',
42374237
'dataclass_type',
42384238
'dataclass_exact_type',
4239+
'default_factory_not_called',
42394240
'none_required',
42404241
'greater_than',
42414242
'greater_than_equal',

‎src/errors/types.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ error_types! {
196196
class_name: {ctx_type: String, ctx_fn: field_from_context},
197197
},
198198
// ---------------------
199+
// Default factory not called (happens when there's already an error and the factory takes data)
200+
DefaultFactoryNotCalled {},
201+
// ---------------------
199202
// None errors
200203
NoneRequired {},
201204
// ---------------------
@@ -493,6 +496,7 @@ impl ErrorType {
493496
Self::ModelAttributesType {..} => "Input should be a valid dictionary or object to extract fields from",
494497
Self::DataclassType {..} => "Input should be a dictionary or an instance of {class_name}",
495498
Self::DataclassExactType {..} => "Input should be an instance of {class_name}",
499+
Self::DefaultFactoryNotCalled {..} => "The default factory uses validated data, but at least one validation error occurred",
496500
Self::NoneRequired {..} => "Input should be None",
497501
Self::GreaterThan {..} => "Input should be greater than {gt}",
498502
Self::GreaterThanEqual {..} => "Input should be greater than or equal to {ge}",

‎src/errors/validation_exception.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,8 @@ impl PyLineError {
528528
};
529529
write!(output, " {message} [type={}", self.error_type.type_string())?;
530530

531-
if !hide_input {
531+
// special case: don't show input for DefaultFactoryNotCalled errors - there is no valid input
532+
if !hide_input && !matches!(self.error_type, ErrorType::DefaultFactoryNotCalled { .. }) {
532533
let input_value = self.input_value.bind(py);
533534
let input_str = safe_repr(input_value);
534535
write!(output, ", input_value=")?;

‎src/validators/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ fn build_validator_inner(
668668
pub struct Extra<'a, 'py> {
669669
/// Validation mode
670670
pub input_type: InputType,
671-
/// This is used as the `data` kwargs to validator functions
671+
/// This is used as the `data` kwargs to validator functions and default factories (if they accept the argument)
672672
pub data: Option<Bound<'py, PyDict>>,
673673
/// whether we're in strict or lax mode
674674
pub strict: Option<bool>,

‎src/validators/model_fields.rs‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,18 @@ impl Validator for ModelFieldsValidator {
210210
fields_set_vec.push(field.name_py.clone_ref(py));
211211
fields_set_count += 1;
212212
}
213-
Err(ValError::Omit) => continue,
214-
Err(ValError::LineErrors(line_errors)) => {
215-
for err in line_errors {
216-
errors.push(lookup_path.apply_error_loc(err, self.loc_by_alias, &field.name));
213+
Err(e) => {
214+
state.has_field_error = true;
215+
match e {
216+
ValError::Omit => continue,
217+
ValError::LineErrors(line_errors) => {
218+
for err in line_errors {
219+
errors.push(lookup_path.apply_error_loc(err, self.loc_by_alias, &field.name));
220+
}
221+
}
222+
err => return Err(err),
217223
}
218224
}
219-
Err(err) => return Err(err),
220225
}
221226
continue;
222227
}

‎src/validators/validation_state.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ pub struct ValidationState<'a, 'py> {
2626
pub fields_set_count: Option<usize>,
2727
// True if `allow_partial=true` and we're validating the last element of a sequence or mapping.
2828
pub allow_partial: PartialMode,
29+
// Whether at least one field had a validation error. This is used in the context of structured types
30+
// (models, dataclasses, etc), where we need to know if a validation error occurred before calling
31+
// a default factory that takes the validated data.
32+
pub has_field_error: bool,
2933
// deliberately make Extra readonly
3034
extra: Extra<'a, 'py>,
3135
}
@@ -37,6 +41,7 @@ impl<'a, 'py> ValidationState<'a, 'py> {
3741
exactness: None,
3842
fields_set_count: None,
3943
allow_partial,
44+
has_field_error: false,
4045
extra,
4146
}
4247
}

‎src/validators/with_default.rs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use pyo3::PyVisit;
1111
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};
1212
use crate::build_tools::py_schema_err;
1313
use crate::build_tools::schema_or_config_same;
14-
use crate::errors::{LocItem, ValError, ValResult};
14+
use crate::errors::{ErrorTypeDefaults,LocItem, ValError, ValResult};
1515
use crate::input::Input;
1616
use crate::py_gc::PyGcTraverse;
1717
use crate::tools::SchemaDict;
@@ -182,6 +182,18 @@ impl Validator for WithDefaultValidator {
182182
outer_loc: Option<impl Into<LocItem>>,
183183
state: &mut ValidationState<'_, 'py>,
184184
) -> ValResult<Option<Py<PyAny>>> {
185+
if matches!(self.default, DefaultType::DefaultFactory(_, true)) && state.has_field_error {
186+
// The default factory might use data from fields that failed to validate, and this results
187+
// in an unhelpul error.
188+
let mut err = ValError::new(
189+
ErrorTypeDefaults::DefaultFactoryNotCalled,
190+
PydanticUndefinedType::new(py).into_bound(py).into_any(),
191+
);
192+
if let Some(outer_loc) = outer_loc {
193+
err = err.with_outer_location(outer_loc);
194+
}
195+
return Err(err);
196+
}
185197
match self.default.default_value(py, state.extra().data.as_ref())? {
186198
Some(stored_dft) => {
187199
let dft: Py<PyAny> = if self.copy_default {

‎tests/test_errors.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ def f(input_value, info):
267267
('model_attributes_type', 'Input should be a valid dictionary or object to extract fields from', None),
268268
('dataclass_exact_type', 'Input should be an instance of Foobar', {'class_name': 'Foobar'}),
269269
('dataclass_type', 'Input should be a dictionary or an instance of Foobar', {'class_name': 'Foobar'}),
270+
(
271+
'default_factory_not_called',
272+
'The default factory uses validated data, but at least one validation error occurred',
273+
None,
274+
),
270275
('missing', 'Field required', None),
271276
('frozen_field', 'Field is frozen', None),
272277
('frozen_instance', 'Instance is frozen', None),

‎tests/validators/test_with_default.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from pydantic_core import (
1010
ArgsKwargs,
11+
PydanticUndefined,
1112
PydanticUseDefault,
1213
SchemaError,
1314
SchemaValidator,
@@ -819,3 +820,59 @@ def _raise(ex: Exception) -> None:
819820
v.validate_python(input_value)
820821

821822
assert exc_info.value.errors(include_url=False, include_context=False) == expected
823+
824+
825+
def test_default_factory_not_called_if_existing_error(pydantic_version) -> None:
826+
class Test:
827+
def __init__(self, a: int, b: int):
828+
self.a = a
829+
self.b = b
830+
831+
schema = core_schema.model_schema(
832+
cls=Test,
833+
schema=core_schema.model_fields_schema(
834+
computed_fields=[],
835+
fields={
836+
'a': core_schema.model_field(
837+
schema=core_schema.int_schema(),
838+
),
839+
'b': core_schema.model_field(
840+
schema=core_schema.with_default_schema(
841+
schema=core_schema.int_schema(),
842+
default_factory=lambda data: data['a'],
843+
default_factory_takes_data=True,
844+
),
845+
),
846+
},
847+
),
848+
)
849+
850+
v = SchemaValidator(schema)
851+
with pytest.raises(ValidationError) as e:
852+
v.validate_python({'a': 'not_an_int'})
853+
854+
assert e.value.errors(include_url=False) == [
855+
{
856+
'type': 'int_parsing',
857+
'loc': ('a',),
858+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
859+
'input': 'not_an_int',
860+
},
861+
{
862+
'input': PydanticUndefined,
863+
'loc': ('b',),
864+
'msg': 'The default factory uses validated data, but at least one validation error occurred',
865+
'type': 'default_factory_not_called',
866+
},
867+
]
868+
869+
assert (
870+
str(e.value)
871+
== f"""2 validation errors for Test
872+
a
873+
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='not_an_int', input_type=str]
874+
For further information visit https://errors.pydantic.dev/{pydantic_version}/v/int_parsing
875+
b
876+
The default factory uses validated data, but at least one validation error occurred [type=default_factory_not_called]
877+
For further information visit https://errors.pydantic.dev/{pydantic_version}/v/default_factory_not_called"""
878+
)

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /