Skip to content

Commit

Permalink
Explicitly raise an error if field names clashes with types (#8243)
Browse files Browse the repository at this point in the history
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
  • Loading branch information
Viicos and sydney-runkle committed Jan 15, 2024
1 parent c7d965c commit 5de67f7
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 18 deletions.
27 changes: 27 additions & 0 deletions docs/errors/usage_errors.md
Expand Up @@ -1018,5 +1018,32 @@ except PydanticUserError as exc_info:
assert exc_info.code == 'root-model-extra'
```

## Cannot evaluate type annotation {#unevaluable-type-annotation}

Because type annotations are evaluated *after* assignments, you might get unexpected results when using a type annotation name
that clashes with one of your fields. We raise an error in the following case:

```py test="skip"
from datetime import date

from pydantic import BaseModel, Field


class Model(BaseModel):
date: date = Field(description='A date')
```

As a workaround, you can either use an alias or change your import:

```py lint="skip"
import datetime
# Or `from datetime import date as _date`

from pydantic import BaseModel, Field


class Model(BaseModel):
date: datetime.date = Field(description='A date')
```

{% endraw %}
1 change: 1 addition & 0 deletions pydantic/errors.py
Expand Up @@ -58,6 +58,7 @@
'invalid_annotated_type',
'type-adapter-config-unused',
'root-model-extra',
'unevaluable-type-annotation',
]


Expand Down
7 changes: 7 additions & 0 deletions pydantic/fields.py
Expand Up @@ -321,6 +321,13 @@ class MyModel(pydantic.BaseModel):
Returns:
A field object with the passed values.
"""
if annotation is default:
raise PydanticUserError(
'Error when building FieldInfo from annotated attribute. '
"Make sure you don't have any field name clashing with a type annotation ",
code='unevaluable-type-annotation',
)

final = False
if _typing_extra.is_finalvar(annotation):
final = True
Expand Down
28 changes: 11 additions & 17 deletions tests/test_edge_cases.py
Expand Up @@ -1375,19 +1375,17 @@ class FooBar:
pass

class Model(BaseModel):
a: int = int
b: Type[int]
c: Type[int] = int
d: FooBar = FooBar
e: Type[FooBar]
f: Type[FooBar] = FooBar
g: Sequence[Type[FooBar]] = [FooBar]
h: Union[Type[FooBar], Sequence[Type[FooBar]]] = FooBar
i: Union[Type[FooBar], Sequence[Type[FooBar]]] = [FooBar]
a: Type[int]
b: Type[int] = int
c: Type[FooBar]
d: Type[FooBar] = FooBar
e: Sequence[Type[FooBar]] = [FooBar]
f: Union[Type[FooBar], Sequence[Type[FooBar]]] = FooBar
g: Union[Type[FooBar], Sequence[Type[FooBar]]] = [FooBar]

model_config = dict(arbitrary_types_allowed=True)

assert Model.model_fields.keys() == set('abcdefghi')
assert Model.model_fields.keys() == set('abcdefg')


def test_assign_type():
Expand Down Expand Up @@ -2436,15 +2434,11 @@ def test_invalid_forward_ref_model():
"""
# The problem:
if sys.version_info >= (3, 11):
error = RecursionError
kwargs = {}
# See PR #8243, this was a RecursionError raised by Python, but is now caught on the Pydantic side
error = errors.PydanticUserError
else:
error = TypeError
kwargs = {
'match': r'Forward references must evaluate to types\.'
r' Got FieldInfo\(annotation=NoneType, required=False\)\.'
}
with pytest.raises(error, **kwargs):
with pytest.raises(error):

class M(BaseModel):
B: ForwardRef('B') = Field(default=None)
Expand Down
16 changes: 15 additions & 1 deletion tests/test_fields.py
@@ -1,7 +1,7 @@
import pytest

import pydantic.dataclasses
from pydantic import BaseModel, Field, RootModel, ValidationError, fields
from pydantic import BaseModel, Field, PydanticUserError, RootModel, ValidationError, fields


def test_field_info_annotation_keyword_argument():
Expand All @@ -17,6 +17,20 @@ def test_field_info_annotation_keyword_argument():
assert e.value.args == ('"annotation" is not permitted as a Field keyword argument',)


def test_field_info_annotated_attribute_name_clashing():
"""This tests that `FieldInfo.from_annotated_attribute` will raise a `PydanticUserError` if attribute names clashes
with a type.
"""

with pytest.raises(PydanticUserError):

class SubModel(BaseModel):
a: int = 1

class Model(BaseModel):
SubModel: SubModel = Field()


def test_init_var_field():
@pydantic.dataclasses.dataclass
class Foo:
Expand Down

0 comments on commit 5de67f7

Please sign in to comment.