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
13 changes: 13 additions & 0 deletions docs/errors/usage_errors.md
Expand Up @@ -628,6 +628,19 @@ except PydanticUserError as exc_info:
assert exc_info.code == 'create-model-field-definitions'
```

Or when you use `typing_extensions.Annotated` module with invalid input
sydney-runkle marked this conversation as resolved.
Show resolved Hide resolved

```py
from typing_extensions import Annotated

from pydantic import PydanticUserError, create_model

try:
create_model('FooModel', foo=Annotated[str, 'NotFieldInfoValue'])
except PydanticUserError as exc_info:
assert exc_info.code == 'create-model-field-definitions'
```

## `create_model` config base {#create-model-config-base}

This error is raised when you use both `__config__` and `__base__` together in `create_model`.
Expand Down
16 changes: 15 additions & 1 deletion pydantic/main.py
Expand Up @@ -1434,7 +1434,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 @@ -1474,6 +1475,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, refer to https://docs.python.org/3/library/typing.html#typing.Annotated
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
31 changes: 31 additions & 0 deletions tests/test_create_model.py
Expand Up @@ -3,6 +3,7 @@
from typing import Generic, Optional, Tuple, TypeVar

import pytest
from typing_extensions import Annotated

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


@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 = 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


def test_create_model_expect_field_info_as_metadata_typing():
annotated_foo = Annotated[int, 10]

with pytest.raises(
PydanticUserError, match=r'Field definitions should be a Annotated\[<type>, FieldInfo\((...)\)\]'
sydney-runkle marked this conversation as resolved.
Show resolved Hide resolved
):
create_model('FooModel', foo=annotated_foo)


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