Skip to content

Refactor FieldInfo creation implementation#11898

Merged
Viicos merged 12 commits intomainfrom
fieldinfo-cleanup
Jun 14, 2025
Merged

Refactor FieldInfo creation implementation#11898
Viicos merged 12 commits intomainfrom
fieldinfo-cleanup

Conversation

@Viicos
Copy link
Copy Markdown
Member

@Viicos Viicos commented May 21, 2025

Change Summary

(best reviewed commit per commit).

Fixes #11870, fixes #11876, fixes #11978.

Fixes #11122 (the three bugs described in the issue).

Issue #10507 isn't fixed with this, but at least now properly documented as a workaround in the implementation.

Most of the context of this PR is described in #11122.

The main thing being done here is the added _construct() classmethod, that centralizes most of the merging logic and avoids repetition (which actually wasn't mirrored in every code path, that's why many inconsistencies existed depending on whether you assigned a Field() to an attribute). We avoid copying FieldInfo instances everywhere, and doing buggy metadata handling.

As a result, the merge_field_infos() method is unused (you can compare both this method and _construct() to see what changed — the latter is much simpler). We need to deprecate it as third-party projects rely on it (unfortunately..). I propose marking it as deprecated only for type checkers for now, and have a runtime deprecation emitted in V3?

openapi-python-client third party failure is unrelated, their CI is currently failing.

Viicos added 6 commits May 21, 2025 21:24
Required to fix a type checking issue
The existing implementation did not warn when a callable
was used first, then a dict. Also use a plain `UserWarning`
as `PydanticJsonSchemaWarning` is meant to be used
*during* JSON Schema generation.
Both were actually inconsistent, in particular when using the
`warnings.deprecated` class as metadata, we would only set
the message string `FieldInfo.deprecated`.
We don't do any mutations of the assigned `Field()` anymore.
@Viicos Viicos added relnotes-fix Used for bugfixes. relnotes-change Used for changes to existing functionality which don't have a better categorization. third-party-tests Add this label on a PR to trigger 3rd party tests labels May 21, 2025
)
default = assigned_value.default.__get__(None, cls)
assigned_value.default = default
assigned_value._attributes_set['default'] = default
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Next step would be to get rid of the _attributes_set logic when merging instances, which is error prone as you need to update it whenever you do a manual assignment on a FieldInfo instance.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Where can I subscribe to see when this happens?

Apologies if this seems weird to respond to now, please let me know if I should turn this into a proper issue or some such for visibility.

I just spent a few hours adjusting to the v2.12 release, which broke our filter-system. In essence, we're creating lots of filters dynamically using a MetaClass and these filters have pydantic Fields. We usually just pass some extra information by setting

    field.json_schema_extra = {"sqla_column": name}

This used to work with previous versions, but now we need to add

    field._attributes_set["json_schema_extra"] = {"sqla_column": name}

to make the final pydantic.BaseModel.__new__() call recognize the json_schema_extra attribute.

So thanks for providing the fix in this comment :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not really a specific issue for _attributes_set, but I gathered everything into #12374. To be help to give some feedback on your approach (and if extending our metaclass is the only possible option=, I would just require a small example of what your library achieves if possible!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for your offer and sorry for the late reply! In the meantime, our library was refactored quite substantially, which eliminated our usage of _attributes_set :)

Comment thread pydantic/fields.py
See the signature of `pydantic.fields.Field` for more details about the expected arguments.
"""
self._attributes_set = {k: v for k, v in kwargs.items() if v is not _Unset}
self._attributes_set = {k: v for k, v in kwargs.items() if v is not _Unset and k not in self.metadata_lookup}
Copy link
Copy Markdown
Member Author

@Viicos Viicos May 21, 2025

Choose a reason for hiding this comment

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

This is kind of important, although it also works without the change:

When we merge field infos, we don't want to reinclude kwargs that are transformed to metadata elements (gt -> annotated_types.Gt, etc). This will result in unnecessary metadata classes to be created (at the end of the FieldInfo.__init__() logic).

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 21, 2025

CodSpeed Performance Report

Merging #11898 will degrade performances by 13.42%

Comparing fieldinfo-cleanup (3ce5218) with main (11a24ef)

Summary

❌ 1 regressions
✅ 45 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Benchmarks breakdown

Benchmark BASE HEAD Change
test_failed_rebuild 218 µs 251.7 µs -13.42%

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2025

Coverage report

Click to see where and how coverage changed

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

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

@Viicos Viicos added the needs-blogpost-entry This PR needs to be documented in the release notes blog post label May 22, 2025
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 22, 2025

Deploying pydantic-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3ce5218
Status: ✅  Deploy successful!
Preview URL: https://96843f8e.pydantic-docs.pages.dev
Branch Preview URL: https://fieldinfo-cleanup.pydantic-docs.pages.dev

View logs

Comment thread pydantic/fields.py
Comment on lines +741 to +762
def _copy(self) -> Self:
"""Return a copy of the `FieldInfo` instance."""
# Note: we can't define a custom `__copy__()`, as `FieldInfo` is being subclassed
# by some third-party libraries with extra attributes defined (and as `FieldInfo`
# is slotted, we can't make a copy of the `__dict__`).
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Alternatively, we can drop slots on FieldInfo, and do the following:

    def __copy__(self) -> Self:
        copied = type(self)()
        copied.__dict__ = self.__dict__
        for attr_name in ('metadata', '_attributes_set', '_qualifiers'):
             # Apply "deep-copy" behavior on collections attributes:
             value = getattr(self, attr_name).copy()
             setattr(copied, attr_name, value)

        return copied

Comment thread tests/test_annotated.py
}
]

# Ensure that the inner annotation does not override the outer, even for metadata:
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Due to one of the bugs being fixed in this PR (related to the usage of forward references), the constraint in Annotated was dropped. Now the test is failing because the unexpected order (described in #10507) applies.

Comment thread pydantic/fields.py
Comment on lines +410 to +418
# HACK 2: FastAPI is subclassing `FieldInfo` and historically expected the actual
# instance's type to be preserved when constructing new models with its subclasses as assignments.
# This code is never reached by Pydantic itself, and in an ideal world this shouldn't be necessary.
if not metadata and isinstance(default, FieldInfo) and type(default) is not FieldInfo:
field_info = default._copy()
field_info._attributes_set.update(attr_overrides)
for k, v in attr_overrides.items():
setattr(field_info, k, v)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Unfortunate stuff introduced since #6862..

@Viicos
Copy link
Copy Markdown
Member Author

Viicos commented May 23, 2025

cc @zmievsa, I've tried looking into the Cadwyn issues but got too scared by the code base.

@zmievsa
Copy link
Copy Markdown
Contributor

zmievsa commented May 23, 2025

Apologies for not fixing it yesterday. I'll fix it in a second

Update: oh, I see that it's unrelated to my recent problems with Cadwyn CI. Well, I'll fix it now anyways :)

@Viicos
Copy link
Copy Markdown
Member Author

Viicos commented May 24, 2025

No rush, the third-party CI is not blocking and this is only going to be included in 2.12 anyway.

@zmievsa
Copy link
Copy Markdown
Contributor

zmievsa commented May 24, 2025

@Viicos WDYT about this approach for extracting metadata? Is this the correct interface to use? (it works according to my tests but I am worried that I'm using the wrong abstraction from pydantic for extracting it
zmievsa/cadwyn#281

zmievsa added a commit to zmievsa/cadwyn that referenced this pull request May 28, 2025
@Viicos Viicos force-pushed the fieldinfo-cleanup branch from 1c1fb3a to a825961 Compare May 28, 2025 15:55
Copy link
Copy Markdown
Contributor

@DouweM DouweM left a comment

Choose a reason for hiding this comment

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

@Viicos Great work Victorien, the separate commits and your comments were very helpful! Just 2 questions about prepend_metadata.

Comment thread pydantic/fields.py
Comment thread pydantic/fields.py Outdated
@pydantic-hooky pydantic-hooky Bot added the awaiting author revision awaiting changes from the PR author label Jun 12, 2025
@Viicos Viicos force-pushed the fieldinfo-cleanup branch from a825961 to 35c4f95 Compare June 13, 2025 09:11
@Viicos Viicos requested a review from DouweM June 13, 2025 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting author revision awaiting changes from the PR author needs-blogpost-entry This PR needs to be documented in the release notes blog post relnotes-change Used for changes to existing functionality which don't have a better categorization. relnotes-fix Used for bugfixes. third-party-tests Add this label on a PR to trigger 3rd party tests

Projects

None yet

4 participants