feat: add mypy plugin for partial models#79
Open
shivayseth wants to merge 2 commits into
Open
Conversation
Add a mypy plugin (pydantic_partial.mypy) that teaches type checkers about model_as_partial(). Using get_dynamic_class_hook, it registers a real TypeInfo for the partial at the assignment site, makes every field Optional, and synthesises an all-optional __init__. It runs alongside pydantic.mypy, which provides the field metadata it reads. Covers the no-argument model_as_partial()/as_partial() call. Field-selecting and recursive calls are not handled yet and degrade gracefully to the original type. Refs ddanier#78
Handle model_as_partial("age") in the mypy plugin: only the named fields
become optional, while unselected fields keep their original requiredness,
read from pydantic's field metadata. Non-literal arguments, *args splats,
and dotted/nested names degrade gracefully to mypy's default.
Refs ddanier#78
a5c58ca to
6e2e15d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the long-standing request for static type-checking support (revisits #2, per the discussion in #78).
What
Adds a mypy plugin,
pydantic_partial.mypy, so partials created withmodel_as_partial()are understood by mypy instead of being seen as the original (still-required) model.Enable alongside Pydantic's own plugin (both required, since this plugin reads the field metadata
pydantic.mypyproduces):How
PartialFoo = Foo.model_as_partial()is theName = call(...)form, which mypy routes throughget_dynamic_class_hook, the same mechanism SQLAlchemy uses forBase = declarative_base(). The plugin registers a realTypeInfoat the assignment site (so mypy never has to resolve the call's return value as a type, which is the wall #2 hit), makes the relevant fieldsOptional, and synthesises a matching__init__. For field-selecting calls it reads each field'shas_defaultfrom pydantic's metadata so unselected fields keep their real requiredness.Two details have dedicated regression tests: mypy reports the hook fullname as
<module>.<Model>.model_as_partial(not the mixin's), so matching is by method-name suffix plus an MRO check; andpydantic.mypywould generate a required__init__for the synthetic class, so the plugin's__init__must win. The hookdefer()s untilpydantic.mypyhas populated its field metadata and rebuilds cleanly on incremental passes.Scope
Supported:
model_as_partial()/as_partial()with no arguments (all fields optional) and with literal field names likemodel_as_partial("name")(only those become optional), in thePartial = Model.model_as_partial(...)assignment form.recursive=isn't fully supported yet: the call still produces a flat partial (top-level fields become optional), but nested models aren't recursed into, so it's stricter than the runtime behaviour rather than wrong.These fall back to mypy's default instead (never a crash or a silently wrong type): field lists that can't be resolved statically (non-literal arguments,
*argssplats, or dotted names like"items.name"), and non-assignment uses.pyrightisn't supported yet.Notes
I originally planned to split the no-argument and field-selecting cases into two PRs, but since neither leaves anything in a broken state and field selection rounds out the feature, I folded both into this one so it is release-ready as a whole. Happy to split it back out if you would rather review or land them separately.
mypy is added to the dev and tox deps (the plugin imports mypy only when mypy runs). New tests in
tests/test_mypy.pyrun real mypy over fixtures in a subprocess, so a shared.mypy_cacheexercises incremental mode.tox,ruff, andpyrightall pass.