Skip to content

Commit

Permalink
Update create_model() to support typing.Annotated as input (#8947)
Browse files Browse the repository at this point in the history
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>
  • Loading branch information
3 people committed Mar 18, 2024
1 parent 5c8afe6 commit 26a2f06
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 1 deletion.
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.Annotated`][] with invalid input

```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.
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>]',
code='create-model-field-definitions',
)

else:
f_annotation, f_value = None, f_def

Expand Down
29 changes: 29 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,34 @@ 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>\]'):
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

0 comments on commit 26a2f06

Please sign in to comment.