Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Only mark model as complete once all fields are complete #11759

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

Merged
merged 5 commits into from
Apr 17, 2025

Conversation

DouweM
Copy link
Contributor

@DouweM DouweM commented Apr 15, 2025

Change Summary

BaseModel.model_rebuild already ensured that __pydantic_fields_complete__ was True before marking the model as complete, including rebuilding model fields to 1) see if any unknown annotations could not be resolved, and 2) if not, to make sure that the mock error message includes the name of the unknown annotation.

We now do the same on ModelMetaclass.__new__, to make sure that subclasses of incomplete superclasses don't incorrectly get marked as complete.

Here is such an example:

class MyModel(BaseModel):
    sub_models: list['SubModel']

class SubModel(MyModel):
    pass

Related issue number

Preparation for #11453

Checklist

  • The pull request title is a good summary of the changes - it will be used in the changelog
  • Unit tests for the changes exist
  • Tests pass on CI
  • Documentation reflects the changes where applicable
  • My PR is ready to review, please add a comment including the phrase "please review" to assign reviewers

Selected Reviewer: @sydney-runkle

@github-actions github-actions bot added the relnotes-fix Used for bugfixes. label Apr 15, 2025
@DouweM
Copy link
Contributor Author

DouweM commented Apr 15, 2025

Hmm, we're good on Python 3.11+, but failing on earlier versions...

The issue appears to be that inside FieldInfo.from_annotation, typing._eval_type("list['SubModel']", {}, {}) raises a NameError on 3.11+ because SubModel couldn't be found, but happily returns list['SubModel'] on earlier versions.

This causes Pydantic to leave the FieldInfo as _complete=True, while the failing test expects it to be False:

field_info = FieldInfo_.from_annotation(ann_type, _source=AnnotationSource.CLASS)
if not evaluated:
field_info._complete = False

Pydantic behaves correctly because GenerateSchema still raises PydanticUndefinedAnnotation (it also uses _eval_type, but only after explicitly parsing the type hints in _get_args_resolving_forward_refs), which causes it to call set_model_mocks and leave __pydantic_complete__ False. But it looks like __pydantic_fields_complete__ cannot be fully relied on <3.11.

Interestingly, sub_model: 'SubModel' does cause NameError to be raised by FieldInfo.from_annotation and _complete to be False, because 'SubModel' is turned into ForwardRef('SubModel') by this method:

def _type_convert(arg: Any) -> Any:
"""Convert `None` to `NoneType` and strings to `ForwardRef` instances.
This is a backport of the private `typing._type_convert` function. When
evaluating a type, `ForwardRef._evaluate` ends up being called, and is
responsible for making this conversion. However, we still have to apply
it for the first argument passed to our type evaluation functions, similarly
to the `typing.get_type_hints` function.
"""
if arg is None:
return NoneType
if isinstance(arg, str):
# Like `typing.get_type_hints`, assume the arg can be in any context,
# hence the proper `is_argument` and `is_class` args:
return _make_forward_ref(arg, is_argument=False, is_class=True)
return arg

For this PR, I'll change it from list['SubModel'] to 'SubModel', but if we want to be able to trust FieldInfo._complete and __pydantic_fields_complete__ on <3.11, we may want to apply logic similar to _get_args_resolving_forward_refs before we call typing._eval_type to turn strs inside generics (like list) into ForwardRefs as well.

@Viicos What do you think?

Copy link

codspeed-hq bot commented Apr 15, 2025

CodSpeed Performance Report

Merging #11759 will not alter performance

Comparing DouweM:model-fields-complete-mismatch (ed18a83) with main (eb295f2)

Summary

✅ 46 untouched benchmarks

Copy link
Contributor

github-actions bot commented Apr 15, 2025

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  pydantic/_internal
  _model_construction.py
Project Total  

This report was generated by python-coverage-comment-action

@DouweM DouweM changed the title Only mark model as complete once all fields are complete Add __pydantic_on_complete__ hook that is called once model is fully ready to be used Apr 16, 2025
@DouweM
Copy link
Contributor Author

DouweM commented Apr 16, 2025

@Viicos Please review!

@Viicos
Copy link
Member

Viicos commented Apr 16, 2025

The issue appears to be that inside FieldInfo.from_annotation, typing._eval_type("list['SubModel']", {}, {}) raises a NameError on 3.11+ because SubModel couldn't be found, but happily returns list['SubModel'] on earlier versions.

This is a bug that was fixed in python/cpython#30900. iirc, this unfortunately can't be backported to typing_extensions because it would require copying a lot of code from typing. Ideally, we should have our own typing._eval_type() implementation, but the logic is already messy with the eval_type_backport integration. Anyway, I think I'll give it another shot once a fix (python/cpython#131583) gets accepted upstream.

but if we want to be able to trust FieldInfo._complete and __pydantic_fields_complete__ on <3.11, we may want to apply logic similar to _get_args_resolving_forward_refs before we call typing._eval_type to turn strs inside generics (like list) into ForwardRefs as well.

I think we can live with the inconsistency here. The primary goal of FieldInfo._complete is to indicate whether we should recreate the instance (by calling FieldInfo.from_annotation()/from_annotated_attribute() using the temp. FieldInfo._original_annotation/_original_assignment attrs.). While a FieldInfo with an annotation like list['Forward'] is currently treated as _complete on < 3.11, there will be no need to rebuild the instance as the forward annotation is "inside" list, and won't affect FieldInfo attributes. On the other hand, a FieldInfo instance with an annotation like "Annotated[Forward, Field(alias='a')]" will have attributes (in this case, alias) altered when being rebuilt with the evaluated annotation.

pydantic/_internal/_model_construction.py Show resolved Hide resolved
@Viicos Viicos added relnotes-feature and removed relnotes-fix Used for bugfixes. labels Apr 16, 2025
@DouweM DouweM force-pushed the model-fields-complete-mismatch branch from 0c9ec0f to dd5f919 Compare April 16, 2025 19:25
@DouweM DouweM changed the title Add __pydantic_on_complete__ hook that is called once model is fully ready to be used Only mark model as complete once all fields are complete Apr 16, 2025
@DouweM
Copy link
Contributor Author

DouweM commented Apr 16, 2025

@Viicos I've limited this PR to the fields/model complete mismatch issue, and split out the new hook into #11762 (to be updated and merged after this).

Copy link
Member

@Viicos Viicos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise looks good

pydantic/_internal/_model_construction.py Show resolved Hide resolved
pydantic/_internal/_model_construction.py Outdated Show resolved Hide resolved
@Viicos Viicos added change Suggested alteration to Pydantic, not a new feature nor a bug relnotes-fix Used for bugfixes. third-party-tests Add this label on a PR to trigger 3rd party tests and removed relnotes-feature change Suggested alteration to Pydantic, not a new feature nor a bug labels Apr 17, 2025
@Viicos Viicos closed this Apr 17, 2025
@Viicos Viicos reopened this Apr 17, 2025
@DouweM
Copy link
Contributor Author

DouweM commented Apr 17, 2025

@Viicos Back to you!

@DouweM
Copy link
Contributor Author

DouweM commented Apr 17, 2025

The failing Pandera test is already being fixed in #11767.

@DouweM DouweM force-pushed the model-fields-complete-mismatch branch from 03eebf1 to ed18a83 Compare April 17, 2025 17:41
Copy link
Member

@Viicos Viicos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @DouweM

@Viicos Viicos merged commit b77ad73 into pydantic:main Apr 17, 2025
86 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ready for review relnotes-fix Used for bugfixes. third-party-tests Add this label on a PR to trigger 3rd party tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.