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

Fix bug with schema generation with Field(...) in a forward ref #8494

Merged
merged 2 commits into from Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 8 additions & 3 deletions pydantic/_internal/_generate_schema.py
Expand Up @@ -991,16 +991,21 @@ def _common_field_schema( # C901

evaluated = _typing_extra.eval_type_lenient(field_info.annotation, types_namespace, None)
if evaluated is not field_info.annotation and not has_instance_in_type(evaluated, PydanticRecursiveRef):
field_info.annotation = evaluated
new_field_info = FieldInfo.from_annotation(evaluated)
field_info.annotation = new_field_info.annotation

# Handle any field info attributes that may have been obtained from now-resolved annotations
new_field_info = FieldInfo.from_annotation(evaluated)
for k, v in new_field_info._attributes_set.items():
# If an attribute is already set, it means it was set by assigning to a call to Field (or just a
# default value), and that should take the highest priority. So don't overwrite existing attributes.
if k not in field_info._attributes_set:
# We skip over "attributes" that are present in the metadata_lookup dict because these won't
# actually end up as attributes of the `FieldInfo` instance.
if k not in field_info._attributes_set and k not in field_info.metadata_lookup:
setattr(field_info, k, v)

# Finally, ensure the field info also reflects all the `_attributes_set` that are actually metadata.
field_info.metadata = [*new_field_info.metadata, *field_info.metadata]

source_type, annotations = field_info.annotation, field_info.metadata

def set_discriminator(schema: CoreSchema) -> CoreSchema:
Expand Down
46 changes: 46 additions & 0 deletions tests/test_annotated.py
Expand Up @@ -424,3 +424,49 @@ class AnnotatedPrivateFieldModel(BaseModel):

with pytest.raises(AttributeError):
assert model.bar


def test_min_length_field_info_not_lost():
class AnnotatedFieldModel(BaseModel):
foo: 'Annotated[String, Field(min_length=3)]' = Field(description='hello')

String = str

AnnotatedFieldModel.model_rebuild()

assert AnnotatedFieldModel(foo='000').foo == '000'

with pytest.raises(ValidationError) as exc_info:
AnnotatedFieldModel(foo='00')

assert exc_info.value.errors(include_url=False) == [
{
'loc': ('foo',),
'input': '00',
'ctx': {'min_length': 3},
'msg': 'String should have at least 3 characters',
'type': 'string_too_short',
}
]

# Ensure that the inner annotation does not override the outer, even for metadata:
class AnnotatedFieldModel2(BaseModel):
foo: 'Annotated[String, Field(min_length=3)]' = Field(description='hello', min_length=2)

AnnotatedFieldModel2(foo='00')

class AnnotatedFieldModel4(BaseModel):
foo: 'Annotated[String, Field(min_length=3)]' = Field(description='hello', min_length=4)

with pytest.raises(ValidationError) as exc_info:
AnnotatedFieldModel4(foo='00')

assert exc_info.value.errors(include_url=False) == [
{
'loc': ('foo',),
'input': '00',
'ctx': {'min_length': 4},
'msg': 'String should have at least 4 characters',
'type': 'string_too_short',
}
]