From 8598f3e319998cb2b308e0144015fe6f1501b47a Mon Sep 17 00:00:00 2001 From: sydney-runkle Date: Wed, 19 Feb 2025 14:55:19 -0500 Subject: [PATCH] starting on alias API unification --- python/pydantic_core/core_schema.py | 34 +++++++++++---------------- src/validators/arguments.rs | 6 ++--- src/validators/dataclass.rs | 4 ++-- src/validators/model_fields.rs | 4 ++-- src/validators/typed_dict.rs | 6 ++--- tests/validators/test_arguments.py | 4 ++-- tests/validators/test_model_fields.py | 16 ++++++------- tests/validators/test_typed_dict.py | 12 +++++----- 8 files changed, 40 insertions(+), 46 deletions(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index e999bdcfc..e908dc337 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -54,8 +54,6 @@ class CoreConfig(TypedDict, total=False): `field_names` to construct error `loc`s. Default is `True`. revalidate_instances: Whether instances of models and dataclasses should re-validate. Default is 'never'. validate_default: Whether to validate default values during validation. Default is `False`. - populate_by_name: Whether an aliased field may be populated by its name as given by the model attribute, - as well as the alias. (Replaces 'allow_population_by_field_name' in Pydantic v1.) Default is `False`. str_max_length: The maximum length for string fields. str_min_length: The minimum length for string fields. str_strip_whitespace: Whether to strip whitespace from string fields. @@ -74,6 +72,9 @@ class CoreConfig(TypedDict, total=False): regex_engine: The regex engine to use for regex pattern validation. Default is 'rust-regex'. See `StringSchema`. cache_strings: Whether to cache strings. Default is `True`, `True` or `'all'` is required to cache strings during general validation since validators don't know if they're in a key or a value. + validate_by_alias: Whether to validate by alias. Default is `True`. + validate_by_name: Whether to validate by attribute name. Default is `False`. Replacement for `populate_by_name`. + serialize_by_alias: Whether to serialize by alias. Default is `False`, expected to change to `True` in V3. """ title: str @@ -91,7 +92,6 @@ class CoreConfig(TypedDict, total=False): # whether to validate default values during validation, default False validate_default: bool # used on typed-dicts and arguments - populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1 # fields related to string fields only str_max_length: int str_min_length: int @@ -111,6 +111,9 @@ class CoreConfig(TypedDict, total=False): coerce_numbers_to_str: bool # default: False regex_engine: Literal['rust-regex', 'python-re'] # default: 'rust-regex' cache_strings: Union[bool, Literal['all', 'keys', 'none']] # default: 'True' + validate_by_alias: bool # default: True + validate_by_name: bool # default: False + serialize_by_alias: bool # default: False IncExCall: TypeAlias = 'set[int | str] | dict[int | str, IncExCall] | None' @@ -2888,7 +2891,6 @@ class TypedDictSchema(TypedDict, total=False): # all these values can be set via config, equivalent fields have `typed_dict_` prefix extra_behavior: ExtraBehavior total: bool # default: True - populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1 ref: str metadata: dict[str, Any] serialization: SerSchema @@ -2904,7 +2906,6 @@ def typed_dict_schema( extras_schema: CoreSchema | None = None, extra_behavior: ExtraBehavior | None = None, total: bool | None = None, - populate_by_name: bool | None = None, ref: str | None = None, metadata: dict[str, Any] | None = None, serialization: SerSchema | None = None, @@ -2938,7 +2939,6 @@ class MyTypedDict(TypedDict): metadata: Any other information you want to include with the schema, not used by pydantic-core extra_behavior: The extra behavior to use for the typed dict total: Whether the typed dict is total, otherwise uses `typed_dict_total` from config - populate_by_name: Whether the typed dict should populate by name serialization: Custom serialization schema """ return _dict_not_none( @@ -2950,7 +2950,6 @@ class MyTypedDict(TypedDict): extras_schema=extras_schema, extra_behavior=extra_behavior, total=total, - populate_by_name=populate_by_name, ref=ref, metadata=metadata, serialization=serialization, @@ -3012,9 +3011,7 @@ class ModelFieldsSchema(TypedDict, total=False): computed_fields: list[ComputedField] strict: bool extras_schema: CoreSchema - # all these values can be set via config, equivalent fields have `typed_dict_` prefix extra_behavior: ExtraBehavior - populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1 from_attributes: bool ref: str metadata: dict[str, Any] @@ -3029,7 +3026,6 @@ def model_fields_schema( strict: bool | None = None, extras_schema: CoreSchema | None = None, extra_behavior: ExtraBehavior | None = None, - populate_by_name: bool | None = None, from_attributes: bool | None = None, ref: str | None = None, metadata: dict[str, Any] | None = None, @@ -3058,7 +3054,6 @@ def model_fields_schema( 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 extra_behavior: The extra behavior to use for the typed dict - populate_by_name: Whether the typed dict should populate by name from_attributes: Whether the typed dict should be populated from attributes serialization: Custom serialization schema """ @@ -3070,7 +3065,6 @@ def model_fields_schema( strict=strict, extras_schema=extras_schema, extra_behavior=extra_behavior, - populate_by_name=populate_by_name, from_attributes=from_attributes, ref=ref, metadata=metadata, @@ -3254,7 +3248,6 @@ class DataclassArgsSchema(TypedDict, total=False): dataclass_name: Required[str] fields: Required[list[DataclassField]] computed_fields: list[ComputedField] - populate_by_name: bool # default: False collect_init_only: bool # default: False ref: str metadata: dict[str, Any] @@ -3267,7 +3260,6 @@ def dataclass_args_schema( fields: list[DataclassField], *, computed_fields: list[ComputedField] | None = None, - populate_by_name: bool | None = None, collect_init_only: bool | None = None, ref: str | None = None, metadata: dict[str, Any] | None = None, @@ -3295,7 +3287,6 @@ def dataclass_args_schema( dataclass_name: The name of the dataclass being validated fields: The fields to use for the dataclass computed_fields: Computed fields to use when serializing the dataclass - populate_by_name: Whether to populate by name collect_init_only: Whether to collect init only fields into a dict to pass to `__post_init__` 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 @@ -3307,7 +3298,6 @@ def dataclass_args_schema( dataclass_name=dataclass_name, fields=fields, computed_fields=computed_fields, - populate_by_name=populate_by_name, collect_init_only=collect_init_only, ref=ref, metadata=metadata, @@ -3436,7 +3426,8 @@ def arguments_parameter( class ArgumentsSchema(TypedDict, total=False): type: Required[Literal['arguments']] arguments_schema: Required[list[ArgumentsParameter]] - populate_by_name: bool + validate_by_name: bool + validate_by_alias: bool var_args_schema: CoreSchema var_kwargs_mode: VarKwargsMode var_kwargs_schema: CoreSchema @@ -3448,7 +3439,8 @@ class ArgumentsSchema(TypedDict, total=False): def arguments_schema( arguments: list[ArgumentsParameter], *, - populate_by_name: bool | None = None, + validate_by_name: bool | None = None, + validate_by_alias: bool | None = None, var_args_schema: CoreSchema | None = None, var_kwargs_mode: VarKwargsMode | None = None, var_kwargs_schema: CoreSchema | None = None, @@ -3475,7 +3467,8 @@ def arguments_schema( Args: arguments: The arguments to use for the arguments schema - populate_by_name: Whether to populate by name + validate_by_name: Whether to populate by argument names, defaults to False. + validate_by_alias: Whether to populate by argument aliases, defaults to True. var_args_schema: The variable args schema to use for the arguments schema var_kwargs_mode: The validation mode to use for variadic keyword arguments. If `'uniform'`, every value of the keyword arguments will be validated against the `var_kwargs_schema` schema. If `'unpacked-typed-dict'`, @@ -3488,7 +3481,8 @@ def arguments_schema( return _dict_not_none( type='arguments', arguments_schema=arguments, - populate_by_name=populate_by_name, + validate_by_name=validate_by_name, + validate_by_alias=validate_by_alias, var_args_schema=var_args_schema, var_kwargs_mode=var_kwargs_mode, var_kwargs_schema=var_kwargs_schema, diff --git a/src/validators/arguments.rs b/src/validators/arguments.rs index ef129610a..0f52c8393 100644 --- a/src/validators/arguments.rs +++ b/src/validators/arguments.rs @@ -8,7 +8,7 @@ use ahash::AHashSet; use pyo3::IntoPyObjectExt; use crate::build_tools::py_schema_err; -use crate::build_tools::{schema_or_config_same, ExtraBehavior}; +use crate::build_tools::ExtraBehavior; use crate::errors::{ErrorTypeDefaults, ValError, ValLineError, ValResult}; use crate::input::{Arguments, BorrowInput, Input, KeywordArgs, PositionalArgs, ValidationMatch}; use crate::lookup_key::LookupKey; @@ -68,7 +68,7 @@ impl BuildValidator for ArgumentsValidator { ) -> PyResult { let py = schema.py(); - let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false); + let validate_by_name = config.get_as(intern!(py, "validate_by_name"))?.unwrap_or(false); let arguments_schema: Bound<'_, PyList> = schema.get_as_req(intern!(py, "arguments_schema"))?; let mut parameters: Vec = Vec::with_capacity(arguments_schema.len()); @@ -102,7 +102,7 @@ impl BuildValidator for ArgumentsValidator { if mode == "keyword_only" || mode == "positional_or_keyword" { kw_lookup_key = match arg.get_item(intern!(py, "alias"))? { Some(alias) => { - let alt_alias = if populate_by_name { Some(name.as_str()) } else { None }; + let alt_alias = if validate_by_name { Some(name.as_str()) } else { None }; Some(LookupKey::from_py(py, &alias, alt_alias)?) } None => Some(LookupKey::from_string(py, &name)), diff --git a/src/validators/dataclass.rs b/src/validators/dataclass.rs index a774c6c9a..c9aadc272 100644 --- a/src/validators/dataclass.rs +++ b/src/validators/dataclass.rs @@ -54,7 +54,7 @@ impl BuildValidator for DataclassArgsValidator { ) -> PyResult { let py = schema.py(); - let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false); + let validate_by_name = config.get_as(intern!(py, "validate_by_name"))?.unwrap_or(false); let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?; @@ -77,7 +77,7 @@ impl BuildValidator for DataclassArgsValidator { let lookup_key = match field.get_item(intern!(py, "validation_alias"))? { Some(alias) => { - let alt_alias = if populate_by_name { Some(name.as_str()) } else { None }; + let alt_alias = if validate_by_name { Some(name.as_str()) } else { None }; LookupKey::from_py(py, &alias, alt_alias)? } None => LookupKey::from_string(py, &name), diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index 392760964..9278a3fb5 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -51,7 +51,7 @@ impl BuildValidator for ModelFieldsValidator { let strict = is_strict(schema, config)?; let from_attributes = schema_or_config_same(schema, config, intern!(py, "from_attributes"))?.unwrap_or(false); - let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false); + let validate_by_name = config.get_as(intern!(py, "validate_by_name"))?.unwrap_or(false); let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?; @@ -81,7 +81,7 @@ impl BuildValidator for ModelFieldsValidator { let lookup_key = match field_info.get_item(intern!(py, "validation_alias"))? { Some(alias) => { - let alt_alias = if populate_by_name { Some(field_name) } else { None }; + let alt_alias = if validate_by_name { Some(field_name) } else { None }; LookupKey::from_py(py, &alias, alt_alias)? } None => LookupKey::from_string(py, field_name), diff --git a/src/validators/typed_dict.rs b/src/validators/typed_dict.rs index 3365c0169..994f952f7 100644 --- a/src/validators/typed_dict.rs +++ b/src/validators/typed_dict.rs @@ -3,7 +3,7 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyString}; use crate::build_tools::py_schema_err; -use crate::build_tools::{is_strict, schema_or_config, schema_or_config_same, ExtraBehavior}; +use crate::build_tools::{is_strict, schema_or_config, ExtraBehavior}; use crate::errors::LocItem; use crate::errors::{ErrorTypeDefaults, ValError, ValLineError, ValResult}; use crate::input::BorrowInput; @@ -55,7 +55,7 @@ impl BuildValidator for TypedDictValidator { let total = schema_or_config(schema, config, intern!(py, "total"), intern!(py, "typed_dict_total"))?.unwrap_or(true); - let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false); + let validate_by_name = config.get_as(intern!(py, "validate_by_name"))?.unwrap_or(false); let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?; @@ -110,7 +110,7 @@ impl BuildValidator for TypedDictValidator { let lookup_key = match field_info.get_item(intern!(py, "validation_alias"))? { Some(alias) => { - let alt_alias = if populate_by_name { Some(field_name) } else { None }; + let alt_alias = if validate_by_name { Some(field_name) } else { None }; LookupKey::from_py(py, &alias, alt_alias)? } None => LookupKey::from_string(py, field_name), diff --git a/tests/validators/test_arguments.py b/tests/validators/test_arguments.py index 496363a1f..c3583f652 100644 --- a/tests/validators/test_arguments.py +++ b/tests/validators/test_arguments.py @@ -883,14 +883,14 @@ def test_alias(py_and_json: PyAndJson, input_value, expected): ], ids=repr, ) -def test_alias_populate_by_name(py_and_json: PyAndJson, input_value, expected): +def test_alias_validate_by_name(py_and_json: PyAndJson, input_value, expected): v = py_and_json( { 'type': 'arguments', 'arguments_schema': [ {'name': 'a', 'mode': 'positional_or_keyword', 'schema': {'type': 'int'}, 'alias': 'Foo'} ], - 'populate_by_name': True, + 'validate_by_name': True, } ) if isinstance(expected, Err): diff --git a/tests/validators/test_model_fields.py b/tests/validators/test_model_fields.py index e5dd53994..aac726577 100644 --- a/tests/validators/test_model_fields.py +++ b/tests/validators/test_model_fields.py @@ -506,9 +506,9 @@ def test_alias_allow_pop(py_and_json: PyAndJson): v = py_and_json( { 'type': 'model-fields', - 'populate_by_name': True, 'fields': {'field_a': {'validation_alias': 'FieldA', 'type': 'model-field', 'schema': {'type': 'int'}}}, - } + }, + config=CoreConfig(validate_by_name=True), ) assert v.validate_test({'FieldA': '123'}) == ({'field_a': 123}, None, {'field_a'}) assert v.validate_test({'field_a': '123'}) == ({'field_a': 123}, None, {'field_a'}) @@ -697,8 +697,8 @@ def test_paths_allow_by_name(py_and_json: PyAndJson, input_value): 'schema': {'type': 'int'}, } }, - 'populate_by_name': True, - } + }, + config=CoreConfig(validate_by_name=True), ) assert v.validate_test(input_value) == ({'field_a': 42}, None, {'field_a'}) @@ -985,8 +985,8 @@ def test_from_attributes_by_name(): core_schema.model_fields_schema( fields={'a': core_schema.model_field(schema=core_schema.int_schema(), validation_alias='a_alias')}, from_attributes=True, - populate_by_name=True, - ) + ), + config=CoreConfig(validate_by_name=True), ) assert v.validate_python(Cls(a_alias=1)) == ({'a': 1}, None, {'a'}) assert v.validate_python(Cls(a=1)) == ({'a': 1}, None, {'a'}) @@ -1383,9 +1383,9 @@ def test_alias_extra_by_name(py_and_json: PyAndJson): 'type': 'model-fields', 'extra_behavior': 'allow', 'from_attributes': True, - 'populate_by_name': True, 'fields': {'field_a': {'validation_alias': 'FieldA', 'type': 'model-field', 'schema': {'type': 'int'}}}, - } + }, + config=CoreConfig(validate_by_name=True), ) assert v.validate_test({'FieldA': 1}) == ({'field_a': 1}, {}, {'field_a'}) assert v.validate_test({'field_a': 1}) == ({'field_a': 1}, {}, {'field_a'}) diff --git a/tests/validators/test_typed_dict.py b/tests/validators/test_typed_dict.py index 6e3826a3b..32ad65b0a 100644 --- a/tests/validators/test_typed_dict.py +++ b/tests/validators/test_typed_dict.py @@ -393,11 +393,11 @@ def test_alias_allow_pop(py_and_json: PyAndJson): v = py_and_json( { 'type': 'typed-dict', - 'populate_by_name': True, 'fields': { 'field_a': {'validation_alias': 'FieldA', 'type': 'typed-dict-field', 'schema': {'type': 'int'}} }, - } + }, + config=CoreConfig(validate_by_name=True), ) assert v.validate_test({'FieldA': '123'}) == {'field_a': 123} assert v.validate_test({'field_a': '123'}) == {'field_a': 123} @@ -590,8 +590,8 @@ def test_paths_allow_by_name(py_and_json: PyAndJson, input_value): 'schema': {'type': 'int'}, } }, - 'populate_by_name': True, - } + }, + config=CoreConfig(validate_by_name=True), ) assert v.validate_test(input_value) == {'field_a': 42} @@ -795,11 +795,11 @@ def test_alias_extra_by_name(py_and_json: PyAndJson): { 'type': 'typed-dict', 'extra_behavior': 'allow', - 'populate_by_name': True, 'fields': { 'field_a': {'validation_alias': 'FieldA', 'type': 'typed-dict-field', 'schema': {'type': 'int'}} }, - } + }, + config=CoreConfig(validate_by_name=True), ) assert v.validate_test({'FieldA': 1}) == {'field_a': 1} assert v.validate_test({'field_a': 1}) == {'field_a': 1}