From a587ecee88fed075aa5a7ec1abe23422a968b2b1 Mon Sep 17 00:00:00 2001 From: Kyle Amos Date: Thu, 11 Aug 2022 06:49:07 -0400 Subject: [PATCH] Fix #4192 bug with BaseModel.construct and aliased Fields (#4191) * Fix a bug where BaseModel.construct would not appropriately respect field aliases * Perhaps do not raise on construct and just apply the fix * Fix quotes and remove check on allow_population_by_field_name * Fix lint * Fix lint, remove bad arg * Black formatted * Mmmm black formatter and single quotes linting, what a world * Added change file * PR feedback Co-authored-by: Kyle Amos --- changes/4192-kylebamos.md | 1 + pydantic/main.py | 4 +++- tests/test_aliases.py | 39 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 changes/4192-kylebamos.md diff --git a/changes/4192-kylebamos.md b/changes/4192-kylebamos.md new file mode 100644 index 0000000000..daa518d84b --- /dev/null +++ b/changes/4192-kylebamos.md @@ -0,0 +1 @@ +Update BaseModel.construct to work with aliased Fields \ No newline at end of file diff --git a/pydantic/main.py b/pydantic/main.py index 5ce26749d2..fddacd97a0 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -588,7 +588,9 @@ def construct(cls: Type['Model'], _fields_set: Optional['SetStr'] = None, **valu m = cls.__new__(cls) fields_values: Dict[str, Any] = {} for name, field in cls.__fields__.items(): - if name in values: + if field.alt_alias and field.alias in values: + fields_values[name] = values[field.alias] + elif name in values: fields_values[name] = values[name] elif not field.required: fields_values[name] = field.get_default() diff --git a/tests/test_aliases.py b/tests/test_aliases.py index 53a08dc0a1..f37daf1b4a 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -1,5 +1,6 @@ import re -from typing import Any, List, Optional +from contextlib import nullcontext as does_not_raise +from typing import Any, ContextManager, List, Optional import pytest @@ -345,3 +346,39 @@ class Model(BaseModel): m = Model(**data) assert m.empty_string_key == 123 assert m.dict(by_alias=True) == data + + +@pytest.mark.parametrize( + 'use_construct, allow_population_by_field_name_config, arg_name, expectation', + [ + [False, True, 'bar', does_not_raise()], + [False, True, 'bar_', does_not_raise()], + [False, False, 'bar', does_not_raise()], + [False, False, 'bar_', pytest.raises(ValueError)], + [True, True, 'bar', does_not_raise()], + [True, True, 'bar_', does_not_raise()], + [True, False, 'bar', does_not_raise()], + [True, False, 'bar_', does_not_raise()], + ], +) +def test_allow_population_by_field_name_config( + use_construct: bool, + allow_population_by_field_name_config: bool, + arg_name: str, + expectation: ContextManager, +): + expected_value: int = 7 + + class Foo(BaseModel): + bar_: int = Field(..., alias='bar') + + class Config(BaseConfig): + allow_population_by_field_name = allow_population_by_field_name_config + + with expectation: + if use_construct: + f = Foo.construct(**{arg_name: expected_value}) + else: + f = Foo(**{arg_name: expected_value}) + + assert f.bar_ == expected_value