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

Teach mypy plugin that validator methods are classmethods #4102

Merged
merged 5 commits into from Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions changes/4102-DMRobertson.md
@@ -0,0 +1 @@
Teach the mypy plugin that methods decorated by `@validator` are classmethods.
19 changes: 19 additions & 0 deletions pydantic/mypy.py
Expand Up @@ -185,6 +185,7 @@ def transform(self) -> None:
ctx = self._ctx
info = self._ctx.cls.info

self.adjust_validator_signatures()
config = self.collect_config()
fields = self.collect_fields(config)
for field in fields:
Expand All @@ -200,6 +201,24 @@ def transform(self) -> None:
'config': config.set_values_dict(),
}

def adjust_validator_signatures(self) -> None:
"""When we decorate a function `f` with `pydantic.validator(...), mypy sees
`f` as a regular method taking a `self` instance, even though pydantic
internally wraps `f` with `classmethod` if necessary.

Teach mypy this by marking any function whose outermost decorator is a
`validator()` call as a classmethod.
"""
for name, sym in self._ctx.cls.info.names.items():
if isinstance(sym.node, Decorator):
first_dec = sym.node.original_decorators[0]
if (
isinstance(first_dec, CallExpr)
and isinstance(first_dec.callee, NameExpr)
and first_dec.callee.fullname == 'pydantic.class_validators.validator'
):
sym.node.func.is_class = True

def collect_config(self) -> 'ModelConfigData':
"""
Collects the values of the config attributes that are used by the plugin, accounting for parent classes.
Expand Down
10 changes: 9 additions & 1 deletion tests/mypy/modules/plugin_success.py
@@ -1,4 +1,4 @@
from typing import ClassVar, Generic, Optional, TypeVar, Union
from typing import ClassVar, Generic, Optional, Type, TypeVar, Union

from pydantic import BaseModel, BaseSettings, Field, create_model, validator
from pydantic.dataclasses import dataclass
Expand Down Expand Up @@ -193,3 +193,11 @@ class Response(GenericModel, Generic[T]):


response = Response[Model](data=model, error=None)


class ModelWithAnnotatedValidator(BaseModel):
name: str

@validator('name')
def noop_validator_with_annotations(cls: Type['ModelWithAnnotatedValidator'], name: str) -> str:
Copy link
Member

Choose a reason for hiding this comment

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

I think this is wrong, surely (like self) you can omit the type hint for cls?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahh yes. I think I'd put this in so that the test failed without the change. Let me see if I can come up with something better.

return name
1 change: 1 addition & 0 deletions tests/mypy/outputs/plugin_success.txt
@@ -1,3 +1,4 @@
122: error: Unexpected keyword argument "name" for "AddProject" [call-arg]
122: error: Unexpected keyword argument "slug" for "AddProject" [call-arg]
122: error: Unexpected keyword argument "description" for "AddProject" [call-arg]
202: error: The erased type of self "Type[plugin_success.ModelWithAnnotatedValidator]" is not a supertype of its class "plugin_success.ModelWithAnnotatedValidator" [misc]