properly change type of self for methods on custom manager classes

This commit is contained in:
Maxim Kurnikov
2019-11-30 21:52:02 +03:00
parent 5a151bf851
commit 2c4827bbaf
3 changed files with 51 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
from collections import OrderedDict from collections import OrderedDict
from typing import Type from typing import List, Tuple, Type
from django.db.models.base import Model from django.db.models.base import Model
from django.db.models.fields import DateField, DateTimeField from django.db.models.fields import DateField, DateTimeField
@@ -7,10 +7,11 @@ from django.db.models.fields.related import ForeignKey
from django.db.models.fields.reverse_related import ( from django.db.models.fields.reverse_related import (
ManyToManyRel, ManyToOneRel, OneToOneRel, ManyToManyRel, ManyToOneRel, OneToOneRel,
) )
from mypy.nodes import ARG_STAR2, Argument, Context, TypeInfo, Var from mypy.nodes import ARG_STAR2, Argument, Context, FuncDef, TypeInfo, Var
from mypy.plugin import ClassDefContext from mypy.plugin import ClassDefContext
from mypy.plugins import common from mypy.plugins import common
from mypy.types import AnyType, Instance from mypy.plugins.common import add_method
from mypy.types import AnyType, CallableType, Instance
from mypy.types import Type as MypyType from mypy.types import Type as MypyType
from mypy.types import TypeOfAny from mypy.types import TypeOfAny
@@ -158,7 +159,22 @@ class AddManagers(ModelClassInitializer):
bases=bases, bases=bases,
fields=OrderedDict()) fields=OrderedDict())
# copy fields to a new manager # copy fields to a new manager
new_cls_def_context = ClassDefContext(cls=custom_manager_info.defn,
reason=self.ctx.reason,
api=self.api)
custom_manager_type = Instance(custom_manager_info, [Instance(self.model_classdef.info, [])])
for name, sym in manager_info.names.items(): for name, sym in manager_info.names.items():
# replace self type with new class, if copying method
if isinstance(sym.node, FuncDef):
arguments, return_type = self.prepare_new_method_arguments(sym.node)
add_method(new_cls_def_context,
name,
args=arguments,
return_type=return_type,
self_type=custom_manager_type)
continue
new_sym = sym.copy() new_sym = sym.copy()
if isinstance(new_sym.node, Var): if isinstance(new_sym.node, Var):
new_var = Var(name, type=sym.type) new_var = Var(name, type=sym.type)
@@ -167,9 +183,22 @@ class AddManagers(ModelClassInitializer):
new_sym.node = new_var new_sym.node = new_var
custom_manager_info.names[name] = new_sym custom_manager_info.names[name] = new_sym
custom_manager_type = Instance(custom_manager_info, [Instance(self.model_classdef.info, [])])
self.add_new_node_to_model_class(manager_name, custom_manager_type) self.add_new_node_to_model_class(manager_name, custom_manager_type)
def prepare_new_method_arguments(self, node: FuncDef) -> Tuple[List[Argument], MypyType]:
arguments = []
for argument in node.arguments[1:]:
if argument.type_annotation is None:
argument.type_annotation = AnyType(TypeOfAny.unannotated)
arguments.append(argument)
if isinstance(node.type, CallableType):
return_type = node.type.ret_type
else:
return_type = AnyType(TypeOfAny.unannotated)
return arguments, return_type
class AddDefaultManagerAttribute(ModelClassInitializer): class AddDefaultManagerAttribute(ModelClassInitializer):
def run_with_model_cls(self, model_cls: Type[Model]) -> None: def run_with_model_cls(self, model_cls: Type[Model]) -> None:

View File

@@ -37,7 +37,6 @@ IGNORED_ERRORS = {
'Argument after ** must be a mapping', 'Argument after ** must be a mapping',
'note:', 'note:',
re.compile(r'Item "None" of "[a-zA-Z_ ,\[\]]+" has no attribute'), re.compile(r'Item "None" of "[a-zA-Z_ ,\[\]]+" has no attribute'),
'"Optional[List[_Record]]"',
'"Callable[..., None]" has no attribute', '"Callable[..., None]" has no attribute',
'does not return a value', 'does not return a value',
'has no attribute "alternatives"', 'has no attribute "alternatives"',
@@ -257,7 +256,6 @@ IGNORED_ERRORS = {
'Item "Field[Any, Any]" of "Union[Field[Any, Any], ForeignObjectRel]" has no attribute', 'Item "Field[Any, Any]" of "Union[Field[Any, Any], ForeignObjectRel]" has no attribute',
'Incompatible types in assignment (expression has type "Type[Person', 'Incompatible types in assignment (expression has type "Type[Person',
'Incompatible types in assignment (expression has type "FloatModel", variable has type', 'Incompatible types in assignment (expression has type "FloatModel", variable has type',
'No overload variant of "bytes" matches argument type "Container[int]"',
'"ImageFile" has no attribute "was_opened"', '"ImageFile" has no attribute "was_opened"',
], ],
'model_indexes': [ 'model_indexes': [
@@ -278,6 +276,7 @@ IGNORED_ERRORS = {
'Incompatible type for "department" of "Worker"', 'Incompatible type for "department" of "Worker"',
'"PickledModel" has no attribute', '"PickledModel" has no attribute',
'"Department" has no attribute "evaluate"', '"Department" has no attribute "evaluate"',
'Unsupported target for indexed assignment',
], ],
'model_formsets_regress': [ 'model_formsets_regress': [
'Incompatible types in assignment (expression has type "int", target has type "str")', 'Incompatible types in assignment (expression has type "int", target has type "str")',
@@ -352,7 +351,8 @@ IGNORED_ERRORS = {
'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any, Optional[Any], Any]";' 'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any, Optional[Any], Any]";'
], ],
'sites_framework': [ 'sites_framework': [
'expression has type "CurrentSiteManager[CustomArticle]", base class "AbstractArticle"' 'expression has type "CurrentSiteManager[CustomArticle]", base class "AbstractArticle"',
"Name 'Optional' is not defined",
], ],
'syndication_tests': [ 'syndication_tests': [
'List or tuple expected as variable arguments' 'List or tuple expected as variable arguments'

View File

@@ -307,9 +307,18 @@
- case: custom_manager_returns_proper_model_types - case: custom_manager_returns_proper_model_types
main: | main: |
from myapp.models import User from myapp.models import User
reveal_type(User.objects.get()) # N: Revealed type is 'myapp.models.User*' reveal_type(User.objects) # N: Revealed type is 'myapp.models.User_MyManager[myapp.models.User]'
reveal_type(User.objects.select_related()) # N: Revealed type is 'myapp.models.User_MyManager[myapp.models.User]' reveal_type(User.objects.select_related()) # N: Revealed type is 'myapp.models.User_MyManager[myapp.models.User]'
reveal_type(User.objects.get()) # N: Revealed type is 'myapp.models.User*'
reveal_type(User.objects.get_instance()) # N: Revealed type is 'builtins.int' reveal_type(User.objects.get_instance()) # N: Revealed type is 'builtins.int'
reveal_type(User.objects.get_instance_untyped('hello')) # N: Revealed type is 'Any'
from myapp.models import ChildUser
reveal_type(ChildUser.objects) # N: Revealed type is 'myapp.models.ChildUser_MyManager[myapp.models.ChildUser]'
reveal_type(ChildUser.objects.select_related()) # N: Revealed type is 'myapp.models.ChildUser_MyManager[myapp.models.ChildUser]'
reveal_type(ChildUser.objects.get()) # N: Revealed type is 'myapp.models.ChildUser*'
reveal_type(ChildUser.objects.get_instance()) # N: Revealed type is 'builtins.int'
reveal_type(ChildUser.objects.get_instance_untyped('hello')) # N: Revealed type is 'Any'
installed_apps: installed_apps:
- myapp - myapp
files: files:
@@ -318,7 +327,11 @@
content: | content: |
from django.db import models from django.db import models
class MyManager(models.Manager): class MyManager(models.Manager):
def get_instance(self: "models.Manager[User]") -> int: def get_instance(self) -> int:
pass
def get_instance_untyped(self, name):
pass pass
class User(models.Model): class User(models.Model):
objects = MyManager() objects = MyManager()
class ChildUser(models.Model):
objects = MyManager()