Skip to content

Commit

Permalink
Fix validation_alias behavior with model_construct for `AliasChoi…
Browse files Browse the repository at this point in the history
…ces` and `AliasPath` (#9223)
  • Loading branch information
sydney-runkle committed Apr 12, 2024
1 parent 59eace3 commit 6aab43e
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 11 deletions.
21 changes: 20 additions & 1 deletion pydantic/aliases.py
Expand Up @@ -2,7 +2,9 @@
from __future__ import annotations

import dataclasses
from typing import Callable, Literal
from typing import Any, Callable, Literal

from pydantic_core import PydanticUndefined

from ._internal import _internal_dataclass

Expand Down Expand Up @@ -32,6 +34,23 @@ def convert_to_aliases(self) -> list[str | int]:
"""
return self.path

def search_dict_for_path(self, d: dict) -> Any:
"""Searches a dictionary for the path specified by the alias.
Returns:
The value at the specified path, or `PydanticUndefined` if the path is not found.
"""
v = d
for k in self.path:
if isinstance(v, str):
# disallow indexing into a str, like for AliasPath('x', 0) and x='abc'
return PydanticUndefined
try:
v = v[k]
except (KeyError, IndexError, TypeError):
return PydanticUndefined
return v


@dataclasses.dataclass(**_internal_dataclass.slots_true)
class AliasChoices:
Expand Down
37 changes: 28 additions & 9 deletions pydantic/main.py
Expand Up @@ -27,6 +27,7 @@
_utils,
)
from ._migration import getattr_migration
from .aliases import AliasChoices, AliasPath
from .annotated_handlers import GetCoreSchemaHandler, GetJsonSchemaHandler
from .config import ConfigDict
from .errors import PydanticUndefinedAnnotation, PydanticUserError
Expand Down Expand Up @@ -197,7 +198,7 @@ def model_fields_set(self) -> set[str]:
return self.__pydantic_fields_set__

@classmethod
def model_construct(cls: type[Model], _fields_set: set[str] | None = None, **values: Any) -> Model:
def model_construct(cls: type[Model], _fields_set: set[str] | None = None, **values: Any) -> Model: # noqa: C901
"""Creates a new instance of the `Model` class with validated data.
Creates a new model setting `__dict__` and `__pydantic_fields_set__` from trusted or pre-validated data.
Expand Down Expand Up @@ -225,14 +226,32 @@ def model_construct(cls: type[Model], _fields_set: set[str] | None = None, **val
if field.alias is not None and field.alias in values:
fields_values[name] = values.pop(field.alias)
fields_set.add(name)
elif field.validation_alias is not None and field.validation_alias in values:
fields_values[name] = values.pop(field.validation_alias)
fields_set.add(name)
elif name in values:
fields_values[name] = values.pop(name)
fields_set.add(name)
elif not field.is_required():
fields_values[name] = field.get_default(call_default_factory=True)

if (name not in fields_set) and (field.validation_alias is not None):
validation_aliases: list[str | AliasPath] = (
field.validation_alias.choices
if isinstance(field.validation_alias, AliasChoices)
else [field.validation_alias]
)

for alias in validation_aliases:
if isinstance(alias, str) and alias in values:
fields_values[name] = values.pop(alias)
fields_set.add(name)
break
elif isinstance(alias, AliasPath):
value = alias.search_dict_for_path(values)
if value is not PydanticUndefined:
fields_values[name] = value
fields_set.add(name)
break

if name not in fields_set:
if name in values:
fields_values[name] = values.pop(name)
fields_set.add(name)
elif not field.is_required():
fields_values[name] = field.get_default(call_default_factory=True)
if _fields_set is None:
_fields_set = fields_set

Expand Down
28 changes: 27 additions & 1 deletion tests/test_construction.py
Expand Up @@ -4,7 +4,7 @@
import pytest
from pydantic_core import PydanticUndefined, ValidationError

from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, PydanticDeprecatedSince20
from pydantic import AliasChoices, AliasPath, BaseModel, ConfigDict, Field, PrivateAttr, PydanticDeprecatedSince20


class Model(BaseModel):
Expand Down Expand Up @@ -561,3 +561,29 @@ class MyModel(BaseModel):

assert m._a == 'a'
assert '_a' in m.__pydantic_private__


def test_model_construct_with_alias_choices() -> None:
class MyModel(BaseModel):
a: str = Field(validation_alias=AliasChoices('aaa', 'AAA'))

assert MyModel.model_construct(a='a_value').a == 'a_value'
assert MyModel.model_construct(aaa='a_value').a == 'a_value'
assert MyModel.model_construct(AAA='a_value').a == 'a_value'


def test_model_construct_with_alias_path() -> None:
class MyModel(BaseModel):
a: str = Field(validation_alias=AliasPath('aaa', 'AAA'))

assert MyModel.model_construct(a='a_value').a == 'a_value'
assert MyModel.model_construct(aaa={'AAA': 'a_value'}).a == 'a_value'


def test_model_construct_with_alias_choices_and_path() -> None:
class MyModel(BaseModel):
a: str = Field(validation_alias=AliasChoices('aaa', AliasPath('AAA', 'aaa')))

assert MyModel.model_construct(a='a_value').a == 'a_value'
assert MyModel.model_construct(aaa='a_value').a == 'a_value'
assert MyModel.model_construct(AAA={'aaa': 'a_value'}).a == 'a_value'

0 comments on commit 6aab43e

Please sign in to comment.