From c0f01dfe82611d573495dfab0783ceff4b1ede8c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 15 Apr 2025 17:57:25 +0000 Subject: [PATCH 1/9] Pass field_name to validators through ValidationState so functions are called with correct ValidationInfo.field_name --- src/validators/dataclass.rs | 25 +++++---- src/validators/function.rs | 32 ++++++++++-- src/validators/generator.rs | 2 + src/validators/mod.rs | 6 +++ src/validators/model_fields.rs | 6 +++ src/validators/typed_dict.rs | 3 ++ tests/validators/test_function.py | 87 +++++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 15 deletions(-) diff --git a/src/validators/dataclass.rs b/src/validators/dataclass.rs index e518757f2..c74d789ec 100644 --- a/src/validators/dataclass.rs +++ b/src/validators/dataclass.rs @@ -24,7 +24,7 @@ use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuild struct Field { kw_only: bool, name: String, - py_name: Py, + name_py: Py, init: bool, init_only: bool, lookup_key_collection: LookupKeyCollection, @@ -72,8 +72,8 @@ impl BuildValidator for DataclassArgsValidator { for field in fields_schema { let field = field.downcast::()?; - let py_name: Bound<'_, PyString> = field.get_as_req(intern!(py, "name"))?; - let name: String = py_name.extract()?; + let name_py: Bound<'_, PyString> = field.get_as_req(intern!(py, "name"))?; + let name: String = name_py.extract()?; let schema = field.get_as_req(intern!(py, "schema"))?; @@ -99,7 +99,7 @@ impl BuildValidator for DataclassArgsValidator { fields.push(Field { kw_only, name, - py_name: py_name.into(), + name_py: name_py.into(), lookup_key_collection, validator, init: field.get_as(intern!(py, "init"))?.unwrap_or(true), @@ -163,13 +163,13 @@ impl Validator for DataclassArgsValidator { macro_rules! set_item { ($field:ident, $value:expr) => {{ - let py_name = $field.py_name.bind(py); + let name_py = $field.name_py.bind(py); if $field.init_only { if let Some(ref mut init_only_args) = init_only_args { init_only_args.push($value); } } else { - output_dict.set_item(py_name, $value)?; + output_dict.set_item(name_py, $value)?; } }}; } @@ -214,6 +214,8 @@ impl Validator for DataclassArgsValidator { } let kw_value = kw_value.as_ref().map(|(path, value)| (path, value.borrow_input())); + let state = &mut state.rebind_extra(|extra| extra.field_name = Some(field.name_py.bind(py).clone())); + match (pos_value, kw_value) { // found both positional and keyword arguments, error (Some(_), Some((_, kw_value))) => { @@ -404,11 +406,12 @@ impl Validator for DataclassArgsValidator { } } - match field.validator.validate( - py, - field_value, - &mut state.rebind_extra(|extra| extra.data = Some(data_dict.clone())), - ) { + let state = &mut state.rebind_extra(|extra| { + extra.data = Some(data_dict.clone()); + extra.field_name = Some(field.name_py.bind(py).clone()); + }); + + match field.validator.validate(py, field_value, state) { Ok(output) => ok(output), Err(ValError::LineErrors(line_errors)) => { let errors = line_errors diff --git a/src/validators/function.rs b/src/validators/function.rs index 6f0eea7f4..584861305 100644 --- a/src/validators/function.rs +++ b/src/validators/function.rs @@ -100,7 +100,13 @@ impl FunctionBeforeValidator { state: &'s mut ValidationState<'_, 'py>, ) -> ValResult { let r = if self.info_arg { - let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone()); + let field_name = state + .extra() + .field_name + .clone() + .map(Bound::unbind) + .or(self.field_name.clone()); + let info = ValidationInfo::new(py, state.extra(), &self.config, field_name); self.func.call1(py, (input.to_object(py)?, info)) } else { self.func.call1(py, (input.to_object(py)?,)) @@ -169,7 +175,13 @@ impl FunctionAfterValidator { ) -> ValResult { let v = call(input, state)?; let r = if self.info_arg { - let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone()); + let field_name = state + .extra() + .field_name + .clone() + .map(Bound::unbind) + .or(self.field_name.clone()); + let info = ValidationInfo::new(py, state.extra(), &self.config, field_name); self.func.call1(py, (v, info)) } else { self.func.call1(py, (v,)) @@ -258,7 +270,13 @@ impl Validator for FunctionPlainValidator { state: &mut ValidationState<'_, 'py>, ) -> ValResult { let r = if self.info_arg { - let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone()); + let field_name = state + .extra() + .field_name + .clone() + .map(Bound::unbind) + .or(self.field_name.clone()); + let info = ValidationInfo::new(py, state.extra(), &self.config, field_name); self.func.call1(py, (input.to_object(py)?, info)) } else { self.func.call1(py, (input.to_object(py)?,)) @@ -322,7 +340,13 @@ impl FunctionWrapValidator { state: &mut ValidationState<'_, 'py>, ) -> ValResult { let r = if self.info_arg { - let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone()); + let field_name = state + .extra() + .field_name + .clone() + .map(Bound::unbind) + .or(self.field_name.clone()); + let info = ValidationInfo::new(py, state.extra(), &self.config, field_name); self.func.call1(py, (input.to_object(py)?, handler, info)) } else { self.func.call1(py, (input.to_object(py)?, handler)) diff --git a/src/validators/generator.rs b/src/validators/generator.rs index bc0cbd82a..12d76b3be 100644 --- a/src/validators/generator.rs +++ b/src/validators/generator.rs @@ -276,6 +276,7 @@ impl InternalValidator { data: self.data.as_ref().map(|data| data.bind(py).clone()), strict: self.strict, from_attributes: self.from_attributes, + field_name: Some(PyString::new(py, field_name)), context: self.context.as_ref().map(|data| data.bind(py)), self_instance: self.self_instance.as_ref().map(|data| data.bind(py)), cache_str: self.cache_str, @@ -313,6 +314,7 @@ impl InternalValidator { data: self.data.as_ref().map(|data| data.bind(py).clone()), strict: self.strict, from_attributes: self.from_attributes, + field_name: None, context: self.context.as_ref().map(|data| data.bind(py)), self_instance: self.self_instance.as_ref().map(|data| data.bind(py)), cache_str: self.cache_str, diff --git a/src/validators/mod.rs b/src/validators/mod.rs index f105e1854..cd166b61b 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -311,6 +311,7 @@ impl SchemaValidator { data: None, strict, from_attributes, + field_name: Some(PyString::new(py, field_name)), context, self_instance: None, cache_str: self.cache_str, @@ -337,6 +338,7 @@ impl SchemaValidator { data: None, strict, from_attributes: None, + field_name: None, context, self_instance: None, cache_str: self.cache_str, @@ -678,6 +680,8 @@ pub struct Extra<'a, 'py> { pub from_attributes: Option, /// context used in validator functions pub context: Option<&'a Bound<'py, PyAny>>, + /// The name of the field being validated, if applicable + pub field_name: Option>, /// This is an instance of the model or dataclass being validated, when validation is performed from `__init__` self_instance: Option<&'a Bound<'py, PyAny>>, /// Whether to use a cache of short strings to accelerate python string construction @@ -705,6 +709,7 @@ impl<'a, 'py> Extra<'a, 'py> { data: None, strict, from_attributes, + field_name: None, context, self_instance, cache_str, @@ -721,6 +726,7 @@ impl Extra<'_, '_> { data: self.data.clone(), strict: Some(true), from_attributes: self.from_attributes, + field_name: self.field_name.clone(), context: self.context, self_instance: self.self_instance, cache_str: self.cache_str, diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index b87f671ab..10d13d7b8 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -197,6 +197,10 @@ impl Validator for ModelFieldsValidator { // extra logic either way used_keys.insert(lookup_path.first_key()); } + + let state = + &mut state.rebind_extra(|extra| extra.field_name = Some(field.name_py.bind(py).clone())); + match field.validator.validate(py, value.borrow_input(), state) { Ok(value) => { model_dict.set_item(&field.name_py, value)?; @@ -422,6 +426,8 @@ impl Validator for ModelFieldsValidator { )); } + let state = &mut state.rebind_extra(|extra| extra.field_name = Some(field.name_py.bind(py).clone())); + prepare_result(field.validator.validate(py, field_value, state))? } else { // Handle extra (unknown) field diff --git a/src/validators/typed_dict.rs b/src/validators/typed_dict.rs index c9c122791..eb4394e39 100644 --- a/src/validators/typed_dict.rs +++ b/src/validators/typed_dict.rs @@ -218,6 +218,9 @@ impl Validator for TypedDictValidator { true => allow_partial, false => false.into(), }; + let state = + &mut state.rebind_extra(|extra| extra.field_name = Some(field.name_py.bind(py).clone())); + match field.validator.validate(py, value.borrow_input(), state) { Ok(value) => { output_dict.set_item(&field.name_py, value)?; diff --git a/tests/validators/test_function.py b/tests/validators/test_function.py index 562687268..07aaa9d87 100644 --- a/tests/validators/test_function.py +++ b/tests/validators/test_function.py @@ -2,6 +2,7 @@ import platform import re from copy import deepcopy +from dataclasses import dataclass from typing import Any import pytest @@ -662,6 +663,36 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: assert v.validate_python({'x': b'foo'}).x == 'input: foo' +def test_model_field_validator_reuse() -> None: + class Model: + x: str + y: str + + def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: + return f'{info.field_name}: {input_value}' + + # When a type alias with a validator function is used on multiple fields, + # its core schema is only generated once (with the first field_name) and reused. + # See https://github.com/pydantic/pydantic/issues/11737 + validator = core_schema.with_info_plain_validator_function(f, field_name='x') + + v = SchemaValidator( + core_schema.model_schema( + Model, + core_schema.model_fields_schema( + { + 'x': core_schema.model_field(validator), + 'y': core_schema.model_field(validator), + } + ), + ) + ) + + m = v.validate_python({'x': 'foo', 'y': 'bar'}) + assert m.x == 'x: foo' + assert m.y == 'y: bar' + + def test_model_field_wrap_validator() -> None: class Model: x: str @@ -821,6 +852,62 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: assert info_stuff == {'field_name': 'c', 'data': {'a': 1}} +def test_typed_dict_validator_reuse() -> None: + def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: + return f'{info.field_name}: {input_value}' + + # When a type alias with a validator function is used on multiple fields, + # its core schema is only generated once (with the first field_name) and reused. + # See https://github.com/pydantic/pydantic/issues/11737 + validator = core_schema.with_info_plain_validator_function(f, field_name='x') + + v = SchemaValidator( + core_schema.typed_dict_schema( + { + 'x': core_schema.model_field(validator), + 'y': core_schema.model_field(validator), + } + ) + ) + + data = v.validate_python({'x': 'foo', 'y': 'bar'}) + assert data['x'] == 'x: foo' + assert data['y'] == 'y: bar' + + +def test_dataclass_validator_reuse() -> None: + @dataclass + class Model: + x: str + y: str + + def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: + return f'{info.field_name}: {input_value}' + + # When a type alias with a validator function is used on multiple fields, + # its core schema is only generated once (with the first field_name) and reused. + # See https://github.com/pydantic/pydantic/issues/11737 + validator = core_schema.with_info_plain_validator_function(f, field_name='x') + + v = SchemaValidator( + core_schema.dataclass_schema( + Model, + core_schema.dataclass_args_schema( + 'Model', + [ + core_schema.dataclass_field(name='x', schema=validator), + core_schema.dataclass_field(name='y', schema=validator), + ], + ), + ['x', 'y'], + ) + ) + + m = v.validate_python({'x': 'foo', 'y': 'bar'}) + assert m.x == 'x: foo' + assert m.y == 'y: bar' + + @pytest.mark.parametrize( 'mode,calls1,calls2', [ From 0fd7ef1591fd48b53cf646461aaa6126e2b63516 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 15 Apr 2025 18:24:13 +0000 Subject: [PATCH 2/9] Update test as validate_assignment ValidationInfo now gets a field_name --- tests/validators/test_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/validators/test_model.py b/tests/validators/test_model.py index 2ce2c0a56..4b2164b46 100644 --- a/tests/validators/test_model.py +++ b/tests/validators/test_model.py @@ -1145,17 +1145,17 @@ class MyModel: ( core_schema.with_info_after_validator_function, (({'a': 1, 'b': 2}, None, {'b'}), 'ValidationInfo(config=None, context=None, data=None, field_name=None)'), - (({'a': 10, 'b': 2}, None, {'a'}), 'ValidationInfo(config=None, context=None, data=None, field_name=None)'), + (({'a': 10, 'b': 2}, None, {'a'}), "ValidationInfo(config=None, context=None, data=None, field_name='a')"), ), ( core_schema.with_info_before_validator_function, ({'b': 2}, 'ValidationInfo(config=None, context=None, data=None, field_name=None)'), - ({'a': 10, 'b': 2}, 'ValidationInfo(config=None, context=None, data=None, field_name=None)'), + ({'a': 10, 'b': 2}, "ValidationInfo(config=None, context=None, data=None, field_name='a')"), ), ( core_schema.with_info_wrap_validator_function, ({'b': 2}, 'ValidationInfo(config=None, context=None, data=None, field_name=None)'), - ({'a': 10, 'b': 2}, 'ValidationInfo(config=None, context=None, data=None, field_name=None)'), + ({'a': 10, 'b': 2}, "ValidationInfo(config=None, context=None, data=None, field_name='a')"), ), ], ) From df34451a13517c0914d0e2b02e9759bef7f7926b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 15 Apr 2025 18:35:01 +0000 Subject: [PATCH 3/9] Update tests as ValidationInfo now always gets a field_name --- tests/validators/test_function.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/validators/test_function.py b/tests/validators/test_function.py index 07aaa9d87..c594b4d3d 100644 --- a/tests/validators/test_function.py +++ b/tests/validators/test_function.py @@ -719,17 +719,13 @@ def f(input_value: Any, val: core_schema.ValidatorFunctionWrapHandler, info: cor assert v.validate_python({'x': b'foo'}).x == 'input: foo' -def check_info_field_name_none(info: core_schema.ValidationInfo) -> None: - assert info.field_name is None - assert info.data == {} - - -def test_non_model_field_before_validator_tries_to_access_field_info() -> None: +def test_non_model_field_before_validator_field_info() -> None: class Model: x: str def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: - check_info_field_name_none(info) + assert info.field_name == 'x' + assert info.data == {} assert isinstance(input_value, bytes) return f'input: {input_value.decode()}' @@ -749,12 +745,13 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: assert v.validate_python({'x': b'foo'}).x == 'input: foo' -def test_non_model_field_after_validator_tries_to_access_field_info() -> None: +def test_non_model_field_after_validator_field_info() -> None: class Model: x: str def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: - check_info_field_name_none(info) + assert info.field_name == 'x' + assert info.data == {} return f'input: {input_value}' v = SchemaValidator( @@ -773,12 +770,13 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: assert v.validate_python({'x': b'foo'}).x == 'input: foo' -def test_non_model_field_plain_validator_tries_to_access_field_info() -> None: +def test_non_model_field_plain_validator_field_info() -> None: class Model: x: str def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: - check_info_field_name_none(info) + assert info.field_name == 'x' + assert info.data == {} assert isinstance(input_value, bytes) return f'input: {input_value.decode()}' @@ -794,13 +792,14 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: assert v.validate_python({'x': b'foo'}).x == 'input: foo' -def test_non_model_field_wrap_validator_tries_to_access_field_info() -> None: +def test_non_model_field_wrap_validator_field_info() -> None: class Model: __slots__ = '__dict__', '__pydantic_fields_set__', '__pydantic_extra__', '__pydantic_private__' x: str def f(input_value: Any, val: core_schema.ValidatorFunctionWrapHandler, info: core_schema.ValidationInfo) -> Any: - check_info_field_name_none(info) + assert info.field_name == 'x' + assert info.data == {} return f'input: {val(input_value)}' v = SchemaValidator( From e609bb38b7531f121921478df58c099e8d7096c8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 15 Apr 2025 18:52:17 +0000 Subject: [PATCH 4/9] Only clone the field_name when necessary --- src/validators/function.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/validators/function.rs b/src/validators/function.rs index 584861305..e65ff79c3 100644 --- a/src/validators/function.rs +++ b/src/validators/function.rs @@ -105,7 +105,7 @@ impl FunctionBeforeValidator { .field_name .clone() .map(Bound::unbind) - .or(self.field_name.clone()); + .or_else(|| self.field_name.clone()); let info = ValidationInfo::new(py, state.extra(), &self.config, field_name); self.func.call1(py, (input.to_object(py)?, info)) } else { @@ -180,7 +180,7 @@ impl FunctionAfterValidator { .field_name .clone() .map(Bound::unbind) - .or(self.field_name.clone()); + .or_else(|| self.field_name.clone()); let info = ValidationInfo::new(py, state.extra(), &self.config, field_name); self.func.call1(py, (v, info)) } else { @@ -275,7 +275,7 @@ impl Validator for FunctionPlainValidator { .field_name .clone() .map(Bound::unbind) - .or(self.field_name.clone()); + .or_else(|| self.field_name.clone()); let info = ValidationInfo::new(py, state.extra(), &self.config, field_name); self.func.call1(py, (input.to_object(py)?, info)) } else { @@ -345,7 +345,7 @@ impl FunctionWrapValidator { .field_name .clone() .map(Bound::unbind) - .or(self.field_name.clone()); + .or_else(|| self.field_name.clone()); let info = ValidationInfo::new(py, state.extra(), &self.config, field_name); self.func.call1(py, (input.to_object(py)?, handler, info)) } else { From 73c2362ed3a1e486425a727cc875867a6eb0079f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 15 Apr 2025 22:25:06 +0000 Subject: [PATCH 5/9] Set ValidationState.field_name for ROOT_FIELD --- src/validators/model.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/validators/model.rs b/src/validators/model.rs index 6c93c209c..f2c3658e8 100644 --- a/src/validators/model.rs +++ b/src/validators/model.rs @@ -202,6 +202,7 @@ impl Validator for ModelValidator { field_name.to_string(), )) } else { + let state = &mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, ROOT_FIELD))); let output = self.validator.validate(py, field_value, state)?; force_setattr(py, model, intern!(py, ROOT_FIELD), output)?; @@ -255,9 +256,11 @@ impl ModelValidator { // we need to set `self_instance` to None for nested validators as we don't want to operate on self_instance // anymore let state = &mut state.rebind_extra(|extra| extra.self_instance = None); - let output = self.validator.validate(py, input, state)?; if self.root_model { + let state = &mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, ROOT_FIELD))); + let output = self.validator.validate(py, input, state)?; + let fields_set = if input.as_python().is_some_and(|py_input| py_input.is(&self.undefined)) { PySet::empty(py)? } else { @@ -266,6 +269,8 @@ impl ModelValidator { force_setattr(py, self_instance, intern!(py, DUNDER_FIELDS_SET_KEY), &fields_set)?; force_setattr(py, self_instance, intern!(py, ROOT_FIELD), &output)?; } else { + let output = self.validator.validate(py, input, state)?; + let (model_dict, model_extra, fields_set): (Bound, Bound, Bound) = output.extract(py)?; set_model_attrs(self_instance, &model_dict, &model_extra, &fields_set)?; @@ -294,11 +299,12 @@ impl ModelValidator { } } - let output = self.validator.validate(py, input, state)?; - let instance = create_class(self.class.bind(py))?; if self.root_model { + let state = &mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, ROOT_FIELD))); + let output = self.validator.validate(py, input, state)?; + let fields_set = if input.as_python().is_some_and(|py_input| py_input.is(&self.undefined)) { PySet::empty(py)? } else { @@ -307,6 +313,8 @@ impl ModelValidator { force_setattr(py, &instance, intern!(py, DUNDER_FIELDS_SET_KEY), &fields_set)?; force_setattr(py, &instance, intern!(py, ROOT_FIELD), output)?; } else { + let output = self.validator.validate(py, input, state)?; + let (model_dict, model_extra, val_fields_set): (Bound, Bound, Bound) = output.extract(py)?; let fields_set = existing_fields_set.unwrap_or(&val_fields_set); From 9bac3bdee3cfc345156c84b120f03a6257ec0626 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 15 Apr 2025 22:31:27 +0000 Subject: [PATCH 6/9] Deprecate field_name argument on core_schema.with_info_{before,after,plain,wrap}_validator_function --- python/pydantic_core/core_schema.py | 38 +++++++++++++--- tests/benchmarks/test_micro_benchmarks.py | 2 +- tests/validators/test_dataclasses.py | 42 +++++------------- tests/validators/test_function.py | 54 +++++++---------------- tests/validators/test_model.py | 4 +- tests/validators/test_model_init.py | 12 ++--- tests/validators/test_model_root.py | 2 +- 7 files changed, 65 insertions(+), 89 deletions(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 0ab3dd947..08117f313 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -1954,7 +1954,7 @@ class NoInfoValidatorFunctionSchema(TypedDict): class WithInfoValidatorFunctionSchema(TypedDict, total=False): type: Required[Literal['with-info']] function: Required[WithInfoValidatorFunction] - field_name: str + field_name: str # deprecated ValidationFunction = Union[NoInfoValidatorFunctionSchema, WithInfoValidatorFunctionSchema] @@ -2042,7 +2042,7 @@ def fn(v: bytes, info: core_schema.ValidationInfo) -> str: return v.decode() + 'world' func_schema = core_schema.with_info_before_validator_function( - function=fn, schema=core_schema.str_schema(), field_name='a' + function=fn, schema=core_schema.str_schema() ) schema = core_schema.typed_dict_schema({'a': core_schema.typed_dict_field(func_schema)}) @@ -2052,13 +2052,19 @@ def fn(v: bytes, info: core_schema.ValidationInfo) -> str: Args: function: The validator function to call - field_name: The name of the field + field_name: The name of the field this validator is applied to, if any (deprecated) schema: The schema to validate the output of the validator function ref: optional unique identifier of the schema, used to reference the schema in other places json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema """ + if field_name is not None: + warnings.warn( + 'The `field_name` argument on `with_info_before_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', + DeprecationWarning, + ) + return _dict_not_none( type='function-before', function=_dict_not_none(type='with-info', function=function, field_name=field_name), @@ -2140,7 +2146,7 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str: return v + 'world' func_schema = core_schema.with_info_after_validator_function( - function=fn, schema=core_schema.str_schema(), field_name='a' + function=fn, schema=core_schema.str_schema() ) schema = core_schema.typed_dict_schema({'a': core_schema.typed_dict_field(func_schema)}) @@ -2151,11 +2157,17 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str: Args: function: The validator function to call after the schema is validated schema: The schema to validate before the validator function - field_name: The name of the field this validators is applied to, if any + field_name: The name of the field this validator is applied to, if any (deprecated) ref: optional unique identifier of the schema, used to reference the schema in other places metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema """ + if field_name is not None: + warnings.warn( + 'The `field_name` argument on `with_info_after_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', + DeprecationWarning, + ) + return _dict_not_none( type='function-after', function=_dict_not_none(type='with-info', function=function, field_name=field_name), @@ -2287,12 +2299,18 @@ def fn( Args: function: The validator function to call schema: The schema to validate the output of the validator function - field_name: The name of the field this validators is applied to, if any + field_name: The name of the field this validator is applied to, if any (deprecated) json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type ref: optional unique identifier of the schema, used to reference the schema in other places metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema """ + if field_name is not None: + warnings.warn( + 'The `field_name` argument on `with_info_wrap_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', + DeprecationWarning, + ) + return _dict_not_none( type='function-wrap', function=_dict_not_none(type='with-info', function=function, field_name=field_name), @@ -2379,12 +2397,18 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str: Args: function: The validator function to call - field_name: The name of the field this validators is applied to, if any + field_name: The name of the field this validator is applied to, if any (deprecated) ref: optional unique identifier of the schema, used to reference the schema in other places json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type metadata: Any other information you want to include with the schema, not used by pydantic-core serialization: Custom serialization schema """ + if field_name is not None: + warnings.warn( + 'The `field_name` argument on `with_info_plain_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', + DeprecationWarning, + ) + return _dict_not_none( type='function-plain', function=_dict_not_none(type='with-info', function=function, field_name=field_name), diff --git a/tests/benchmarks/test_micro_benchmarks.py b/tests/benchmarks/test_micro_benchmarks.py index 5640431ae..6d36ffa8d 100644 --- a/tests/benchmarks/test_micro_benchmarks.py +++ b/tests/benchmarks/test_micro_benchmarks.py @@ -1348,7 +1348,7 @@ def f(v: int, info: core_schema.ValidationInfo) -> int: limit = pydantic_core._pydantic_core._recursion_limit - 3 for _ in range(limit): - schema = core_schema.with_info_after_validator_function(f, schema, field_name='x') + schema = core_schema.with_info_after_validator_function(f, schema) schema = core_schema.typed_dict_schema({'x': core_schema.typed_dict_field(schema)}) diff --git a/tests/validators/test_dataclasses.py b/tests/validators/test_dataclasses.py index cfead4423..6ae9f54f9 100644 --- a/tests/validators/test_dataclasses.py +++ b/tests/validators/test_dataclasses.py @@ -506,9 +506,7 @@ def validate_b(cls, v: str, info: core_schema.ValidationInfo) -> str: core_schema.dataclass_field(name='a', schema=core_schema.int_schema()), core_schema.dataclass_field( name='b', - schema=core_schema.with_info_after_validator_function( - Foo.validate_b, core_schema.str_schema(), field_name='b' - ), + schema=core_schema.with_info_after_validator_function(Foo.validate_b, core_schema.str_schema()), ), ], ), @@ -540,7 +538,7 @@ def validate_b(cls, v: bytes, info: core_schema.ValidationInfo) -> str: [ core_schema.dataclass_field(name='a', schema=core_schema.int_schema()), core_schema.dataclass_field( - name='b', schema=core_schema.with_info_plain_validator_function(Foo.validate_b, field_name='b') + name='b', schema=core_schema.with_info_plain_validator_function(Foo.validate_b) ), ], ), @@ -573,9 +571,7 @@ def validate_b(cls, v: bytes, info: core_schema.ValidationInfo) -> bytes: core_schema.dataclass_field(name='a', schema=core_schema.int_schema()), core_schema.dataclass_field( name='b', - schema=core_schema.with_info_before_validator_function( - Foo.validate_b, core_schema.str_schema(), field_name='b' - ), + schema=core_schema.with_info_before_validator_function(Foo.validate_b, core_schema.str_schema()), ), ], ), @@ -612,9 +608,7 @@ def validate_b( core_schema.dataclass_field(name='a', schema=core_schema.int_schema()), core_schema.dataclass_field( name='b', - schema=core_schema.with_info_wrap_validator_function( - Foo.validate_b, core_schema.str_schema(), field_name='b' - ), + schema=core_schema.with_info_wrap_validator_function(Foo.validate_b, core_schema.str_schema()), ), ], ), @@ -649,9 +643,7 @@ def validate_b( core_schema.dataclass_field(name='a', schema=core_schema.int_schema()), core_schema.dataclass_field( name='b', - schema=core_schema.with_info_wrap_validator_function( - Foo.validate_b, core_schema.str_schema(), field_name='b' - ), + schema=core_schema.with_info_wrap_validator_function(Foo.validate_b, core_schema.str_schema()), ), ], ), @@ -878,9 +870,7 @@ def func(x, info): core_schema.dataclass_field('field_a', core_schema.str_schema()), core_schema.dataclass_field( 'field_b', - core_schema.with_info_after_validator_function( - func, core_schema.int_schema(), field_name='field_b' - ), + core_schema.with_info_after_validator_function(func, core_schema.int_schema()), ), core_schema.dataclass_field('field_c', core_schema.int_schema()), ], @@ -1295,9 +1285,7 @@ def validate_b(cls, v: bytes, info: core_schema.ValidationInfo) -> bytes: core_schema.dataclass_field(name='a', schema=core_schema.int_schema()), core_schema.dataclass_field( name='b', - schema=core_schema.with_info_before_validator_function( - Foo.validate_b, core_schema.str_schema(), field_name='b' - ), + schema=core_schema.with_info_before_validator_function(Foo.validate_b, core_schema.str_schema()), ), ], ), @@ -1332,9 +1320,7 @@ def validate_b(cls, v: str, info: core_schema.ValidationInfo) -> str: core_schema.dataclass_field(name='a', schema=core_schema.int_schema()), core_schema.dataclass_field( name='b', - schema=core_schema.with_info_after_validator_function( - Foo.validate_b, core_schema.str_schema(), field_name='b' - ), + schema=core_schema.with_info_after_validator_function(Foo.validate_b, core_schema.str_schema()), ), ], ), @@ -1550,15 +1536,9 @@ def _wrap_validator(cls, v, validator, info): field_schema = core_schema.int_schema() if validator == 'field': - field_schema = core_schema.with_info_before_validator_function( - Dataclass._validator, field_schema, field_name='a' - ) - field_schema = core_schema.with_info_wrap_validator_function( - Dataclass._wrap_validator, field_schema, field_name='a' - ) - field_schema = core_schema.with_info_after_validator_function( - Dataclass._validator, field_schema, field_name='a' - ) + field_schema = core_schema.with_info_before_validator_function(Dataclass._validator, field_schema) + field_schema = core_schema.with_info_wrap_validator_function(Dataclass._wrap_validator, field_schema) + field_schema = core_schema.with_info_after_validator_function(Dataclass._validator, field_schema) dataclass_schema = core_schema.dataclass_schema( Dataclass, diff --git a/tests/validators/test_function.py b/tests/validators/test_function.py index c594b4d3d..09ef7c74f 100644 --- a/tests/validators/test_function.py +++ b/tests/validators/test_function.py @@ -605,7 +605,7 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: core_schema.model_fields_schema( { 'x': core_schema.model_field( - core_schema.with_info_before_validator_function(f, core_schema.str_schema(), field_name='x') + core_schema.with_info_before_validator_function(f, core_schema.str_schema()) ) } ), @@ -631,7 +631,7 @@ def f(input_value: str, info: core_schema.ValidationInfo) -> Any: core_schema.model_fields_schema( { 'x': core_schema.model_field( - core_schema.with_info_after_validator_function(f, core_schema.str_schema(), field_name='x') + core_schema.with_info_after_validator_function(f, core_schema.str_schema()) ) } ), @@ -655,7 +655,7 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: core_schema.model_schema( Model, core_schema.model_fields_schema( - {'x': core_schema.model_field(core_schema.with_info_plain_validator_function(f, field_name='x'))} + {'x': core_schema.model_field(core_schema.with_info_plain_validator_function(f))} ), ) ) @@ -674,7 +674,10 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: # When a type alias with a validator function is used on multiple fields, # its core schema is only generated once (with the first field_name) and reused. # See https://github.com/pydantic/pydantic/issues/11737 - validator = core_schema.with_info_plain_validator_function(f, field_name='x') + with pytest.warns( + DeprecationWarning, match='`field_name` argument on `with_info_plain_validator_function` is deprecated' + ): + validator = core_schema.with_info_plain_validator_function(f, field_name='x') v = SchemaValidator( core_schema.model_schema( @@ -709,7 +712,7 @@ def f(input_value: Any, val: core_schema.ValidatorFunctionWrapHandler, info: cor core_schema.model_fields_schema( { 'x': core_schema.model_field( - core_schema.with_info_wrap_validator_function(f, core_schema.str_schema(), field_name='x') + core_schema.with_info_wrap_validator_function(f, core_schema.str_schema()) ) } ), @@ -833,7 +836,7 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: 'a': core_schema.typed_dict_field(core_schema.int_schema()), 'b': core_schema.typed_dict_field(core_schema.int_schema()), 'c': core_schema.typed_dict_field( - core_schema.with_info_after_validator_function(f, core_schema.str_schema(), field_name='c') + core_schema.with_info_after_validator_function(f, core_schema.str_schema()) ), } ) @@ -858,7 +861,10 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: # When a type alias with a validator function is used on multiple fields, # its core schema is only generated once (with the first field_name) and reused. # See https://github.com/pydantic/pydantic/issues/11737 - validator = core_schema.with_info_plain_validator_function(f, field_name='x') + with pytest.warns( + DeprecationWarning, match='`field_name` argument on `with_info_plain_validator_function` is deprecated' + ): + validator = core_schema.with_info_plain_validator_function(f, field_name='x') v = SchemaValidator( core_schema.typed_dict_schema( @@ -886,7 +892,10 @@ def f(input_value: Any, info: core_schema.ValidationInfo) -> Any: # When a type alias with a validator function is used on multiple fields, # its core schema is only generated once (with the first field_name) and reused. # See https://github.com/pydantic/pydantic/issues/11737 - validator = core_schema.with_info_plain_validator_function(f, field_name='x') + with pytest.warns( + DeprecationWarning, match='`field_name` argument on `with_info_plain_validator_function` is deprecated' + ): + validator = core_schema.with_info_plain_validator_function(f, field_name='x') v = SchemaValidator( core_schema.dataclass_schema( @@ -1002,35 +1011,6 @@ def f_w(v: Any, handler: core_schema.ValidatorFunctionWrapHandler, info: core_sc calls.clear() -def test_reprs() -> None: - reprs: list[str] = [] - - def sample_repr(v: Any, info: core_schema.ValidationInfo) -> Any: - reprs.append(repr(info)) - return v - - v = SchemaValidator( - core_schema.chain_schema( - [ - core_schema.with_info_plain_validator_function(sample_repr), - core_schema.with_info_plain_validator_function(sample_repr, field_name='x'), - ] - ) - ) - - class Foo: - def __repr__(self) -> str: - return 'This is Foo!' - - v.validate_python(Foo()) - - # insert_assert(reprs) - assert reprs == [ - 'ValidationInfo(config=None, context=None, data=None, field_name=None)', - "ValidationInfo(config=None, context=None, data=None, field_name='x')", - ] - - def test_function_after_doesnt_change_mode() -> None: # https://github.com/pydantic/pydantic/issues/7468 - function-after was # incorrectly forcing Python validation mode diff --git a/tests/validators/test_model.py b/tests/validators/test_model.py index 4b2164b46..253b8a734 100644 --- a/tests/validators/test_model.py +++ b/tests/validators/test_model.py @@ -1054,9 +1054,7 @@ def func(x, info): { 'field_a': core_schema.model_field(core_schema.str_schema()), 'field_b': core_schema.model_field( - core_schema.with_info_after_validator_function( - func, core_schema.int_schema(), field_name='field_b' - ) + core_schema.with_info_after_validator_function(func, core_schema.int_schema()) ), 'field_c': core_schema.model_field(core_schema.int_schema()), } diff --git a/tests/validators/test_model_init.py b/tests/validators/test_model_init.py index 08f13ac54..463a9a48a 100644 --- a/tests/validators/test_model_init.py +++ b/tests/validators/test_model_init.py @@ -430,15 +430,9 @@ def _wrap_validator(cls, v, validator, info): field_schema = core_schema.int_schema() if validator == 'field': - field_schema = core_schema.with_info_before_validator_function( - Model._validator, field_schema, field_name='a' - ) - field_schema = core_schema.with_info_wrap_validator_function( - Model._wrap_validator, field_schema, field_name='a' - ) - field_schema = core_schema.with_info_after_validator_function( - Model._validator, field_schema, field_name='a' - ) + field_schema = core_schema.with_info_before_validator_function(Model._validator, field_schema) + field_schema = core_schema.with_info_wrap_validator_function(Model._wrap_validator, field_schema) + field_schema = core_schema.with_info_after_validator_function(Model._validator, field_schema) model_schema = core_schema.model_schema( Model, core_schema.model_fields_schema({'a': core_schema.model_field(field_schema)}) diff --git a/tests/validators/test_model_root.py b/tests/validators/test_model_root.py index 4e0a55048..96e492c4d 100644 --- a/tests/validators/test_model_root.py +++ b/tests/validators/test_model_root.py @@ -138,7 +138,7 @@ def f(input_value: str, info): v = SchemaValidator( core_schema.model_schema( RootModel, - core_schema.with_info_after_validator_function(f, core_schema.str_schema(), field_name='root'), + core_schema.with_info_after_validator_function(f, core_schema.str_schema()), root_model=True, ) ) From b5cc2e9e78ddb8c49001b182409165df929914c9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 16 Apr 2025 14:15:34 +0000 Subject: [PATCH 7/9] Set stacklevel on validator function field_name arg DeprecationWarning --- python/pydantic_core/core_schema.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 08117f313..e5d85f541 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -2063,6 +2063,7 @@ def fn(v: bytes, info: core_schema.ValidationInfo) -> str: warnings.warn( 'The `field_name` argument on `with_info_before_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', DeprecationWarning, + stacklevel=2, ) return _dict_not_none( @@ -2166,6 +2167,7 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str: warnings.warn( 'The `field_name` argument on `with_info_after_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', DeprecationWarning, + stacklevel=2, ) return _dict_not_none( @@ -2309,6 +2311,7 @@ def fn( warnings.warn( 'The `field_name` argument on `with_info_wrap_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', DeprecationWarning, + stacklevel=2, ) return _dict_not_none( @@ -2407,6 +2410,7 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str: warnings.warn( 'The `field_name` argument on `with_info_plain_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.', DeprecationWarning, + stacklevel=2, ) return _dict_not_none( From 718324d14cf7d9078e9b3b8f945a254ab5762cbf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 16 Apr 2025 17:48:00 +0000 Subject: [PATCH 8/9] Set field_name when validating call arguments --- src/validators/arguments.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/validators/arguments.rs b/src/validators/arguments.rs index 2da3bbfcb..2ca3ba5f6 100644 --- a/src/validators/arguments.rs +++ b/src/validators/arguments.rs @@ -229,6 +229,9 @@ impl Validator for ArgumentsValidator { } } + let state = + &mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, parameter.name.as_str()))); + match (pos_value, kw_value) { (Some(_), Some((_, kw_value))) => { errors.push(ValLineError::new_with_loc( From 3d699810c596bc694a950b4fb02890704151d1e5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 16 Apr 2025 18:02:20 +0000 Subject: [PATCH 9/9] Add deprecated comment to WithInfoWrapValidatorFunctionSchema.field_name --- python/pydantic_core/core_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index e5d85f541..f03daac36 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -2201,7 +2201,7 @@ class NoInfoWrapValidatorFunctionSchema(TypedDict): class WithInfoWrapValidatorFunctionSchema(TypedDict, total=False): type: Required[Literal['with-info']] function: Required[WithInfoWrapValidatorFunction] - field_name: str + field_name: str # deprecated WrapValidatorFunction = Union[NoInfoWrapValidatorFunctionSchema, WithInfoWrapValidatorFunctionSchema]