Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update create_model() to support typing.Annotated as input #8947

Merged
merged 11 commits into from Mar 18, 2024
16 changes: 15 additions & 1 deletion pydantic/main.py
Expand Up @@ -1433,7 +1433,8 @@ def create_model( # noqa: C901
__cls_kwargs__: A dictionary of keyword arguments for class creation, such as `metaclass`.
__slots__: Deprecated. Should not be passed to `create_model`.
**field_definitions: Attributes of the new model. They should be passed in the format:
`<name>=(<type>, <default value>)` or `<name>=(<type>, <FieldInfo>)`.
`<name>=(<type>, <default value>)`, `<name>=(<type>, <FieldInfo>)`, or `typing.Annotated[<type>, <FieldInfo>]`.
Any additional metadata in `typing.Annotated[<type>, <FieldInfo>, ...]` will be ignored.
wannieman98 marked this conversation as resolved.
Show resolved Hide resolved

Returns:
The new [model][pydantic.BaseModel].
Expand Down Expand Up @@ -1473,6 +1474,19 @@ def create_model( # noqa: C901
'Field definitions should be a `(<type>, <default>)`.',
code='create-model-field-definitions',
) from e

elif _typing_extra.is_annotated(f_def):
(f_annotation, f_value, *_) = typing_extensions.get_args(
f_def
) # first two input are expected from Annotated
wannieman98 marked this conversation as resolved.
Show resolved Hide resolved
from .fields import FieldInfo

if not isinstance(f_value, FieldInfo):
raise PydanticUserError(
'Field definitions should be a Annotated[<type>, FieldInfo(...)]',
sydney-runkle marked this conversation as resolved.
Show resolved Hide resolved
code='create-model-field-definitions',
wannieman98 marked this conversation as resolved.
Show resolved Hide resolved
)

else:
f_annotation, f_value = None, f_def

Expand Down
54 changes: 54 additions & 0 deletions tests/test_create_model.py
@@ -1,8 +1,11 @@
import platform
import re
import sys
import typing
from typing import Generic, Optional, Tuple, TypeVar

import pytest
import typing_extensions

from pydantic import (
BaseModel,
Expand Down Expand Up @@ -516,6 +519,57 @@ def test_create_model_non_annotated():
create_model('FooModel', foo=(str, ...), bar=123)


@pytest.mark.skipif(sys.version_info < (3, 9), reason='typing.Annotated is introduced after python3.9')
wannieman98 marked this conversation as resolved.
Show resolved Hide resolved
@pytest.mark.parametrize(
'annotation_type,field_info',
[
(bool, Field(alias='foo_bool_alias', description='foo boolean')),
(str, Field(alias='foo_str_alis', description='foo string')),
],
)
def test_create_model_typing_annotated_field_info(annotation_type, field_info):
annotated_foo = typing.Annotated[annotation_type, field_info]
model = create_model('FooModel', foo=annotated_foo, bar=(int, 123))

assert model.model_fields.keys() == {'foo', 'bar'}

foo = model.model_fields.get('foo')

assert foo is not None
assert foo.annotation == annotation_type
assert foo.alias == field_info.alias
assert foo.description == field_info.description


@pytest.mark.skipif(sys.version_info < (3, 9), reason='typing.Annotated is introduced after python3.9')
def test_create_model_expect_field_info_as_metadata_typing():
annotated_foo = typing_extensions.Annotated[int, 10]

with pytest.raises(PydanticUserError):
wannieman98 marked this conversation as resolved.
Show resolved Hide resolved
create_model('FooModel', foo=annotated_foo)


def test_create_model_expect_field_info_as_metadata_typing_extensions():
annotated_foo = typing_extensions.Annotated[int, 10]

with pytest.raises(PydanticUserError):
create_model('FooModel', foo=annotated_foo)


@pytest.mark.parametrize(
'annotation_type,field_info', [(bool, Field(alias='foo_bool_alias')), (str, Field(alias='foo_str_alias'))]
)
def test_create_model_typing_extension_field_info(annotation_type, field_info):
annotated_foo = typing_extensions.Annotated[annotation_type, field_info]
model = create_model('FooModel', foo=annotated_foo)

foo = model.model_fields.get('foo')

assert foo is not None
assert foo.annotation == annotation_type
assert foo.alias == field_info.alias


def test_create_model_tuple():
model = create_model('FooModel', foo=(Tuple[int, int], (1, 2)))
assert model().foo == (1, 2)
Expand Down