diff --git a/benches/main.rs b/benches/main.rs index c558ca3c6..fc2133a6d 100644 --- a/benches/main.rs +++ b/benches/main.rs @@ -34,7 +34,7 @@ fn ints_json(bench: &mut Bencher) { let validator = build_schema_validator(py, c"{'type': 'int'}"); let result = validator - .validate_json(py, &json(py, "123"), None, None, None, false.into(), None, None) + .validate_json(py, &json(py, "123"), None, None, None, None, false.into(), None, None) .unwrap(); let result_int: i64 = result.extract(py).unwrap(); assert_eq!(result_int, 123); @@ -42,7 +42,7 @@ fn ints_json(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_json(py, &json(py, "123"), None, None, None, false.into(), None, None) + .validate_json(py, &json(py, "123"), None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -56,7 +56,7 @@ fn ints_python(bench: &mut Bencher) { let Ok(input) = 123_i64.into_pyobject(py); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); let result_int: i64 = result.extract(py).unwrap(); assert_eq!(result_int, 123); @@ -65,7 +65,7 @@ fn ints_python(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -84,7 +84,7 @@ fn list_int_json(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_json(py, &json(py, &code), None, None, None, false.into(), None, None) + .validate_json(py, &json(py, &code), None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -110,7 +110,7 @@ fn list_int_python(bench: &mut Bencher) { let input = black_box(input.bind(py)); bench.iter(|| { let v = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); black_box(v) }) @@ -123,13 +123,13 @@ fn list_int_python_isinstance(bench: &mut Bencher) { let (validator, input) = list_int_input(py); let input = black_box(input.bind(py)); let v = validator - .isinstance_python(py, &input, None, None, None, None, None, None) + .isinstance_python(py, &input, None, None, None, None, None, None, None) .unwrap(); assert!(v); bench.iter(|| { let v = validator - .isinstance_python(py, &input, None, None, None, None, None, None) + .isinstance_python(py, &input, None, None, None, None, None, None, None) .unwrap(); black_box(v) }) @@ -148,7 +148,7 @@ fn list_error_json(bench: &mut Bencher) { .join(", ") ); - match validator.validate_json(py, &json(py, &code), None, None, None, false.into(), None, None) { + match validator.validate_json(py, &json(py, &code), None, None, None, None, false.into(), None, None) { Ok(_) => panic!("unexpectedly valid"), Err(e) => { let v = e.value(py); @@ -159,12 +159,12 @@ fn list_error_json(bench: &mut Bencher) { } }; - bench.iter( - || match validator.validate_json(py, &json(py, &code), None, None, None, false.into(), None, None) { + bench.iter(|| { + match validator.validate_json(py, &json(py, &code), None, None, None, None, false.into(), None, None) { Ok(_) => panic!("unexpectedly valid"), Err(e) => black_box(e), - }, - ) + } + }) }) } @@ -181,7 +181,7 @@ fn list_error_python_input(py: Python<'_>) -> (SchemaValidator, PyObject) { let input = py.eval(&code, None, None).unwrap().extract().unwrap(); - match validator.validate_python(py, &input, None, None, None, None, false.into(), None, None) { + match validator.validate_python(py, &input, None, None, None, None, None, false.into(), None, None) { Ok(_) => panic!("unexpectedly valid"), Err(e) => { let v = e.value(py); @@ -201,7 +201,7 @@ fn list_error_python(bench: &mut Bencher) { let input = black_box(input.bind(py)); bench.iter(|| { - let result = validator.validate_python(py, &input, None, None, None, None, false.into(), None, None); + let result = validator.validate_python(py, &input, None, None, None, None, None, false.into(), None, None); match result { Ok(_) => panic!("unexpectedly valid"), @@ -217,14 +217,14 @@ fn list_error_python_isinstance(bench: &mut Bencher) { let (validator, input) = list_error_python_input(py); let input = black_box(input.bind(py)); let r = validator - .isinstance_python(py, &input, None, None, None, None, None, None) + .isinstance_python(py, &input, None, None, None, None, None, None, None) .unwrap(); assert!(!r); bench.iter(|| { black_box( validator - .isinstance_python(py, &input, None, None, None, None, None, None) + .isinstance_python(py, &input, None, None, None, None, None, None, None) .unwrap(), ); }) @@ -243,7 +243,7 @@ fn list_any_json(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_json(py, &json(py, &code), None, None, None, false.into(), None, None) + .validate_json(py, &json(py, &code), None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -263,7 +263,7 @@ fn list_any_python(bench: &mut Bencher) { let input = black_box(input); bench.iter(|| { let v = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); black_box(v) }) @@ -297,7 +297,7 @@ fn dict_json(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_json(py, &json(py, &code), None, None, None, false.into(), None, None) + .validate_json(py, &json(py, &code), None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -324,7 +324,7 @@ fn dict_python(bench: &mut Bencher) { let input = black_box(input); bench.iter(|| { let v = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); black_box(v) }) @@ -354,7 +354,7 @@ fn dict_value_error(bench: &mut Bencher) { let input = py.eval(&code, None, None).unwrap(); - match validator.validate_python(py, &input, None, None, None, None, false.into(), None, None) { + match validator.validate_python(py, &input, None, None, None, None, None, false.into(), None, None) { Ok(_) => panic!("unexpectedly valid"), Err(e) => { let v = e.value(py); @@ -367,7 +367,7 @@ fn dict_value_error(bench: &mut Bencher) { let input = black_box(input); bench.iter(|| { - let result = validator.validate_python(py, &input, None, None, None, None, false.into(), None, None); + let result = validator.validate_python(py, &input, None, None, None, None, None, false.into(), None, None); match result { Ok(_) => panic!("unexpectedly valid"), @@ -405,7 +405,7 @@ fn typed_dict_json(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_json(py, &json(py, code), None, None, None, false.into(), None, None) + .validate_json(py, &json(py, code), None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -440,7 +440,7 @@ fn typed_dict_python(bench: &mut Bencher) { let input = black_box(input); bench.iter(|| { let v = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); black_box(v) }) @@ -481,7 +481,7 @@ fn typed_dict_deep_error(bench: &mut Bencher) { let input = py.eval(code, None, None).unwrap(); let input = black_box(input); - match validator.validate_python(py, &input, None, None, None, None, false.into(), None, None) { + match validator.validate_python(py, &input, None, None, None, None, None, false.into(), None, None) { Ok(_) => panic!("unexpectedly valid"), Err(e) => { let v = e.value(py); @@ -493,7 +493,7 @@ fn typed_dict_deep_error(bench: &mut Bencher) { }; bench.iter(|| { - let result = validator.validate_python(py, &input, None, None, None, None, false.into(), None, None); + let result = validator.validate_python(py, &input, None, None, None, None, None, false.into(), None, None); match result { Ok(_) => panic!("unexpectedly valid"), @@ -520,7 +520,7 @@ fn complete_model(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ); }) @@ -542,13 +542,13 @@ fn nested_model_using_definitions(bench: &mut Bencher) { let input = black_box(input); validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ); }) @@ -570,13 +570,13 @@ fn nested_model_inlined(bench: &mut Bencher) { let input = black_box(input); validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ); }) @@ -590,7 +590,7 @@ fn literal_ints_few_python(bench: &mut Bencher) { let Ok(input) = 4_i64.into_pyobject(py); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); let result_int: i64 = result.extract(py).unwrap(); assert_eq!(result_int, 4); @@ -599,7 +599,7 @@ fn literal_ints_few_python(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -614,7 +614,7 @@ fn literal_strings_few_small_python(bench: &mut Bencher) { let input = py.eval(c"'4'", None, None).unwrap(); let input_str: String = input.extract().unwrap(); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); let result_str: String = result.extract(py).unwrap(); assert_eq!(result_str, input_str); @@ -623,7 +623,7 @@ fn literal_strings_few_small_python(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -641,7 +641,7 @@ fn literal_strings_few_large_python(bench: &mut Bencher) { let input = py.eval(c"'a' * 25 + '4'", None, None).unwrap(); let input_str: String = input.extract().unwrap(); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); let result_str: String = result.extract(py).unwrap(); assert_eq!(result_str, input_str); @@ -650,7 +650,7 @@ fn literal_strings_few_large_python(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -684,7 +684,7 @@ class Foo(Enum): let input = py.eval(c"Foo.v4", Some(&globals), None).unwrap(); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); assert!(input.eq(result).unwrap()); @@ -692,7 +692,7 @@ class Foo(Enum): bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -706,7 +706,7 @@ fn literal_ints_many_python(bench: &mut Bencher) { let Ok(input) = 99_i64.into_pyobject(py); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); let result_int: i64 = result.extract(py).unwrap(); assert_eq!(result_int, 99); @@ -715,7 +715,7 @@ fn literal_ints_many_python(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -731,7 +731,7 @@ fn literal_strings_many_small_python(bench: &mut Bencher) { let input = py.eval(c"'99'", None, None).unwrap(); let input_str: String = input.extract().unwrap(); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); let result_str: String = result.extract(py).unwrap(); assert_eq!(result_str, input_str); @@ -740,7 +740,7 @@ fn literal_strings_many_small_python(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -758,7 +758,7 @@ fn literal_strings_many_large_python(bench: &mut Bencher) { let input = py.eval(c"'a' * 25 + '99'", None, None).unwrap(); let input_str: String = input.extract().unwrap(); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); let result_str: String = result.extract(py).unwrap(); assert_eq!(result_str, input_str); @@ -767,7 +767,7 @@ fn literal_strings_many_large_python(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -781,7 +781,7 @@ fn literal_ints_many_json(bench: &mut Bencher) { let input_json = py.eval(c"'99'", None, None).unwrap(); let result = validator - .validate_json(py, &input_json, None, None, None, false.into(), None, None) + .validate_json(py, &input_json, None, None, None, None, false.into(), None, None) .unwrap(); let result_int: i64 = result.extract(py).unwrap(); assert_eq!(result_int, 99); @@ -790,7 +790,7 @@ fn literal_ints_many_json(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_json(py, &input_json, None, None, None, false.into(), None, None) + .validate_json(py, &input_json, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -809,7 +809,7 @@ fn literal_strings_many_large_json(bench: &mut Bencher) { let input_json = py.eval(c"'\"' + 'a' * 25 + '99' + '\"'", None, None).unwrap(); let input_str: String = input.extract().unwrap(); let result = validator - .validate_json(py, &input_json, None, None, None, false.into(), None, None) + .validate_json(py, &input_json, None, None, None, None, false.into(), None, None) .unwrap(); let result_str: String = result.extract(py).unwrap(); assert_eq!(result_str, input_str); @@ -818,7 +818,7 @@ fn literal_strings_many_large_json(bench: &mut Bencher) { bench.iter(|| { black_box( validator - .validate_json(py, &input_json, None, None, None, false.into(), None, None) + .validate_json(py, &input_json, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -854,7 +854,7 @@ class Foo(Enum): let input = py.eval(c"'null'", None, None).unwrap(); let input_str: String = input.extract().unwrap(); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); let result_str: String = result.extract(py).unwrap(); assert_eq!(result_str, input_str); @@ -863,7 +863,7 @@ class Foo(Enum): bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -874,7 +874,7 @@ class Foo(Enum): let input = py.eval(c"-1", None, None).unwrap(); let input_int: i64 = input.extract().unwrap(); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); let result_int: i64 = result.extract(py).unwrap(); assert_eq!(result_int, input_int); @@ -883,7 +883,7 @@ class Foo(Enum): bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -893,7 +893,7 @@ class Foo(Enum): { let input = py.eval(c"None", None, None).unwrap(); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); assert!(input.eq(result).unwrap()); @@ -901,7 +901,7 @@ class Foo(Enum): bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) @@ -911,7 +911,7 @@ class Foo(Enum): { let input = py.eval(c"Foo.v4", Some(&globals), None).unwrap(); let result = validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(); assert!(input.eq(result).unwrap()); @@ -919,7 +919,7 @@ class Foo(Enum): bench.iter(|| { black_box( validator - .validate_python(py, &input, None, None, None, None, false.into(), None, None) + .validate_python(py, &input, None, None, None, None, None, false.into(), None, None) .unwrap(), ) }) diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index 398020bb1..950df891a 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -6,7 +6,7 @@ from _typeshed import SupportsAllComparisons from typing_extensions import LiteralString, Self, TypeAlias from pydantic_core import ErrorDetails, ErrorTypeInfo, InitErrorDetails, MultiHostHost -from pydantic_core.core_schema import CoreConfig, CoreSchema, ErrorType +from pydantic_core.core_schema import CoreConfig, CoreSchema, ErrorType, ExtraBehavior __all__ = [ '__version__', @@ -93,6 +93,7 @@ class SchemaValidator: input: Any, *, strict: bool | None = None, + extra: ExtraBehavior | None = None, from_attributes: bool | None = None, context: Any | None = None, self_instance: Any | None = None, @@ -107,6 +108,8 @@ class SchemaValidator: input: The Python object to validate. strict: Whether to validate the object in strict mode. If `None`, the value of [`CoreConfig.strict`][pydantic_core.core_schema.CoreConfig] is used. + extra: The behavior for handling extra fields. + If `None`, the value of [`CoreConfig.extra_fields_behavior`][pydantic_core.core_schema.CoreConfig] is used. from_attributes: Whether to validate objects as inputs to models by extracting attributes. If `None`, the value of [`CoreConfig.from_attributes`][pydantic_core.core_schema.CoreConfig] is used. context: The context to use for validation, this is passed to functional validators as @@ -131,6 +134,7 @@ class SchemaValidator: input: Any, *, strict: bool | None = None, + extra: ExtraBehavior | None = None, from_attributes: bool | None = None, context: Any | None = None, self_instance: Any | None = None, @@ -151,6 +155,7 @@ class SchemaValidator: input: str | bytes | bytearray, *, strict: bool | None = None, + extra: ExtraBehavior | None = None, context: Any | None = None, self_instance: Any | None = None, allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False, @@ -170,6 +175,8 @@ class SchemaValidator: input: The JSON data to validate. strict: Whether to validate the object in strict mode. If `None`, the value of [`CoreConfig.strict`][pydantic_core.core_schema.CoreConfig] is used. + extra: The behavior for handling extra fields. + If `None`, the value of [`CoreConfig.extra_fields_behavior`][pydantic_core.core_schema.CoreConfig] is used. context: The context to use for validation, this is passed to functional validators as [`info.context`][pydantic_core.core_schema.ValidationInfo.context]. self_instance: An instance of a model set attributes on from validation. @@ -191,6 +198,7 @@ class SchemaValidator: input: _StringInput, *, strict: bool | None = None, + extra: ExtraBehavior | None = None, context: Any | None = None, allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False, by_alias: bool | None = None, @@ -206,6 +214,8 @@ class SchemaValidator: input: The input as a string, or bytes/bytearray if `strict=False`. strict: Whether to validate the object in strict mode. If `None`, the value of [`CoreConfig.strict`][pydantic_core.core_schema.CoreConfig] is used. + extra: The behavior for handling extra fields. + If `None`, the value of [`CoreConfig.extra_fields_behavior`][pydantic_core.core_schema.CoreConfig] is used. context: The context to use for validation, this is passed to functional validators as [`info.context`][pydantic_core.core_schema.ValidationInfo.context]. allow_partial: Whether to allow partial validation; if `True` errors in the last element of sequences @@ -228,6 +238,7 @@ class SchemaValidator: field_value: Any, *, strict: bool | None = None, + extra: ExtraBehavior | None = None, from_attributes: bool | None = None, context: Any | None = None, by_alias: bool | None = None, @@ -242,6 +253,8 @@ class SchemaValidator: field_value: The value to assign to the field. strict: Whether to validate the object in strict mode. If `None`, the value of [`CoreConfig.strict`][pydantic_core.core_schema.CoreConfig] is used. + extra: The behavior for handling extra fields. + If `None`, the value of [`CoreConfig.extra_fields_behavior`][pydantic_core.core_schema.CoreConfig] is used. from_attributes: Whether to validate objects as inputs to models by extracting attributes. If `None`, the value of [`CoreConfig.from_attributes`][pydantic_core.core_schema.CoreConfig] is used. context: The context to use for validation, this is passed to functional validators as diff --git a/src/build_tools.rs b/src/build_tools.rs index 6c7e63455..2abcbd9ca 100644 --- a/src/build_tools.rs +++ b/src/build_tools.rs @@ -1,5 +1,6 @@ use std::error::Error; use std::fmt; +use std::str::FromStr; use pyo3::exceptions::PyException; use pyo3::prelude::*; @@ -176,7 +177,7 @@ macro_rules! py_schema_err { pub(crate) use py_schema_err; #[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub(crate) enum ExtraBehavior { +pub enum ExtraBehavior { Allow, Forbid, Ignore, @@ -197,12 +198,22 @@ impl ExtraBehavior { )? .flatten(); let res = match extra_behavior.as_ref().map(|s| s.to_str()).transpose()? { - Some("allow") => Self::Allow, - Some("ignore") => Self::Ignore, - Some("forbid") => Self::Forbid, - Some(v) => return py_schema_err!("Invalid extra_behavior: `{}`", v), + Some(s) => Self::from_str(s)?, None => default, }; Ok(res) } } + +impl FromStr for ExtraBehavior { + type Err = PyErr; + + fn from_str(s: &str) -> Result { + match s { + "allow" => Ok(Self::Allow), + "forbid" => Ok(Self::Forbid), + "ignore" => Ok(Self::Ignore), + s => py_schema_err!("Invalid extra_behavior: `{}`", s), + } + } +} diff --git a/src/url.rs b/src/url.rs index da63be5af..b25bd97ea 100644 --- a/src/url.rs +++ b/src/url.rs @@ -45,7 +45,7 @@ impl PyUrl { pub fn py_new(py: Python, url: &Bound<'_, PyAny>) -> PyResult { let schema_obj = SCHEMA_DEFINITION_URL .get_or_init(py, || build_schema_validator(py, "url")) - .validate_python(py, url, None, None, None, None, false.into(), None, None)?; + .validate_python(py, url, None, None, None, None, None, false.into(), None, None)?; schema_obj.extract(py) } @@ -225,7 +225,7 @@ impl PyMultiHostUrl { pub fn py_new(py: Python, url: &Bound<'_, PyAny>) -> PyResult { let schema_obj = SCHEMA_DEFINITION_MULTI_HOST_URL .get_or_init(py, || build_schema_validator(py, "multi-host-url")) - .validate_python(py, url, None, None, None, None, false.into(), None, None)?; + .validate_python(py, url, None, None, None, None, None, false.into(), None, None)?; schema_obj.extract(py) } diff --git a/src/validators/arguments_v3.rs b/src/validators/arguments_v3.rs index 22c9c6d0f..821a7547d 100644 --- a/src/validators/arguments_v3.rs +++ b/src/validators/arguments_v3.rs @@ -241,13 +241,15 @@ impl ArgumentsV3Validator { let validate_by_alias = state.validate_by_alias_or(self.validate_by_alias); let validate_by_name = state.validate_by_name_or(self.validate_by_name); + let extra_behavior = state.extra_behavior_or(self.extra); // Keep track of used keys for extra behavior: - let mut used_keys: Option> = if self.extra == ExtraBehavior::Ignore || mapping.is_py_get_attr() { - None - } else { - Some(AHashSet::with_capacity(self.parameters.len())) - }; + let mut used_keys: Option> = + if extra_behavior == ExtraBehavior::Ignore || mapping.is_py_get_attr() { + None + } else { + Some(AHashSet::with_capacity(self.parameters.len())) + }; for parameter in &self.parameters { let lookup_key = parameter @@ -492,7 +494,7 @@ impl ArgumentsV3Validator { mapping.iterate(ValidateExtra { used_keys, errors: &mut errors, - extra_behavior: self.extra, + extra_behavior, })??; } @@ -524,6 +526,7 @@ impl ArgumentsV3Validator { let validate_by_alias = state.validate_by_alias_or(self.validate_by_alias); let validate_by_name = state.validate_by_name_or(self.validate_by_name); + let extra_behavior = state.extra_behavior_or(self.extra); // go through non variadic parameters, getting the value from args or kwargs and validating it for (index, parameter) in self.parameters.iter().filter(|p| !p.is_variadic()).enumerate() { @@ -687,7 +690,7 @@ impl ArgumentsV3Validator { match maybe_var_kwargs_parameter { None => { - if self.extra == ExtraBehavior::Forbid { + if extra_behavior == ExtraBehavior::Forbid { errors.push(ValLineError::new_with_loc( ErrorTypeDefaults::UnexpectedKeywordArgument, value, diff --git a/src/validators/dataclass.rs b/src/validators/dataclass.rs index c74d789ec..00ea88f81 100644 --- a/src/validators/dataclass.rs +++ b/src/validators/dataclass.rs @@ -155,6 +155,7 @@ impl Validator for DataclassArgsValidator { let mut used_keys: AHashSet<&str> = AHashSet::with_capacity(self.fields.len()); let state = &mut state.rebind_extra(|extra| extra.data = Some(output_dict.clone())); + let extra_behavior = state.extra_behavior_or(self.extra_behavior); let validate_by_alias = state.validate_by_alias_or(self.validate_by_alias); let validate_by_name = state.validate_by_name_or(self.validate_by_name); @@ -308,7 +309,7 @@ impl Validator for DataclassArgsValidator { Ok(either_str) => { if !used_keys.contains(either_str.as_cow()?.as_ref()) { // Unknown / extra field - match self.extra_behavior { + match extra_behavior { ExtraBehavior::Forbid => { errors.push(ValLineError::new_with_loc( ErrorTypeDefaults::UnexpectedKeywordArgument, @@ -379,6 +380,7 @@ impl Validator for DataclassArgsValidator { state: &mut ValidationState<'_, 'py>, ) -> ValResult { let dict = obj.downcast::()?; + let extra_behavior = state.extra_behavior_or(self.extra_behavior); let ok = |output: PyObject| { dict.set_item(field_name, output)?; @@ -426,7 +428,7 @@ impl Validator for DataclassArgsValidator { // Handle extra (unknown) field // We partially use the extra_behavior for initialization / validation // to determine how to handle assignment - match self.extra_behavior { + match extra_behavior { // For dataclasses we allow assigning unknown fields // to match stdlib dataclass behavior ExtraBehavior::Allow => ok(field_value.clone().unbind()), diff --git a/src/validators/generator.rs b/src/validators/generator.rs index b4be4c68f..1720827b6 100644 --- a/src/validators/generator.rs +++ b/src/validators/generator.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use pyo3::types::{PyDict, PyString}; use pyo3::{prelude::*, IntoPyObjectExt, PyTraverseError, PyVisit}; +use crate::build_tools::ExtraBehavior; use crate::errors::{ErrorType, LocItem, ValError, ValResult}; use crate::input::{BorrowInput, GenericIterator, Input}; use crate::py_gc::PyGcTraverse; @@ -220,6 +221,7 @@ pub struct InternalValidator { // TODO, do we need data? data: Option>, strict: Option, + extra_behavior: Option, from_attributes: Option, context: Option, self_instance: Option, @@ -252,6 +254,7 @@ impl InternalValidator { validator, data: extra.data.as_ref().map(|d| d.clone().into()), strict: extra.strict, + extra_behavior: extra.extra_behavior, from_attributes: extra.from_attributes, context: extra.context.map(|d| d.clone().unbind()), self_instance: extra.self_instance.map(|d| d.clone().unbind()), @@ -277,6 +280,7 @@ impl InternalValidator { input_type: self.validation_mode, data: self.data.as_ref().map(|data| data.bind(py).clone()), strict: self.strict, + extra_behavior: self.extra_behavior, from_attributes: self.from_attributes, field_name: Some(PyString::new(py, field_name)), context: self.context.as_ref().map(|data| data.bind(py)), @@ -315,6 +319,7 @@ impl InternalValidator { input_type: self.validation_mode, data: self.data.as_ref().map(|data| data.bind(py).clone()), strict: self.strict, + extra_behavior: self.extra_behavior, from_attributes: self.from_attributes, field_name: None, context: self.context.as_ref().map(|data| data.bind(py)), diff --git a/src/validators/mod.rs b/src/validators/mod.rs index 0e497f634..5cbe22b61 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -1,4 +1,5 @@ use std::fmt::Debug; +use std::str::FromStr; use enum_dispatch::enum_dispatch; use jiter::{PartialMode, StringCacheMode}; @@ -10,7 +11,7 @@ use pyo3::types::{PyAny, PyDict, PyString, PyTuple, PyType}; use pyo3::{intern, PyTraverseError, PyVisit}; use pyo3::{prelude::*, IntoPyObjectExt}; -use crate::build_tools::{py_schema_err, py_schema_error_type, SchemaError}; +use crate::build_tools::{py_schema_err, py_schema_error_type, ExtraBehavior, SchemaError}; use crate::definitions::{Definitions, DefinitionsBuilder}; use crate::errors::{LocItem, ValError, ValResult, ValidationError}; use crate::input::{Input, InputType, StringMapping}; @@ -160,12 +161,13 @@ impl SchemaValidator { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (input, *, strict=None, from_attributes=None, context=None, self_instance=None, allow_partial=PartialMode::Off, by_alias=None, by_name=None))] + #[pyo3(signature = (input, *, strict=None, extra=None, from_attributes=None, context=None, self_instance=None, allow_partial=PartialMode::Off, by_alias=None, by_name=None))] pub fn validate_python( &self, py: Python, input: &Bound<'_, PyAny>, strict: Option, + extra: Option<&Bound<'_, PyString>>, from_attributes: Option, context: Option<&Bound<'_, PyAny>>, self_instance: Option<&Bound<'_, PyAny>>, @@ -173,12 +175,15 @@ impl SchemaValidator { by_alias: Option, by_name: Option, ) -> PyResult { + let extra_behavior = extra.map(|e| ExtraBehavior::from_str(e.to_str()?)).transpose()?; + #[allow(clippy::used_underscore_items)] self._validate( py, input, InputType::Python, strict, + extra_behavior, from_attributes, context, self_instance, @@ -190,24 +195,28 @@ impl SchemaValidator { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (input, *, strict=None, from_attributes=None, context=None, self_instance=None, by_alias=None, by_name=None))] + #[pyo3(signature = (input, *, strict=None, extra=None, from_attributes=None, context=None, self_instance=None, by_alias=None, by_name=None))] pub fn isinstance_python( &self, py: Python, input: &Bound<'_, PyAny>, strict: Option, + extra: Option<&Bound<'_, PyString>>, from_attributes: Option, context: Option<&Bound<'_, PyAny>>, self_instance: Option<&Bound<'_, PyAny>>, by_alias: Option, by_name: Option, ) -> PyResult { + let extra_behavior = extra.map(|e| ExtraBehavior::from_str(e.to_str()?)).transpose()?; + #[allow(clippy::used_underscore_items)] match self._validate( py, input, InputType::Python, strict, + extra_behavior, from_attributes, context, self_instance, @@ -224,18 +233,21 @@ impl SchemaValidator { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (input, *, strict=None, context=None, self_instance=None, allow_partial=PartialMode::Off, by_alias=None, by_name=None))] + #[pyo3(signature = (input, *, strict=None, extra=None, context=None, self_instance=None, allow_partial=PartialMode::Off, by_alias=None, by_name=None))] pub fn validate_json( &self, py: Python, input: &Bound<'_, PyAny>, strict: Option, + extra: Option<&Bound<'_, PyString>>, context: Option<&Bound<'_, PyAny>>, self_instance: Option<&Bound<'_, PyAny>>, allow_partial: PartialMode, by_alias: Option, by_name: Option, ) -> PyResult { + let extra_behavior = extra.map(|e| ExtraBehavior::from_str(e.to_str()?)).transpose()?; + let r = match json::validate_json_bytes(input) { #[allow(clippy::used_underscore_items)] Ok(v_match) => self._validate_json( @@ -243,6 +255,7 @@ impl SchemaValidator { input, v_match.into_inner().as_slice(), strict, + extra_behavior, context, self_instance, allow_partial, @@ -255,12 +268,13 @@ impl SchemaValidator { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (input, *, strict=None, context=None, allow_partial=PartialMode::Off, by_alias=None, by_name=None))] + #[pyo3(signature = (input, *, strict=None, extra=None, context=None, allow_partial=PartialMode::Off, by_alias=None, by_name=None))] pub fn validate_strings( &self, py: Python, input: Bound<'_, PyAny>, strict: Option, + extra: Option<&Bound<'_, PyString>>, context: Option<&Bound<'_, PyAny>>, allow_partial: PartialMode, by_alias: Option, @@ -268,6 +282,7 @@ impl SchemaValidator { ) -> PyResult { let t = InputType::String; let string_mapping = StringMapping::new_value(input).map_err(|e| self.prepare_validation_err(py, e, t))?; + let extra_behavior = extra.map(|e| ExtraBehavior::from_str(e.to_str()?)).transpose()?; #[allow(clippy::used_underscore_items)] match self._validate( @@ -275,6 +290,7 @@ impl SchemaValidator { &string_mapping, t, strict, + extra_behavior, None, context, None, @@ -288,7 +304,7 @@ impl SchemaValidator { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (obj, field_name, field_value, *, strict=None, from_attributes=None, context=None, by_alias=None, by_name=None))] + #[pyo3(signature = (obj, field_name, field_value, *, strict=None, extra=None,from_attributes=None, context=None, by_alias=None, by_name=None))] pub fn validate_assignment( &self, py: Python, @@ -296,15 +312,19 @@ impl SchemaValidator { field_name: &str, field_value: Bound<'_, PyAny>, strict: Option, + extra: Option<&Bound<'_, PyString>>, from_attributes: Option, context: Option<&Bound<'_, PyAny>>, by_alias: Option, by_name: Option, ) -> PyResult { + let extra_behavior = extra.map(|e| ExtraBehavior::from_str(e.to_str()?)).transpose()?; + let extra = Extra { input_type: InputType::Python, data: None, strict, + extra_behavior, from_attributes, field_name: Some(PyString::new(py, field_name)), context, @@ -332,6 +352,7 @@ impl SchemaValidator { input_type: InputType::Python, data: None, strict, + extra_behavior: None, from_attributes: None, field_name: None, context, @@ -389,6 +410,7 @@ impl SchemaValidator { input: &(impl Input<'py> + ?Sized), input_type: InputType, strict: Option, + extra_behavior: Option, from_attributes: Option, context: Option<&Bound<'py, PyAny>>, self_instance: Option<&Bound<'py, PyAny>>, @@ -400,6 +422,7 @@ impl SchemaValidator { let mut state = ValidationState::new( Extra::new( strict, + extra_behavior, from_attributes, context, self_instance, @@ -421,6 +444,7 @@ impl SchemaValidator { input: &Bound<'_, PyAny>, json_data: &[u8], strict: Option, + extra_behavior: Option, context: Option<&Bound<'_, PyAny>>, self_instance: Option<&Bound<'_, PyAny>>, allow_partial: PartialMode, @@ -435,6 +459,7 @@ impl SchemaValidator { &json_value, InputType::Json, strict, + extra_behavior, None, context, self_instance, @@ -477,7 +502,17 @@ impl<'py> SelfValidator<'py> { let py = schema.py(); let mut recursion_guard = RecursionState::default(); let mut state = ValidationState::new( - Extra::new(strict, None, None, None, InputType::Python, true.into(), None, None), + Extra::new( + strict, + None, + None, + None, + None, + InputType::Python, + true.into(), + None, + None, + ), &mut recursion_guard, false.into(), ); @@ -697,6 +732,9 @@ pub struct Extra<'a, 'py> { pub data: Option>, /// whether we're in strict or lax mode pub strict: Option, + /// The behavior for handling extra fields + #[allow(clippy::struct_field_names)] + pub extra_behavior: Option, /// Validation time setting of `from_attributes` pub from_attributes: Option, /// context used in validator functions @@ -717,6 +755,7 @@ impl<'a, 'py> Extra<'a, 'py> { #[allow(clippy::too_many_arguments)] pub fn new( strict: Option, + extra_behavior: Option, from_attributes: Option, context: Option<&'a Bound<'py, PyAny>>, self_instance: Option<&'a Bound<'py, PyAny>>, @@ -729,6 +768,7 @@ impl<'a, 'py> Extra<'a, 'py> { input_type, data: None, strict, + extra_behavior, from_attributes, field_name: None, context, @@ -746,6 +786,7 @@ impl Extra<'_, '_> { input_type: self.input_type, data: self.data.clone(), strict: Some(true), + extra_behavior: self.extra_behavior, from_attributes: self.from_attributes, field_name: self.field_name.clone(), context: self.context, diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index 10d13d7b8..1c7524696 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -131,6 +131,7 @@ impl Validator for ModelFieldsValidator { state.allow_partial = false.into(); let strict = state.strict_or(self.strict); + let extra_behavior = state.extra_behavior_or(self.extra_behavior); let from_attributes = state.extra().from_attributes.unwrap_or(self.from_attributes); // we convert the DictType error to a ModelType error @@ -167,12 +168,12 @@ impl Validator for ModelFieldsValidator { // we only care about which keys have been used if we're iterating over the object for extra after // the first pass - let mut used_keys: Option> = - if self.extra_behavior == ExtraBehavior::Ignore || dict.is_py_get_attr() { - None - } else { - Some(AHashSet::with_capacity(self.fields.len())) - }; + let mut used_keys: Option> = if extra_behavior == ExtraBehavior::Ignore || dict.is_py_get_attr() + { + None + } else { + Some(AHashSet::with_capacity(self.fields.len())) + }; { let state = &mut state.rebind_extra(|extra| extra.data = Some(model_dict.clone())); @@ -351,13 +352,13 @@ impl Validator for ModelFieldsValidator { used_keys, errors: &mut errors, fields_set_vec: &mut fields_set_vec, - extra_behavior: self.extra_behavior, + extra_behavior, extras_validator: self.extras_validator.as_deref(), extras_keys_validator: self.extras_keys_validator.as_deref(), state, })??; - if matches!(self.extra_behavior, ExtraBehavior::Allow) { + if matches!(extra_behavior, ExtraBehavior::Allow) { model_extra_dict_op = Some(model_extra_dict); } } @@ -370,7 +371,7 @@ impl Validator for ModelFieldsValidator { // if we have extra=allow, but we didn't create a dict because we were validating // from attributes, set it now so __pydantic_extra__ is always a dict if extra=allow - if matches!(self.extra_behavior, ExtraBehavior::Allow) && model_extra_dict_op.is_none() { + if matches!(extra_behavior, ExtraBehavior::Allow) && model_extra_dict_op.is_none() { model_extra_dict_op = Some(PyDict::new(py)); } @@ -387,6 +388,7 @@ impl Validator for ModelFieldsValidator { state: &mut ValidationState<'_, 'py>, ) -> ValResult { let dict = obj.downcast::()?; + let extra_behavior = state.extra_behavior_or(self.extra_behavior); let get_updated_dict = |output: &Bound<'py, PyAny>| { dict.set_item(field_name, output)?; @@ -435,7 +437,7 @@ impl Validator for ModelFieldsValidator { // to determine how to handle assignment // For models / typed dicts we forbid assigning extra attributes // unless the user explicitly set extra_behavior to 'allow' - match self.extra_behavior { + match extra_behavior { ExtraBehavior::Allow => match self.extras_validator { Some(ref validator) => prepare_result(validator.validate(py, field_value, state))?, None => get_updated_dict(field_value)?, @@ -454,7 +456,7 @@ impl Validator for ModelFieldsValidator { } }; - let new_extra = match &self.extra_behavior { + let new_extra = match &extra_behavior { ExtraBehavior::Allow => { let non_extra_data = PyDict::new(py); self.fields.iter().try_for_each(|f| -> PyResult<()> { diff --git a/src/validators/typed_dict.rs b/src/validators/typed_dict.rs index eb4394e39..4f8c89d66 100644 --- a/src/validators/typed_dict.rs +++ b/src/validators/typed_dict.rs @@ -156,6 +156,7 @@ impl Validator for TypedDictValidator { state: &mut ValidationState<'_, 'py>, ) -> ValResult { let strict = state.strict_or(self.strict); + let extra_behavior = state.extra_behavior_or(self.extra_behavior); let dict = input.validate_dict(strict)?; let output_dict = PyDict::new(py); @@ -173,12 +174,12 @@ impl Validator for TypedDictValidator { // we only care about which keys have been used if we're iterating over the object for extra after // the first pass - let mut used_keys: Option> = - if self.extra_behavior == ExtraBehavior::Ignore || dict.is_py_get_attr() { - None - } else { - Some(AHashSet::with_capacity(self.fields.len())) - }; + let mut used_keys: Option> = if extra_behavior == ExtraBehavior::Ignore || dict.is_py_get_attr() + { + None + } else { + Some(AHashSet::with_capacity(self.fields.len())) + }; { let state = &mut state.rebind_extra(|extra| extra.data = Some(output_dict.clone())); @@ -366,7 +367,7 @@ impl Validator for TypedDictValidator { extras_validator: self.extras_validator.as_deref(), output_dict: &output_dict, state, - extra_behavior: self.extra_behavior, + extra_behavior, partial_last_key, allow_partial, })??; diff --git a/src/validators/validation_state.rs b/src/validators/validation_state.rs index b5bf37f48..ed14f2f13 100644 --- a/src/validators/validation_state.rs +++ b/src/validators/validation_state.rs @@ -3,6 +3,7 @@ use pyo3::types::PyString; use jiter::{PartialMode, StringCacheMode}; +use crate::build_tools::ExtraBehavior; use crate::recursion_guard::{ContainsRecursionState, RecursionState}; use crate::tools::new_py_string; @@ -64,6 +65,10 @@ impl<'a, 'py> ValidationState<'a, 'py> { self.extra.strict.unwrap_or(default) } + pub fn extra_behavior_or(&self, default: ExtraBehavior) -> ExtraBehavior { + self.extra.extra_behavior.unwrap_or(default) + } + pub fn validate_by_alias_or(&self, default: Option) -> bool { self.extra.by_alias.or(default).unwrap_or(true) } diff --git a/tests/conftest.py b/tests/conftest.py index e9079c8ba..478da16d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ import pytest from pydantic_core import ArgsKwargs, CoreSchema, SchemaValidator, ValidationError, validate_core_schema -from pydantic_core.core_schema import CoreConfig +from pydantic_core.core_schema import CoreConfig, ExtraBehavior __all__ = 'Err', 'PyAndJson', 'assert_gc', 'is_free_threaded', 'plain_repr', 'infinite_generator' @@ -73,14 +73,19 @@ def validate_python(self, py_input, strict: bool | None = None, context: Any = N def validate_json(self, json_str: str, strict: bool | None = None, context: Any = None): return self.validator.validate_json(json_str, strict=strict, context=context) - def validate_test(self, py_input, strict: bool | None = None, context: Any = None): + def validate_test( + self, py_input, strict: bool | None = None, context: Any = None, extra: ExtraBehavior | None = None + ): if self.validator_type == 'json': return self.validator.validate_json( - json.dumps(py_input, default=json_default), strict=strict, context=context + json.dumps(py_input, default=json_default), + strict=strict, + extra=extra, + context=context, ) else: assert self.validator_type == 'python', self.validator_type - return self.validator.validate_python(py_input, strict=strict, context=context) + return self.validator.validate_python(py_input, strict=strict, context=context, extra=extra) def isinstance_test(self, py_input, strict: bool | None = None, context: Any = None): if self.validator_type == 'json': diff --git a/tests/test.rs b/tests/test.rs index 58e2904f5..c0537412a 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -136,7 +136,7 @@ json_input = '{"a": "something"}' let json_input = locals.get_item("json_input").unwrap().unwrap(); let binding = SchemaValidator::py_new(py, &schema, None) .unwrap() - .validate_json(py, &json_input, None, None, None, false.into(), None, None) + .validate_json(py, &json_input, None, None, None, None, false.into(), None, None) .unwrap(); let validation_result: Bound<'_, PyAny> = binding.extract(py).unwrap(); let repr = format!("{}", validation_result.repr().unwrap()); diff --git a/tests/test_isinstance.py b/tests/test_isinstance.py index 8368e36e8..c2836596e 100644 --- a/tests/test_isinstance.py +++ b/tests/test_isinstance.py @@ -30,6 +30,15 @@ def test_isinstance_strict(): assert v.isinstance_python('123') is False +def test_isinstance_forbid_extra_fn_override(): + v = SchemaValidator(cs.typed_dict_schema({'f': cs.typed_dict_field(cs.str_schema())})) + + with pytest.raises(ValidationError, match='Extra inputs are not permitted'): + v.validate_python({'f': 'x', 'extra_field': '123'}, extra='forbid') + + assert v.isinstance_python({'f': 'x', 'extra_field': '123'}, extra='forbid') is False + + def test_internal_error(): v = SchemaValidator( cs.model_schema(cls=int, schema=cs.model_fields_schema(fields={'f': cs.model_field(schema=cs.int_schema())})) diff --git a/tests/test_validate_strings.py b/tests/test_validate_strings.py index 0e0350d0b..ff654e8b7 100644 --- a/tests/test_validate_strings.py +++ b/tests/test_validate_strings.py @@ -119,3 +119,16 @@ def test_typed_dict(): assert m2 == {'field_a': 1, 'field_b': date(2017, 1, 1)} m2 = v.validate_strings({'field_a': '1', 'field_b': '2017-01-01'}, strict=True) assert m2 == {'field_a': 1, 'field_b': date(2017, 1, 1)} + + +def test_validate_strings_forbid_extra_fn_override(): + v = SchemaValidator( + core_schema.typed_dict_schema( + { + 'f': core_schema.typed_dict_field(core_schema.int_schema()), + } + ) + ) + + with pytest.raises(ValidationError, match='Extra inputs are not permitted'): + v.validate_strings({'f': '1', 'extra_field': '123'}, extra='forbid') diff --git a/tests/validators/arguments_v3/test_extra.py b/tests/validators/arguments_v3/test_extra.py index 749c3f342..3512efc6f 100644 --- a/tests/validators/arguments_v3/test_extra.py +++ b/tests/validators/arguments_v3/test_extra.py @@ -1,3 +1,5 @@ +from typing import Any, Union + import pytest from pydantic_core import ArgsKwargs, ValidationError @@ -6,6 +8,13 @@ from ...conftest import PyAndJson +@pytest.mark.parametrize( + 'schema_extra_behavior,validate_fn_extra_kw', + [ + ({'extra_behavior': 'forbid'}, None), + ({'extra_behavior': 'ignore'}, 'forbid'), + ], +) @pytest.mark.parametrize( ['input_value', 'err_type'], ( @@ -15,19 +24,25 @@ [{'a': 1, 'c': 3, 'extra': 'value'}, 'extra_forbidden'], ), ) -def test_extra_forbid(py_and_json: PyAndJson, input_value, err_type) -> None: +def test_extra_forbid( + py_and_json: PyAndJson, + schema_extra_behavior: dict[str, Any], + validate_fn_extra_kw: Union[cs.ExtraBehavior, None], + input_value, + err_type, +) -> None: v = py_and_json( cs.arguments_v3_schema( [ cs.arguments_v3_parameter(name='a', schema=cs.int_schema()), cs.arguments_v3_parameter(name='b', schema=cs.int_schema(), alias='c'), ], - extra_behavior='forbid', + **schema_extra_behavior, ), ) with pytest.raises(ValidationError) as exc_info: - v.validate_test(input_value) + v.validate_test(input_value, extra=validate_fn_extra_kw) error = exc_info.value.errors()[0] diff --git a/tests/validators/test_dataclasses.py b/tests/validators/test_dataclasses.py index da4105a55..b9a4642d7 100644 --- a/tests/validators/test_dataclasses.py +++ b/tests/validators/test_dataclasses.py @@ -9,6 +9,7 @@ from dirty_equals import IsListOrTuple, IsStr from pydantic_core import ArgsKwargs, SchemaValidator, ValidationError, core_schema +from pydantic_core.core_schema import ExtraBehavior from ..conftest import Err, PyAndJson, assert_gc, is_free_threaded @@ -948,16 +949,25 @@ class MyModel: @pytest.mark.parametrize( - 'config,schema_extra_behavior_kw', + 'config,schema_extra_behavior_kw,validate_fn_extra_kw', [ - (core_schema.CoreConfig(extra_fields_behavior='ignore'), {}), - (core_schema.CoreConfig(extra_fields_behavior='ignore'), {'extra_behavior': None}), - (core_schema.CoreConfig(), {'extra_behavior': 'ignore'}), - (None, {'extra_behavior': 'ignore'}), - (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': 'ignore'}), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {}, None), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {'extra_behavior': None}, None), + (core_schema.CoreConfig(), {'extra_behavior': 'ignore'}, None), + (None, {'extra_behavior': 'ignore'}, None), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': 'ignore'}, None), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {}, 'ignore'), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': None}, 'ignore'), + (core_schema.CoreConfig(), {'extra_behavior': 'allow'}, 'ignore'), + (None, {'extra_behavior': 'allow'}, 'ignore'), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': 'allow'}, 'ignore'), ], ) -def test_extra_behavior_ignore(config: Union[core_schema.CoreConfig, None], schema_extra_behavior_kw: dict[str, Any]): +def test_extra_behavior_ignore( + config: Union[core_schema.CoreConfig, None], + schema_extra_behavior_kw: dict[str, Any], + validate_fn_extra_kw: Union[ExtraBehavior, None], +): @dataclasses.dataclass class MyModel: f: str @@ -973,15 +983,15 @@ class MyModel: config=config, ) - m: MyModel = v.validate_python({'f': 'x', 'extra_field': 123}) + m: MyModel = v.validate_python({'f': 'x', 'extra_field': 123}, extra=validate_fn_extra_kw) assert m.f == 'x' assert not hasattr(m, 'extra_field') - v.validate_assignment(m, 'f', 'y') + v.validate_assignment(m, 'f', 'y', extra=validate_fn_extra_kw) assert m.f == 'y' with pytest.raises(ValidationError) as exc_info: - v.validate_assignment(m, 'not_f', 'xyz') + v.validate_assignment(m, 'not_f', 'xyz', extra=validate_fn_extra_kw) assert exc_info.value.errors(include_url=False) == [ { @@ -996,16 +1006,28 @@ class MyModel: @pytest.mark.parametrize( - 'config,schema_extra_behavior_kw', + 'config,schema_extra_behavior_kw,validate_fn_extra_kw', [ - (core_schema.CoreConfig(extra_fields_behavior='forbid'), {}), - (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': None}), - (core_schema.CoreConfig(), {'extra_behavior': 'forbid'}), - (None, {'extra_behavior': 'forbid'}), - (core_schema.CoreConfig(extra_fields_behavior='ignore'), {'extra_behavior': 'forbid'}), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {}, None), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': None}, None), + (core_schema.CoreConfig(), {'extra_behavior': 'forbid'}, None), + (None, {'extra_behavior': 'forbid'}, None), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {'extra_behavior': 'forbid'}, None), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {}, 'forbid'), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {'extra_behavior': None}, 'forbid'), + (core_schema.CoreConfig(), {'extra_behavior': 'ignore'}, 'forbid'), + (None, {'extra_behavior': 'ignore'}, 'forbid'), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': 'ignore'}, 'forbid'), + (core_schema.CoreConfig(), {}, 'forbid'), + (core_schema.CoreConfig(), {'extra_behavior': None}, 'forbid'), + (None, {'extra_behavior': None}, 'forbid'), ], ) -def test_extra_behavior_forbid(config: Union[core_schema.CoreConfig, None], schema_extra_behavior_kw: dict[str, Any]): +def test_extra_behavior_forbid( + config: Union[core_schema.CoreConfig, None], + schema_extra_behavior_kw: dict[str, Any], + validate_fn_extra_kw: Union[ExtraBehavior, None], +): @dataclasses.dataclass class MyModel: f: str @@ -1021,14 +1043,14 @@ class MyModel: config=config, ) - m: MyModel = v.validate_python({'f': 'x'}) + m: MyModel = v.validate_python({'f': 'x'}, extra=validate_fn_extra_kw) assert m.f == 'x' - v.validate_assignment(m, 'f', 'y') + v.validate_assignment(m, 'f', 'y', extra=validate_fn_extra_kw) assert m.f == 'y' with pytest.raises(ValidationError) as exc_info: - v.validate_assignment(m, 'not_f', 'xyz') + v.validate_assignment(m, 'not_f', 'xyz', extra=validate_fn_extra_kw) assert exc_info.value.errors(include_url=False) == [ { 'type': 'no_such_attribute', @@ -1042,16 +1064,28 @@ class MyModel: @pytest.mark.parametrize( - 'config,schema_extra_behavior_kw', + 'config,schema_extra_behavior_kw,validate_fn_extra_kw', [ - (core_schema.CoreConfig(extra_fields_behavior='allow'), {}), - (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': None}), - (core_schema.CoreConfig(), {'extra_behavior': 'allow'}), - (None, {'extra_behavior': 'allow'}), - (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': 'allow'}), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {}, None), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': None}, None), + (core_schema.CoreConfig(), {'extra_behavior': 'allow'}, None), + (None, {'extra_behavior': 'allow'}, None), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': 'allow'}, None), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {}, 'allow'), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': None}, 'allow'), + (core_schema.CoreConfig(), {'extra_behavior': 'forbid'}, 'allow'), + (None, {'extra_behavior': 'forbid'}, 'allow'), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {'extra_behavior': 'forbid'}, 'allow'), + (core_schema.CoreConfig(), {}, 'allow'), + (core_schema.CoreConfig(), {'extra_behavior': None}, 'allow'), + (None, {'extra_behavior': None}, 'allow'), ], ) -def test_extra_behavior_allow(config: Union[core_schema.CoreConfig, None], schema_extra_behavior_kw: dict[str, Any]): +def test_extra_behavior_allow( + config: Union[core_schema.CoreConfig, None], + schema_extra_behavior_kw: dict[str, Any], + validate_fn_extra_kw: Union[ExtraBehavior, None], +): @dataclasses.dataclass class MyModel: f: str @@ -1067,14 +1101,14 @@ class MyModel: ) ) - m: MyModel = v.validate_python({'f': 'x', 'extra_field': '123'}) + m: MyModel = v.validate_python({'f': 'x', 'extra_field': '123'}, extra=validate_fn_extra_kw) assert m.f == 'x' assert getattr(m, 'extra_field') == '123' - v.validate_assignment(m, 'f', 'y') + v.validate_assignment(m, 'f', 'y', extra=validate_fn_extra_kw) assert m.f == 'y' - v.validate_assignment(m, 'not_f', '123') + v.validate_assignment(m, 'not_f', '123', extra=validate_fn_extra_kw) assert getattr(m, 'not_f') == '123' diff --git a/tests/validators/test_model.py b/tests/validators/test_model.py index 253b8a734..dfadf71a1 100644 --- a/tests/validators/test_model.py +++ b/tests/validators/test_model.py @@ -2,12 +2,13 @@ import sys from copy import deepcopy from decimal import Decimal -from typing import Any, Callable +from typing import Any, Callable, Union import pytest from dirty_equals import HasRepr, IsInstance from pydantic_core import CoreConfig, SchemaError, SchemaValidator, ValidationError, core_schema +from pydantic_core.core_schema import ExtraBehavior def test_model_class(): @@ -46,7 +47,14 @@ class MyModel: assert m2.__dict__ == {'field_a': 'test', 'field_b': 12} -def test_model_class_extra(): +@pytest.mark.parametrize( + 'schema_extra_behavior,validate_fn_extra_kw', + [ + ({'extra_behavior': 'allow'}, None), + ({'extra_behavior': 'ignore'}, 'allow'), + ], +) +def test_model_class_extra(schema_extra_behavior: dict[str, Any], validate_fn_extra_kw: Union[ExtraBehavior, None]): class MyModel: # this is not required, but it avoids `__pydantic_fields_set__` being included in `__dict__` __slots__ = '__dict__', '__pydantic_fields_set__', '__pydantic_extra__', '__pydantic_private__' @@ -61,11 +69,11 @@ class MyModel: 'field_a': core_schema.model_field(core_schema.str_schema()), 'field_b': core_schema.model_field(core_schema.int_schema()), }, - extra_behavior='allow', + **schema_extra_behavior, ), ) ) - m = v.validate_python({'field_a': 'test', 'field_b': 12, 'field_c': 'extra'}) + m = v.validate_python({'field_a': 'test', 'field_b': 12, 'field_c': 'extra'}, extra=validate_fn_extra_kw) assert isinstance(m, MyModel) assert m.field_a == 'test' assert m.field_b == 12 @@ -74,7 +82,16 @@ class MyModel: assert m.__dict__ == {'field_a': 'test', 'field_b': 12} -def test_model_class_extra_forbid(): +@pytest.mark.parametrize( + 'schema_extra_behavior,validate_fn_extra_kw', + [ + ({'extra_behavior': 'forbid'}, None), + ({'extra_behavior': 'ignore'}, 'forbid'), + ], +) +def test_model_class_extra_forbid( + schema_extra_behavior: dict[str, Any], validate_fn_extra_kw: Union[ExtraBehavior, None] +): class MyModel: class Meta: pass @@ -102,20 +119,20 @@ def __getattr__(self, key): 'field_a': core_schema.model_field(core_schema.str_schema()), 'field_b': core_schema.model_field(core_schema.int_schema()), }, - extra_behavior='forbid', + **schema_extra_behavior, ), ) ) - m = v.validate_python({'field_a': 'test', 'field_b': 12}) + m = v.validate_python({'field_a': 'test', 'field_b': 12}, extra=validate_fn_extra_kw) assert isinstance(m, MyModel) assert m.field_a == 'test' assert m.field_b == 12 # try revalidating from the model's attributes - m = v.validate_python(Wrapper(m), from_attributes=True) + m = v.validate_python(Wrapper(m), from_attributes=True, extra=validate_fn_extra_kw) with pytest.raises(ValidationError) as exc_info: - m = v.validate_python({'field_a': 'test', 'field_b': 12, 'field_c': 'extra'}) + m = v.validate_python({'field_a': 'test', 'field_b': 12, 'field_c': 'extra'}, extra=validate_fn_extra_kw) assert exc_info.value.errors(include_url=False) == [ {'type': 'extra_forbidden', 'loc': ('field_c',), 'msg': 'Extra inputs are not permitted', 'input': 'extra'} diff --git a/tests/validators/test_model_fields.py b/tests/validators/test_model_fields.py index cc04f07c1..47edc1fb4 100644 --- a/tests/validators/test_model_fields.py +++ b/tests/validators/test_model_fields.py @@ -10,6 +10,7 @@ from dirty_equals import FunctionCheck, HasRepr, IsStr from pydantic_core import CoreConfig, SchemaError, SchemaValidator, ValidationError, core_schema, validate_core_schema +from pydantic_core.core_schema import ExtraBehavior from ..conftest import Err, PyAndJson @@ -228,6 +229,12 @@ def test_allow_extra_wrong(): ) +def test_allow_extra_fn_override_wrong(): + v = SchemaValidator(schema=core_schema.model_fields_schema(fields={})) + with pytest.raises(SchemaError, match='Invalid extra_behavior: `wrong`'): + v.validate_python({}, extra='wrong') + + def test_str_config(): v = SchemaValidator( core_schema.model_fields_schema(fields={'field_a': core_schema.model_field(schema=core_schema.str_schema())}), @@ -1674,6 +1681,9 @@ def test_extra_behavior_allow( assert new_fields_set == {'not_f'} +# We can't test the extra parameter of the validate_* functions above, since the +# extras_schema parameter isn't valid unless the models are configured with extra='allow'. +# Test the validate_* extra parameter separately instead: @pytest.mark.parametrize( 'config,schema_extra_behavior_kw', [ @@ -1681,10 +1691,16 @@ def test_extra_behavior_allow( (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': None}), (core_schema.CoreConfig(), {'extra_behavior': 'forbid'}), (None, {'extra_behavior': 'forbid'}), - (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': 'forbid'}), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {'extra_behavior': 'forbid'}), + (core_schema.CoreConfig(), {}), + (core_schema.CoreConfig(), {'extra_behavior': None}), + (None, {'extra_behavior': None}), ], ) -def test_extra_behavior_forbid(config: Union[core_schema.CoreConfig, None], schema_extra_behavior_kw: dict[str, Any]): +def test_extra_behavior_allow_with_validate_fn_override( + config: Union[core_schema.CoreConfig, None], + schema_extra_behavior_kw: dict[str, Any], +): v = SchemaValidator( core_schema.model_fields_schema( {'f': core_schema.model_field(core_schema.str_schema())}, **schema_extra_behavior_kw @@ -1692,21 +1708,65 @@ def test_extra_behavior_forbid(config: Union[core_schema.CoreConfig, None], sche config=config, ) - m, model_extra, fields_set = v.validate_python({'f': 'x'}) + m, model_extra, fields_set = v.validate_python({'f': 'x', 'extra_field': '123'}, extra='allow') + assert m == {'f': 'x'} + assert model_extra == {'extra_field': '123'} + assert fields_set == {'f', 'extra_field'} + + v.validate_assignment(m, 'f', 'y', extra='allow') + assert m == {'f': 'y'} + + new_m, new_model_extra, new_fields_set = v.validate_assignment({**m, **model_extra}, 'not_f', '123', extra='allow') + assert new_m == {'f': 'y'} + assert new_model_extra == {'extra_field': '123', 'not_f': '123'} + assert new_fields_set == {'not_f'} + + +@pytest.mark.parametrize( + 'config,schema_extra_behavior_kw,validate_fn_extra_kw', + [ + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {}, None), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': None}, None), + (core_schema.CoreConfig(), {'extra_behavior': 'forbid'}, None), + (None, {'extra_behavior': 'forbid'}, None), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': 'forbid'}, None), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {}, 'forbid'), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {'extra_behavior': None}, 'forbid'), + (core_schema.CoreConfig(), {'extra_behavior': 'ignore'}, 'forbid'), + (None, {'extra_behavior': 'ignore'}, 'forbid'), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': 'ignore'}, 'forbid'), + (core_schema.CoreConfig(), {}, 'forbid'), + (core_schema.CoreConfig(), {'extra_behavior': None}, 'forbid'), + (None, {'extra_behavior': None}, 'forbid'), + ], +) +def test_extra_behavior_forbid( + config: Union[core_schema.CoreConfig, None], + schema_extra_behavior_kw: dict[str, Any], + validate_fn_extra_kw: Union[ExtraBehavior, None], +): + v = SchemaValidator( + core_schema.model_fields_schema( + {'f': core_schema.model_field(core_schema.str_schema())}, **schema_extra_behavior_kw + ), + config=config, + ) + + m, model_extra, fields_set = v.validate_python({'f': 'x'}, extra=validate_fn_extra_kw) assert m == {'f': 'x'} assert fields_set == {'f'} with pytest.raises(ValidationError) as exc_info: - v.validate_python({'f': 'x', 'extra_field': 123}) + v.validate_python({'f': 'x', 'extra_field': 123}, extra=validate_fn_extra_kw) assert exc_info.value.errors(include_url=False) == [ {'type': 'extra_forbidden', 'loc': ('extra_field',), 'msg': 'Extra inputs are not permitted', 'input': 123} ] - v.validate_assignment(m, 'f', 'y') + v.validate_assignment(m, 'f', 'y', extra=validate_fn_extra_kw) assert m['f'] == 'y' with pytest.raises(ValidationError) as exc_info: - v.validate_assignment(m, 'not_f', 'xyz') + v.validate_assignment(m, 'not_f', 'xyz', extra=validate_fn_extra_kw) assert exc_info.value.errors(include_url=False) == [ { 'type': 'no_such_attribute', @@ -1720,18 +1780,26 @@ def test_extra_behavior_forbid(config: Union[core_schema.CoreConfig, None], sche @pytest.mark.parametrize( - 'config,schema_extra_behavior_kw', + 'config,schema_extra_behavior_kw,validate_fn_extra_kw', [ - (core_schema.CoreConfig(extra_fields_behavior='ignore'), {}), - (core_schema.CoreConfig(), {'extra_behavior': 'ignore'}), - (None, {'extra_behavior': 'ignore'}), - (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': 'ignore'}), - (core_schema.CoreConfig(), {}), - (core_schema.CoreConfig(), {'extra_behavior': None}), - (None, {'extra_behavior': None}), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {}, None), + (core_schema.CoreConfig(), {'extra_behavior': 'ignore'}, None), + (None, {'extra_behavior': 'ignore'}, None), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': 'ignore'}, None), + (core_schema.CoreConfig(), {}, None), + (core_schema.CoreConfig(), {'extra_behavior': None}, None), + (None, {'extra_behavior': None}, None), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {}, 'ignore'), + (core_schema.CoreConfig(), {'extra_behavior': 'allow'}, 'ignore'), + (None, {'extra_behavior': 'allow'}, 'ignore'), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': 'allow'}, 'ignore'), ], ) -def test_extra_behavior_ignore(config: Union[core_schema.CoreConfig, None], schema_extra_behavior_kw: dict[str, Any]): +def test_extra_behavior_ignore( + config: Union[core_schema.CoreConfig, None], + schema_extra_behavior_kw: dict[str, Any], + validate_fn_extra_kw: Union[ExtraBehavior, None], +): v = SchemaValidator( core_schema.model_fields_schema( {'f': core_schema.model_field(core_schema.str_schema())}, **schema_extra_behavior_kw @@ -1739,12 +1807,12 @@ def test_extra_behavior_ignore(config: Union[core_schema.CoreConfig, None], sche config=config, ) - m, model_extra, fields_set = v.validate_python({'f': 'x', 'extra_field': 123}) + m, model_extra, fields_set = v.validate_python({'f': 'x', 'extra_field': 123}, extra=validate_fn_extra_kw) assert m == {'f': 'x'} assert model_extra is None assert fields_set == {'f'} - v.validate_assignment(m, 'f', 'y') + v.validate_assignment(m, 'f', 'y', extra=validate_fn_extra_kw) assert m['f'] == 'y' # even if we ignore extra attributes during initialization / validation @@ -1752,7 +1820,7 @@ def test_extra_behavior_ignore(config: Union[core_schema.CoreConfig, None], sche # instead if extra='ignore' was set (or nothing was set since that's the default) # we treat it as if it were extra='forbid' with pytest.raises(ValidationError) as exc_info: - v.validate_assignment(m, 'not_f', 'xyz') + v.validate_assignment(m, 'not_f', 'xyz', extra=validate_fn_extra_kw) assert exc_info.value.errors(include_url=False) == [ { 'type': 'no_such_attribute', diff --git a/tests/validators/test_typed_dict.py b/tests/validators/test_typed_dict.py index b224b65ea..0034bf71e 100644 --- a/tests/validators/test_typed_dict.py +++ b/tests/validators/test_typed_dict.py @@ -9,6 +9,7 @@ from dirty_equals import FunctionCheck from pydantic_core import CoreConfig, SchemaError, SchemaValidator, ValidationError, core_schema, validate_core_schema +from pydantic_core.core_schema import ExtraBehavior from ..conftest import Err, PyAndJson, assert_gc @@ -166,15 +167,22 @@ def test_ignore_extra(): assert v.validate_python({'field_a': b'123', 'field_b': 1, 'field_c': 123}) == {'field_a': '123', 'field_b': 1} -def test_forbid_extra(): +@pytest.mark.parametrize( + 'schema_extra_behavior,validate_fn_extra_kw', + [ + ({'extra_behavior': 'forbid'}, None), + ({}, 'forbid'), + ], +) +def test_forbid_extra(schema_extra_behavior: dict[str, Any], validate_fn_extra_kw: Union[ExtraBehavior, None]): v = SchemaValidator( core_schema.typed_dict_schema( - fields={'field_a': core_schema.typed_dict_field(schema=core_schema.str_schema())}, extra_behavior='forbid' + fields={'field_a': core_schema.typed_dict_field(schema=core_schema.str_schema())}, **schema_extra_behavior ) ) with pytest.raises(ValidationError) as exc_info: - v.validate_python({'field_a': 'abc', 'field_b': 1}) + v.validate_python({'field_a': 'abc', 'field_b': 1}, extra=validate_fn_extra_kw) assert exc_info.value.errors(include_url=False) == [ {'type': 'extra_forbidden', 'loc': ('field_b',), 'msg': 'Extra inputs are not permitted', 'input': 1} @@ -1117,6 +1125,9 @@ def test_extra_behavior_allow( assert m == {'f': 'x', 'extra_field': expected_extra_value} +# We can't test the extra parameter of the validate_* functions above, since the +# extras_schema parameter isn't valid unless the models are configured with extra='allow'. +# Test the validate_* extra parameter separately here instead: @pytest.mark.parametrize( 'config,schema_extra_behavior_kw', [ @@ -1124,39 +1135,90 @@ def test_extra_behavior_allow( (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': None}), (core_schema.CoreConfig(), {'extra_behavior': 'forbid'}), (None, {'extra_behavior': 'forbid'}), - (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': 'forbid'}), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {'extra_behavior': 'forbid'}), + (core_schema.CoreConfig(), {}), + (core_schema.CoreConfig(), {'extra_behavior': None}), + (None, {'extra_behavior': None}), ], ) -def test_extra_behavior_forbid(config: Union[core_schema.CoreConfig, None], schema_extra_behavior_kw: dict[str, Any]): +def test_extra_behavior_allow_with_validate_fn_override( + config: Union[core_schema.CoreConfig, None], + schema_extra_behavior_kw: dict[str, Any], +): v = SchemaValidator( core_schema.typed_dict_schema( - {'f': core_schema.typed_dict_field(core_schema.str_schema())}, **schema_extra_behavior_kw, config=config + {'f': core_schema.typed_dict_field(core_schema.str_schema())}, + **schema_extra_behavior_kw, + config=config, ) ) - m: dict[str, Any] = v.validate_python({'f': 'x'}) + m: dict[str, Any] = v.validate_python({'f': 'x', 'extra_field': '123'}, extra='allow') + assert m == {'f': 'x', 'extra_field': '123'} + + +@pytest.mark.parametrize( + 'config,schema_extra_behavior_kw,validate_fn_extra_kw', + [ + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {}, None), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': None}, None), + (core_schema.CoreConfig(), {'extra_behavior': 'forbid'}, None), + (None, {'extra_behavior': 'forbid'}, None), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': 'forbid'}, None), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {}, 'forbid'), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {'extra_behavior': None}, 'forbid'), + (core_schema.CoreConfig(), {'extra_behavior': 'ignore'}, 'forbid'), + (None, {'extra_behavior': 'ignore'}, 'forbid'), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {'extra_behavior': 'ignore'}, 'forbid'), + (core_schema.CoreConfig(), {}, 'forbid'), + (core_schema.CoreConfig(), {'extra_behavior': None}, 'forbid'), + (None, {'extra_behavior': None}, 'forbid'), + ], +) +def test_extra_behavior_forbid( + config: Union[core_schema.CoreConfig, None], + schema_extra_behavior_kw: dict[str, Any], + validate_fn_extra_kw: Union[ExtraBehavior, None], +): + v = SchemaValidator( + core_schema.typed_dict_schema( + {'f': core_schema.typed_dict_field(core_schema.str_schema())}, + **schema_extra_behavior_kw, + config=config, + ) + ) + + m: dict[str, Any] = v.validate_python({'f': 'x'}, extra=validate_fn_extra_kw) assert m == {'f': 'x'} with pytest.raises(ValidationError) as exc_info: - v.validate_python({'f': 'x', 'extra_field': 123}) + v.validate_python({'f': 'x', 'extra_field': 123}, extra=validate_fn_extra_kw) assert exc_info.value.errors(include_url=False) == [ {'type': 'extra_forbidden', 'loc': ('extra_field',), 'msg': 'Extra inputs are not permitted', 'input': 123} ] @pytest.mark.parametrize( - 'config,schema_extra_behavior_kw', + 'config,schema_extra_behavior_kw,validate_fn_extra_kw', [ - (core_schema.CoreConfig(extra_fields_behavior='ignore'), {}), - (core_schema.CoreConfig(), {'extra_behavior': 'ignore'}), - (None, {'extra_behavior': 'ignore'}), - (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': 'ignore'}), - (core_schema.CoreConfig(), {}), - (core_schema.CoreConfig(), {'extra_behavior': None}), - (None, {'extra_behavior': None}), + (core_schema.CoreConfig(extra_fields_behavior='ignore'), {}, None), + (core_schema.CoreConfig(), {'extra_behavior': 'ignore'}, None), + (None, {'extra_behavior': 'ignore'}, None), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': 'ignore'}, None), + (core_schema.CoreConfig(), {}, None), + (core_schema.CoreConfig(), {'extra_behavior': None}, None), + (None, {'extra_behavior': None}, None), + (core_schema.CoreConfig(extra_fields_behavior='allow'), {}, 'ignore'), + (core_schema.CoreConfig(), {'extra_behavior': 'allow'}, 'ignore'), + (None, {'extra_behavior': 'allow'}, 'ignore'), + (core_schema.CoreConfig(extra_fields_behavior='forbid'), {'extra_behavior': 'allow'}, 'ignore'), ], ) -def test_extra_behavior_ignore(config: Union[core_schema.CoreConfig, None], schema_extra_behavior_kw: dict[str, Any]): +def test_extra_behavior_ignore( + config: Union[core_schema.CoreConfig, None], + schema_extra_behavior_kw: dict[str, Any], + validate_fn_extra_kw: Union[ExtraBehavior, None], +): v = SchemaValidator( core_schema.typed_dict_schema( {'f': core_schema.typed_dict_field(core_schema.str_schema())}, **schema_extra_behavior_kw @@ -1164,7 +1226,7 @@ def test_extra_behavior_ignore(config: Union[core_schema.CoreConfig, None], sche config=config, ) - m: dict[str, Any] = v.validate_python({'f': 'x', 'extra_field': 123}) + m: dict[str, Any] = v.validate_python({'f': 'x', 'extra_field': 123}, extra=validate_fn_extra_kw) assert m == {'f': 'x'}