diff --git a/README.md b/README.md index 182ef4f..75a9f13 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ pip install django-stubs | django-stubs | mypy version | django version | python version | ------------ | ---- | ---- | ---- | +| 1.3.0 | 0.750 | 2.2.x | ^3.6 | 1.2.0 | 0.730 | 2.2.x | ^3.6 | 1.1.0 | 0.720 | 2.2.x | ^3.6 | 0.12.x | old semantic analyzer (<0.711), dmypy support | 2.1.x | ^3.6 diff --git a/django-sources b/django-sources index 612c2d1..9a17ae5 160000 --- a/django-sources +++ b/django-sources @@ -1 +1 @@ -Subproject commit 612c2d166c9048c59ee241ce4ba89858aa65665d +Subproject commit 9a17ae50c61a3a0ea6c552ce4e3eab27f796d094 diff --git a/django-stubs/test/utils.pyi b/django-stubs/test/utils.pyi index af1776e..18b192a 100644 --- a/django-stubs/test/utils.pyi +++ b/django-stubs/test/utils.pyi @@ -1,9 +1,22 @@ import decimal -import warnings from contextlib import contextmanager from decimal import Decimal from io import StringIO -from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, Optional, Set, Tuple, Type, Union +from typing import ( + Any, + Callable, + Dict, + Iterable, + Iterator, + List, + Mapping, + Optional, + Set, + Tuple, + Type, + Union, + ContextManager, +) from django.apps.registry import Apps from django.core.checks.registry import CheckRegistry @@ -86,7 +99,7 @@ class ignore_warnings(TestContextDecorator): ignore_kwargs: Dict[str, Any] = ... filter_func: Callable = ... def __init__(self, **kwargs: Any) -> None: ... - catch_warnings: warnings.catch_warnings = ... + catch_warnings: ContextManager[Optional[list]] = ... requires_tz_support: Any diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index 6adc50d..1e68763 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -367,7 +367,7 @@ class DjangoContext: lookup_type: MypyType = lookup_base.args[0] # if it's Field, consider lookup_type a __get__ of current field if (isinstance(lookup_type, Instance) - and lookup_type.type.fullname() == fullnames.FIELD_FULLNAME): + and lookup_type.type.fullname == fullnames.FIELD_FULLNAME): field_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), field.__class__) if field_info is None: return AnyType(TypeOfAny.explicit) diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 07952e0..af99bf6 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -179,10 +179,10 @@ def add_new_class_for_module(module: MypyFile, name: str, bases: List[Instance], # make new class expression classdef = ClassDef(new_class_unique_name, Block([])) - classdef.fullname = module.fullname() + '.' + new_class_unique_name + classdef.fullname = module.fullname + '.' + new_class_unique_name # make new TypeInfo - new_typeinfo = TypeInfo(SymbolTable(), classdef, module.fullname()) + new_typeinfo = TypeInfo(SymbolTable(), classdef, module.fullname) new_typeinfo.bases = bases calculate_mro(new_typeinfo) new_typeinfo.calculate_metaclass_type() @@ -191,7 +191,7 @@ def add_new_class_for_module(module: MypyFile, name: str, bases: List[Instance], for field_name, field_type in fields.items(): var = Var(field_name, type=field_type) var.info = new_typeinfo - var._fullname = new_typeinfo.fullname() + '.' + field_name + var._fullname = new_typeinfo.fullname + '.' + field_name new_typeinfo.names[field_name] = SymbolTableNode(MDEF, var, plugin_generated=True) classdef.info = new_typeinfo @@ -276,7 +276,7 @@ def get_typechecker_api(ctx: Union[AttributeContext, MethodContext, FunctionCont def is_model_subclass_info(info: TypeInfo, django_context: 'DjangoContext') -> bool: - return (info.fullname() in django_context.all_registered_model_class_fullnames + return (info.fullname in django_context.all_registered_model_class_fullnames or info.has_base(fullnames.MODEL_CLASS_FULLNAME)) @@ -293,7 +293,7 @@ def add_new_sym_for_info(info: TypeInfo, *, name: str, sym_type: MypyType) -> No var = Var(name=name, type=sym_type) # var.info: type of the object variable is bound to var.info = info - var._fullname = info.fullname() + '.' + name + var._fullname = info.fullname + '.' + name var.is_initialized_in_class = True var.is_inferred = True info.names[name] = SymbolTableNode(MDEF, var, diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 5a39b85..3ea89be 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -121,17 +121,17 @@ class NewSemanalDjangoPlugin(Plugin): def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]: # for settings - if file.fullname() == 'django.conf' and self.django_context.django_settings_module: + if file.fullname == 'django.conf' and self.django_context.django_settings_module: return [self._new_dependency(self.django_context.django_settings_module)] # for values / values_list - if file.fullname() == 'django.db.models': + if file.fullname == 'django.db.models': return [self._new_dependency('mypy_extensions'), self._new_dependency('typing')] # for `get_user_model()` if self.django_context.settings: - if (file.fullname() == 'django.contrib.auth' - or file.fullname() in {'django.http', 'django.http.request'}): + if (file.fullname == 'django.contrib.auth' + or file.fullname in {'django.http', 'django.http.request'}): auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL try: auth_user_module = self.django_context.apps_registry.get_model(auth_user_model_name).__module__ @@ -141,7 +141,7 @@ class NewSemanalDjangoPlugin(Plugin): return [self._new_dependency(auth_user_module)] # ensure that all mentioned to='someapp.SomeModel' are loaded with corresponding related Fields - defined_model_classes = self.django_context.model_modules.get(file.fullname()) + defined_model_classes = self.django_context.model_modules.get(file.fullname) if not defined_model_classes: return [] deps = set() @@ -153,13 +153,13 @@ class NewSemanalDjangoPlugin(Plugin): if related_model_cls is None: continue related_model_module = related_model_cls.__module__ - if related_model_module != file.fullname(): + if related_model_module != file.fullname: deps.add(self._new_dependency(related_model_module)) # reverse relations for relation in model_class._meta.related_objects: related_model_cls = self.django_context.get_field_related_model_cls(relation) related_model_module = related_model_cls.__module__ - if related_model_module != file.fullname(): + if related_model_module != file.fullname: deps.add(self._new_dependency(related_model_module)) return list(deps) diff --git a/mypy_django_plugin/transformers/fields.py b/mypy_django_plugin/transformers/fields.py index 32d20bf..b88fdbf 100644 --- a/mypy_django_plugin/transformers/fields.py +++ b/mypy_django_plugin/transformers/fields.py @@ -29,7 +29,7 @@ def _get_current_field_from_assignment(ctx: FunctionContext, django_context: Dja if field_name is None: return None - model_cls = django_context.get_model_class_by_fullname(outer_model_info.fullname()) + model_cls = django_context.get_model_class_by_fullname(outer_model_info.fullname) if model_cls is None: return None diff --git a/mypy_django_plugin/transformers/init_create.py b/mypy_django_plugin/transformers/init_create.py index 2edfdef..fe0b19e 100644 --- a/mypy_django_plugin/transformers/init_create.py +++ b/mypy_django_plugin/transformers/init_create.py @@ -54,7 +54,7 @@ def typecheck_model_method(ctx: Union[FunctionContext, MethodContext], django_co def redefine_and_typecheck_model_init(ctx: FunctionContext, django_context: DjangoContext) -> MypyType: assert isinstance(ctx.default_return_type, Instance) - model_fullname = ctx.default_return_type.type.fullname() + model_fullname = ctx.default_return_type.type.fullname model_cls = django_context.get_model_class_by_fullname(model_fullname) if model_cls is None: return ctx.default_return_type @@ -67,7 +67,7 @@ def redefine_and_typecheck_model_create(ctx: MethodContext, django_context: Djan # only work with ctx.default_return_type = model Instance return ctx.default_return_type - model_fullname = ctx.default_return_type.type.fullname() + model_fullname = ctx.default_return_type.type.fullname model_cls = django_context.get_model_class_by_fullname(model_fullname) if model_cls is None: return ctx.default_return_type diff --git a/mypy_django_plugin/transformers/meta.py b/mypy_django_plugin/transformers/meta.py index 2b83df6..9549ac9 100644 --- a/mypy_django_plugin/transformers/meta.py +++ b/mypy_django_plugin/transformers/meta.py @@ -28,7 +28,7 @@ def return_proper_field_type_from_get_field(ctx: MethodContext, django_context: if not isinstance(model_type, Instance): return ctx.default_return_type - model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname()) + model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname) if model_cls is None: return ctx.default_return_type diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index aa95cdb..f85c661 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -1,5 +1,5 @@ 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.fields import DateField, DateTimeField @@ -7,10 +7,11 @@ from django.db.models.fields.related import ForeignKey from django.db.models.fields.reverse_related import ( 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.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 TypeOfAny @@ -43,7 +44,7 @@ class ModelClassInitializer: var = Var(name=name, type=typ) # var.info: type of the object variable is bound to var.info = self.model_classdef.info - var._fullname = self.model_classdef.info.fullname() + '.' + name + var._fullname = self.model_classdef.info.fullname + '.' + name var.is_initialized_in_class = True var.is_inferred = True return var @@ -126,7 +127,7 @@ class AddRelatedModelsId(ModelClassInitializer): class AddManagers(ModelClassInitializer): def _is_manager_any(self, typ: Instance) -> bool: - return typ.type.fullname() == fullnames.MANAGER_CLASS_FULLNAME and type(typ.args[0]) == AnyType + return typ.type.fullname == fullnames.MANAGER_CLASS_FULLNAME and type(typ.args[0]) == AnyType def run_with_model_cls(self, model_cls: Type[Model]) -> None: for manager_name, manager in model_cls._meta.managers_map.items(): @@ -158,18 +159,46 @@ class AddManagers(ModelClassInitializer): bases=bases, fields=OrderedDict()) # 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(): + # 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() if isinstance(new_sym.node, Var): new_var = Var(name, type=sym.type) new_var.info = custom_manager_info - new_var._fullname = custom_manager_info.fullname() + '.' + name + new_var._fullname = custom_manager_info.fullname + '.' + name new_sym.node = new_var 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) + 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): def run_with_model_cls(self, model_cls: Type[Model]) -> None: diff --git a/mypy_django_plugin/transformers/orm_lookups.py b/mypy_django_plugin/transformers/orm_lookups.py index e1ed913..0aa516b 100644 --- a/mypy_django_plugin/transformers/orm_lookups.py +++ b/mypy_django_plugin/transformers/orm_lookups.py @@ -16,7 +16,7 @@ def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext) if not ctx.type.args or not isinstance(ctx.type.args[0], Instance): return ctx.default_return_type - model_cls_fullname = ctx.type.args[0].type.fullname() + model_cls_fullname = ctx.type.args[0].type.fullname model_cls = django_context.get_model_class_by_fullname(model_cls_fullname) if model_cls is None: return ctx.default_return_type @@ -44,7 +44,7 @@ def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext) def resolve_combinable_type(combinable_type: Instance, django_context: DjangoContext) -> MypyType: - if combinable_type.type.fullname() != fullnames.F_EXPRESSION_FULLNAME: + if combinable_type.type.fullname != fullnames.F_EXPRESSION_FULLNAME: # Combinables aside from F expressions are unsupported return AnyType(TypeOfAny.explicit) diff --git a/mypy_django_plugin/transformers/querysets.py b/mypy_django_plugin/transformers/querysets.py index 8789995..5c086ef 100644 --- a/mypy_django_plugin/transformers/querysets.py +++ b/mypy_django_plugin/transformers/querysets.py @@ -117,7 +117,7 @@ def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context: if model_type is None: return AnyType(TypeOfAny.from_omitted_generics) - model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname()) + model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname) if model_cls is None: return ctx.default_return_type @@ -166,7 +166,7 @@ def extract_proper_type_queryset_values(ctx: MethodContext, django_context: Djan if model_type is None: return AnyType(TypeOfAny.from_omitted_generics) - model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname()) + model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname) if model_cls is None: return ctx.default_return_type diff --git a/scripts/enabled_test_modules.py b/scripts/enabled_test_modules.py index 2bb30b9..7eba8b9 100644 --- a/scripts/enabled_test_modules.py +++ b/scripts/enabled_test_modules.py @@ -37,7 +37,6 @@ IGNORED_ERRORS = { 'Argument after ** must be a mapping', 'note:', re.compile(r'Item "None" of "[a-zA-Z_ ,\[\]]+" has no attribute'), - '"Optional[List[_Record]]"', '"Callable[..., None]" has no attribute', 'does not return a value', 'has no attribute "alternatives"', @@ -257,7 +256,6 @@ IGNORED_ERRORS = { '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 "FloatModel", variable has type', - 'No overload variant of "bytes" matches argument type "Container[int]"', '"ImageFile" has no attribute "was_opened"', ], 'model_indexes': [ @@ -278,6 +276,7 @@ IGNORED_ERRORS = { 'Incompatible type for "department" of "Worker"', '"PickledModel" has no attribute', '"Department" has no attribute "evaluate"', + 'Unsupported target for indexed assignment', ], 'model_formsets_regress': [ '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]";' ], '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': [ 'List or tuple expected as variable arguments' diff --git a/setup.py b/setup.py index e66da9a..9e5bbe0 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ with open('README.md', 'r') as f: readme = f.read() dependencies = [ - 'mypy>=0.740,<0.750', + 'mypy>=0.750,<0.760', 'typing-extensions', 'django', ] diff --git a/test-data/typecheck/managers/test_managers.yml b/test-data/typecheck/managers/test_managers.yml index 939a782..9cc8698 100644 --- a/test-data/typecheck/managers/test_managers.yml +++ b/test-data/typecheck/managers/test_managers.yml @@ -307,9 +307,18 @@ - case: custom_manager_returns_proper_model_types main: | 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.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_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: - myapp files: @@ -320,5 +329,9 @@ class MyManager(models.Manager): def get_instance(self) -> int: pass + def get_instance_untyped(self, name): + pass class User(models.Model): objects = MyManager() + class ChildUser(models.Model): + objects = MyManager()