Skip to content

Commit 4760f38

Browse files
authored
Ensure ValidationInfo.field_name is correct on validator reuse (#1692)
1 parent 9a25aa6 commit 4760f38

File tree

15 files changed

+242
-121
lines changed

15 files changed

+242
-121
lines changed

python/pydantic_core/core_schema.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,7 +1954,7 @@ class NoInfoValidatorFunctionSchema(TypedDict):
19541954
class WithInfoValidatorFunctionSchema(TypedDict, total=False):
19551955
type: Required[Literal['with-info']]
19561956
function: Required[WithInfoValidatorFunction]
1957-
field_name: str
1957+
field_name: str # deprecated
19581958

19591959

19601960
ValidationFunction = Union[NoInfoValidatorFunctionSchema, WithInfoValidatorFunctionSchema]
@@ -2042,7 +2042,7 @@ def fn(v: bytes, info: core_schema.ValidationInfo) -> str:
20422042
return v.decode() + 'world'
20432043
20442044
func_schema = core_schema.with_info_before_validator_function(
2045-
function=fn, schema=core_schema.str_schema(), field_name='a'
2045+
function=fn, schema=core_schema.str_schema()
20462046
)
20472047
schema = core_schema.typed_dict_schema({'a': core_schema.typed_dict_field(func_schema)})
20482048
@@ -2052,13 +2052,20 @@ def fn(v: bytes, info: core_schema.ValidationInfo) -> str:
20522052
20532053
Args:
20542054
function: The validator function to call
2055-
field_name: The name of the field
2055+
field_name: The name of the field this validator is applied to, if any (deprecated)
20562056
schema: The schema to validate the output of the validator function
20572057
ref: optional unique identifier of the schema, used to reference the schema in other places
20582058
json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type
20592059
metadata: Any other information you want to include with the schema, not used by pydantic-core
20602060
serialization: Custom serialization schema
20612061
"""
2062+
if field_name is not None:
2063+
warnings.warn(
2064+
'The `field_name` argument on `with_info_before_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.',
2065+
DeprecationWarning,
2066+
stacklevel=2,
2067+
)
2068+
20622069
return _dict_not_none(
20632070
type='function-before',
20642071
function=_dict_not_none(type='with-info', function=function, field_name=field_name),
@@ -2140,7 +2147,7 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str:
21402147
return v + 'world'
21412148
21422149
func_schema = core_schema.with_info_after_validator_function(
2143-
function=fn, schema=core_schema.str_schema(), field_name='a'
2150+
function=fn, schema=core_schema.str_schema()
21442151
)
21452152
schema = core_schema.typed_dict_schema({'a': core_schema.typed_dict_field(func_schema)})
21462153
@@ -2151,11 +2158,18 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str:
21512158
Args:
21522159
function: The validator function to call after the schema is validated
21532160
schema: The schema to validate before the validator function
2154-
field_name: The name of the field this validators is applied to, if any
2161+
field_name: The name of the field this validator is applied to, if any (deprecated)
21552162
ref: optional unique identifier of the schema, used to reference the schema in other places
21562163
metadata: Any other information you want to include with the schema, not used by pydantic-core
21572164
serialization: Custom serialization schema
21582165
"""
2166+
if field_name is not None:
2167+
warnings.warn(
2168+
'The `field_name` argument on `with_info_after_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.',
2169+
DeprecationWarning,
2170+
stacklevel=2,
2171+
)
2172+
21592173
return _dict_not_none(
21602174
type='function-after',
21612175
function=_dict_not_none(type='with-info', function=function, field_name=field_name),
@@ -2187,7 +2201,7 @@ class NoInfoWrapValidatorFunctionSchema(TypedDict):
21872201
class WithInfoWrapValidatorFunctionSchema(TypedDict, total=False):
21882202
type: Required[Literal['with-info']]
21892203
function: Required[WithInfoWrapValidatorFunction]
2190-
field_name: str
2204+
field_name: str # deprecated
21912205

21922206

21932207
WrapValidatorFunction = Union[NoInfoWrapValidatorFunctionSchema, WithInfoWrapValidatorFunctionSchema]
@@ -2287,12 +2301,19 @@ def fn(
22872301
Args:
22882302
function: The validator function to call
22892303
schema: The schema to validate the output of the validator function
2290-
field_name: The name of the field this validators is applied to, if any
2304+
field_name: The name of the field this validator is applied to, if any (deprecated)
22912305
json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type
22922306
ref: optional unique identifier of the schema, used to reference the schema in other places
22932307
metadata: Any other information you want to include with the schema, not used by pydantic-core
22942308
serialization: Custom serialization schema
22952309
"""
2310+
if field_name is not None:
2311+
warnings.warn(
2312+
'The `field_name` argument on `with_info_wrap_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.',
2313+
DeprecationWarning,
2314+
stacklevel=2,
2315+
)
2316+
22962317
return _dict_not_none(
22972318
type='function-wrap',
22982319
function=_dict_not_none(type='with-info', function=function, field_name=field_name),
@@ -2379,12 +2400,19 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str:
23792400
23802401
Args:
23812402
function: The validator function to call
2382-
field_name: The name of the field this validators is applied to, if any
2403+
field_name: The name of the field this validator is applied to, if any (deprecated)
23832404
ref: optional unique identifier of the schema, used to reference the schema in other places
23842405
json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type
23852406
metadata: Any other information you want to include with the schema, not used by pydantic-core
23862407
serialization: Custom serialization schema
23872408
"""
2409+
if field_name is not None:
2410+
warnings.warn(
2411+
'The `field_name` argument on `with_info_plain_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.',
2412+
DeprecationWarning,
2413+
stacklevel=2,
2414+
)
2415+
23882416
return _dict_not_none(
23892417
type='function-plain',
23902418
function=_dict_not_none(type='with-info', function=function, field_name=field_name),

src/validators/arguments.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ impl Validator for ArgumentsValidator {
229229
}
230230
}
231231

232+
let state =
233+
&mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, parameter.name.as_str())));
234+
232235
match (pos_value, kw_value) {
233236
(Some(_), Some((_, kw_value))) => {
234237
errors.push(ValLineError::new_with_loc(

src/validators/dataclass.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuild
2424
struct Field {
2525
kw_only: bool,
2626
name: String,
27-
py_name: Py<PyString>,
27+
name_py: Py<PyString>,
2828
init: bool,
2929
init_only: bool,
3030
lookup_key_collection: LookupKeyCollection,
@@ -72,8 +72,8 @@ impl BuildValidator for DataclassArgsValidator {
7272
for field in fields_schema {
7373
let field = field.downcast::<PyDict>()?;
7474

75-
let py_name: Bound<'_, PyString> = field.get_as_req(intern!(py, "name"))?;
76-
let name: String = py_name.extract()?;
75+
let name_py: Bound<'_, PyString> = field.get_as_req(intern!(py, "name"))?;
76+
let name: String = name_py.extract()?;
7777

7878
let schema = field.get_as_req(intern!(py, "schema"))?;
7979

@@ -99,7 +99,7 @@ impl BuildValidator for DataclassArgsValidator {
9999
fields.push(Field {
100100
kw_only,
101101
name,
102-
py_name: py_name.into(),
102+
name_py: name_py.into(),
103103
lookup_key_collection,
104104
validator,
105105
init: field.get_as(intern!(py, "init"))?.unwrap_or(true),
@@ -163,13 +163,13 @@ impl Validator for DataclassArgsValidator {
163163

164164
macro_rules! set_item {
165165
($field:ident, $value:expr) => {{
166-
let py_name = $field.py_name.bind(py);
166+
let name_py = $field.name_py.bind(py);
167167
if $field.init_only {
168168
if let Some(ref mut init_only_args) = init_only_args {
169169
init_only_args.push($value);
170170
}
171171
} else {
172-
output_dict.set_item(py_name, $value)?;
172+
output_dict.set_item(name_py, $value)?;
173173
}
174174
}};
175175
}
@@ -214,6 +214,8 @@ impl Validator for DataclassArgsValidator {
214214
}
215215
let kw_value = kw_value.as_ref().map(|(path, value)| (path, value.borrow_input()));
216216

217+
let state = &mut state.rebind_extra(|extra| extra.field_name = Some(field.name_py.bind(py).clone()));
218+
217219
match (pos_value, kw_value) {
218220
// found both positional and keyword arguments, error
219221
(Some(_), Some((_, kw_value))) => {
@@ -404,11 +406,12 @@ impl Validator for DataclassArgsValidator {
404406
}
405407
}
406408

407-
match field.validator.validate(
408-
py,
409-
field_value,
410-
&mut state.rebind_extra(|extra| extra.data = Some(data_dict.clone())),
411-
) {
409+
let state = &mut state.rebind_extra(|extra| {
410+
extra.data = Some(data_dict.clone());
411+
extra.field_name = Some(field.name_py.bind(py).clone());
412+
});
413+
414+
match field.validator.validate(py, field_value, state) {
412415
Ok(output) => ok(output),
413416
Err(ValError::LineErrors(line_errors)) => {
414417
let errors = line_errors

src/validators/function.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,13 @@ impl FunctionBeforeValidator {
100100
state: &'s mut ValidationState<'_, 'py>,
101101
) -> ValResult<PyObject> {
102102
let r = if self.info_arg {
103-
let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone());
103+
let field_name = state
104+
.extra()
105+
.field_name
106+
.clone()
107+
.map(Bound::unbind)
108+
.or_else(|| self.field_name.clone());
109+
let info = ValidationInfo::new(py, state.extra(), &self.config, field_name);
104110
self.func.call1(py, (input.to_object(py)?, info))
105111
} else {
106112
self.func.call1(py, (input.to_object(py)?,))
@@ -169,7 +175,13 @@ impl FunctionAfterValidator {
169175
) -> ValResult<PyObject> {
170176
let v = call(input, state)?;
171177
let r = if self.info_arg {
172-
let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone());
178+
let field_name = state
179+
.extra()
180+
.field_name
181+
.clone()
182+
.map(Bound::unbind)
183+
.or_else(|| self.field_name.clone());
184+
let info = ValidationInfo::new(py, state.extra(), &self.config, field_name);
173185
self.func.call1(py, (v, info))
174186
} else {
175187
self.func.call1(py, (v,))
@@ -258,7 +270,13 @@ impl Validator for FunctionPlainValidator {
258270
state: &mut ValidationState<'_, 'py>,
259271
) -> ValResult<PyObject> {
260272
let r = if self.info_arg {
261-
let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone());
273+
let field_name = state
274+
.extra()
275+
.field_name
276+
.clone()
277+
.map(Bound::unbind)
278+
.or_else(|| self.field_name.clone());
279+
let info = ValidationInfo::new(py, state.extra(), &self.config, field_name);
262280
self.func.call1(py, (input.to_object(py)?, info))
263281
} else {
264282
self.func.call1(py, (input.to_object(py)?,))
@@ -322,7 +340,13 @@ impl FunctionWrapValidator {
322340
state: &mut ValidationState<'_, 'py>,
323341
) -> ValResult<PyObject> {
324342
let r = if self.info_arg {
325-
let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone());
343+
let field_name = state
344+
.extra()
345+
.field_name
346+
.clone()
347+
.map(Bound::unbind)
348+
.or_else(|| self.field_name.clone());
349+
let info = ValidationInfo::new(py, state.extra(), &self.config, field_name);
326350
self.func.call1(py, (input.to_object(py)?, handler, info))
327351
} else {
328352
self.func.call1(py, (input.to_object(py)?, handler))

src/validators/generator.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ impl InternalValidator {
276276
data: self.data.as_ref().map(|data| data.bind(py).clone()),
277277
strict: self.strict,
278278
from_attributes: self.from_attributes,
279+
field_name: Some(PyString::new(py, field_name)),
279280
context: self.context.as_ref().map(|data| data.bind(py)),
280281
self_instance: self.self_instance.as_ref().map(|data| data.bind(py)),
281282
cache_str: self.cache_str,
@@ -313,6 +314,7 @@ impl InternalValidator {
313314
data: self.data.as_ref().map(|data| data.bind(py).clone()),
314315
strict: self.strict,
315316
from_attributes: self.from_attributes,
317+
field_name: None,
316318
context: self.context.as_ref().map(|data| data.bind(py)),
317319
self_instance: self.self_instance.as_ref().map(|data| data.bind(py)),
318320
cache_str: self.cache_str,

src/validators/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ impl SchemaValidator {
311311
data: None,
312312
strict,
313313
from_attributes,
314+
field_name: Some(PyString::new(py, field_name)),
314315
context,
315316
self_instance: None,
316317
cache_str: self.cache_str,
@@ -337,6 +338,7 @@ impl SchemaValidator {
337338
data: None,
338339
strict,
339340
from_attributes: None,
341+
field_name: None,
340342
context,
341343
self_instance: None,
342344
cache_str: self.cache_str,
@@ -678,6 +680,8 @@ pub struct Extra<'a, 'py> {
678680
pub from_attributes: Option<bool>,
679681
/// context used in validator functions
680682
pub context: Option<&'a Bound<'py, PyAny>>,
683+
/// The name of the field being validated, if applicable
684+
pub field_name: Option<Bound<'py, PyString>>,
681685
/// This is an instance of the model or dataclass being validated, when validation is performed from `__init__`
682686
self_instance: Option<&'a Bound<'py, PyAny>>,
683687
/// Whether to use a cache of short strings to accelerate python string construction
@@ -705,6 +709,7 @@ impl<'a, 'py> Extra<'a, 'py> {
705709
data: None,
706710
strict,
707711
from_attributes,
712+
field_name: None,
708713
context,
709714
self_instance,
710715
cache_str,
@@ -721,6 +726,7 @@ impl Extra<'_, '_> {
721726
data: self.data.clone(),
722727
strict: Some(true),
723728
from_attributes: self.from_attributes,
729+
field_name: self.field_name.clone(),
724730
context: self.context,
725731
self_instance: self.self_instance,
726732
cache_str: self.cache_str,

src/validators/model.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ impl Validator for ModelValidator {
202202
field_name.to_string(),
203203
))
204204
} else {
205+
let state = &mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, ROOT_FIELD)));
205206
let output = self.validator.validate(py, field_value, state)?;
206207

207208
force_setattr(py, model, intern!(py, ROOT_FIELD), output)?;
@@ -255,9 +256,11 @@ impl ModelValidator {
255256
// we need to set `self_instance` to None for nested validators as we don't want to operate on self_instance
256257
// anymore
257258
let state = &mut state.rebind_extra(|extra| extra.self_instance = None);
258-
let output = self.validator.validate(py, input, state)?;
259259

260260
if self.root_model {
261+
let state = &mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, ROOT_FIELD)));
262+
let output = self.validator.validate(py, input, state)?;
263+
261264
let fields_set = if input.as_python().is_some_and(|py_input| py_input.is(&self.undefined)) {
262265
PySet::empty(py)?
263266
} else {
@@ -266,6 +269,8 @@ impl ModelValidator {
266269
force_setattr(py, self_instance, intern!(py, DUNDER_FIELDS_SET_KEY), &fields_set)?;
267270
force_setattr(py, self_instance, intern!(py, ROOT_FIELD), &output)?;
268271
} else {
272+
let output = self.validator.validate(py, input, state)?;
273+
269274
let (model_dict, model_extra, fields_set): (Bound<PyAny>, Bound<PyAny>, Bound<PyAny>) =
270275
output.extract(py)?;
271276
set_model_attrs(self_instance, &model_dict, &model_extra, &fields_set)?;
@@ -294,11 +299,12 @@ impl ModelValidator {
294299
}
295300
}
296301

297-
let output = self.validator.validate(py, input, state)?;
298-
299302
let instance = create_class(self.class.bind(py))?;
300303

301304
if self.root_model {
305+
let state = &mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, ROOT_FIELD)));
306+
let output = self.validator.validate(py, input, state)?;
307+
302308
let fields_set = if input.as_python().is_some_and(|py_input| py_input.is(&self.undefined)) {
303309
PySet::empty(py)?
304310
} else {
@@ -307,6 +313,8 @@ impl ModelValidator {
307313
force_setattr(py, &instance, intern!(py, DUNDER_FIELDS_SET_KEY), &fields_set)?;
308314
force_setattr(py, &instance, intern!(py, ROOT_FIELD), output)?;
309315
} else {
316+
let output = self.validator.validate(py, input, state)?;
317+
310318
let (model_dict, model_extra, val_fields_set): (Bound<PyAny>, Bound<PyAny>, Bound<PyAny>) =
311319
output.extract(py)?;
312320
let fields_set = existing_fields_set.unwrap_or(&val_fields_set);

src/validators/model_fields.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ impl Validator for ModelFieldsValidator {
197197
// extra logic either way
198198
used_keys.insert(lookup_path.first_key());
199199
}
200+
201+
let state =
202+
&mut state.rebind_extra(|extra| extra.field_name = Some(field.name_py.bind(py).clone()));
203+
200204
match field.validator.validate(py, value.borrow_input(), state) {
201205
Ok(value) => {
202206
model_dict.set_item(&field.name_py, value)?;
@@ -422,6 +426,8 @@ impl Validator for ModelFieldsValidator {
422426
));
423427
}
424428

429+
let state = &mut state.rebind_extra(|extra| extra.field_name = Some(field.name_py.bind(py).clone()));
430+
425431
prepare_result(field.validator.validate(py, field_value, state))?
426432
} else {
427433
// Handle extra (unknown) field

0 commit comments

Comments
 (0)