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 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
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 @@ -244,6 +244,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 @@ -259,6 +260,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
16 changes: 15 additions & 1 deletion tests/mypy/modules/plugin_fail.py
@@ -1,6 +1,6 @@
from typing import Any, Generic, List, Optional, Set, TypeVar, Union

from pydantic import BaseModel, BaseSettings, Extra, Field
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
from pydantic.dataclasses import dataclass
from pydantic.generics import GenericModel

Expand Down Expand Up @@ -248,3 +248,17 @@ class FieldDefaultTestingModel(BaseModel):

# Default and default factory
m: int = Field(default=1, default_factory=list)


class ModelWithAnnotatedValidator(BaseModel):
name: str

@validator('name')
def noop_validator_with_annotations(self, name: str) -> str:
# This is a mistake: the first argument to a validator is the class itself,
# like a classmethod.
self.instance_method()
return name

def instance_method(self) -> None:
...
8 changes: 8 additions & 0 deletions tests/mypy/modules/plugin_success.py
Expand Up @@ -195,6 +195,14 @@ 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, name: str) -> str:
return name


def _default_factory_str() -> str:
...

Expand Down
1 change: 1 addition & 0 deletions tests/mypy/outputs/plugin-fail-strict.txt
Expand Up @@ -41,3 +41,4 @@
246: error: Incompatible types in assignment (expression has type "List[_T]", variable has type "List[int]") [assignment]
247: error: Argument "default_factory" to "Field" has incompatible type "int"; expected "Optional[Callable[[], Any]]" [arg-type]
250: error: Field default and default_factory cannot be specified together [pydantic-field]
260: error: Missing positional argument "self" in call to "instance_method" of "ModelWithAnnotatedValidator" [call-arg]
1 change: 1 addition & 0 deletions tests/mypy/outputs/plugin-fail.txt
Expand Up @@ -30,3 +30,4 @@
246: error: Incompatible types in assignment (expression has type "List[_T]", variable has type "List[int]") [assignment]
247: error: Argument "default_factory" to "Field" has incompatible type "int"; expected "Optional[Callable[[], Any]]" [arg-type]
250: error: Field default and default_factory cannot be specified together [pydantic-field]
260: error: Missing positional argument "self" in call to "instance_method" of "ModelWithAnnotatedValidator" [call-arg]