diff --git a/django-stubs/contrib/admin/options.pyi b/django-stubs/contrib/admin/options.pyi index 63928972d..c2c0ab58a 100644 --- a/django-stubs/contrib/admin/options.pyi +++ b/django-stubs/contrib/admin/options.pyi @@ -135,7 +135,7 @@ class ModelAdmin(BaseModelAdmin): delete_selected_confirmation_template: str = ... object_history_template: str = ... popup_response_template: str = ... - actions: Sequence[Callable[[ModelAdmin, HttpRequest, QuerySet], None]] = ... + actions: Sequence[Union[Callable[[ModelAdmin, HttpRequest, QuerySet], None], str]] = ... action_form: Any = ... actions_on_top: bool = ... actions_on_bottom: bool = ... diff --git a/django-stubs/core/paginator.pyi b/django-stubs/core/paginator.pyi index d838cb476..1d5bf3705 100644 --- a/django-stubs/core/paginator.pyi +++ b/django-stubs/core/paginator.pyi @@ -29,8 +29,8 @@ class Paginator: orphans: int = ..., allow_empty_first_page: bool = ..., ) -> None: ... - def validate_number(self, number: Optional[Union[float, str]]) -> int: ... - def get_page(self, number: Optional[int]) -> Page: ... + def validate_number(self, number: Optional[Union[int, float, str]]) -> int: ... + def get_page(self, number: Optional[Union[int, float, str]]) -> Page: ... def page(self, number: Union[int, str]) -> Page: ... @property def count(self) -> int: ... diff --git a/django-stubs/db/models/enums.pyi b/django-stubs/db/models/enums.pyi index ddfe54897..557312ec7 100644 --- a/django-stubs/db/models/enums.pyi +++ b/django-stubs/db/models/enums.pyi @@ -10,6 +10,10 @@ class ChoicesMeta(enum.EnumMeta): class Choices(enum.Enum, metaclass=ChoicesMeta): def __str__(self): ... + @property + def label(self) -> str: ... + @property + def value(self) -> Any: ... # fake class _IntegerChoicesMeta(ChoicesMeta): @@ -18,7 +22,9 @@ class _IntegerChoicesMeta(ChoicesMeta): labels: List[str] = ... values: List[int] = ... -class IntegerChoices(int, Choices, metaclass=_IntegerChoicesMeta): ... +class IntegerChoices(int, Choices, metaclass=_IntegerChoicesMeta): + @property + def value(self) -> int: ... # fake class _TextChoicesMeta(ChoicesMeta): @@ -27,4 +33,6 @@ class _TextChoicesMeta(ChoicesMeta): labels: List[str] = ... values: List[str] = ... -class TextChoices(str, Choices, metaclass=_TextChoicesMeta): ... +class TextChoices(str, Choices, metaclass=_TextChoicesMeta): + @property + def value(self) -> str: ... diff --git a/django-stubs/forms/fields.pyi b/django-stubs/forms/fields.pyi index 5a5632014..fad689476 100644 --- a/django-stubs/forms/fields.pyi +++ b/django-stubs/forms/fields.pyi @@ -14,7 +14,7 @@ class Field: initial: Any label: Optional[str] required: bool - widget: Type[Widget] = ... + widget: Union[Type[Widget], Widget] = ... hidden_widget: Any = ... default_validators: Any = ... default_error_messages: Any = ... diff --git a/django-stubs/http/response.pyi b/django-stubs/http/response.pyi index 29ffa90a3..3bcad2d6c 100644 --- a/django-stubs/http/response.pyi +++ b/django-stubs/http/response.pyi @@ -83,12 +83,13 @@ class HttpResponse(HttpResponseBase): context: Context resolver_match: ResolverMatch def json(self) -> Any: ... + def getvalue(self) -> bytes: ... class StreamingHttpResponse(HttpResponseBase): content: Any streaming_content: Iterator[Any] def __init__(self, streaming_content: Iterable[Any] = ..., *args: Any, **kwargs: Any) -> None: ... - def getvalue(self) -> Any: ... + def getvalue(self) -> bytes: ... class FileResponse(StreamingHttpResponse): client: Client diff --git a/django-stubs/utils/functional.pyi b/django-stubs/utils/functional.pyi index 40e415ac4..7fa7abace 100644 --- a/django-stubs/utils/functional.pyi +++ b/django-stubs/utils/functional.pyi @@ -56,4 +56,8 @@ class SimpleLazyObject(LazyObject): def __copy__(self) -> List[int]: ... def __deepcopy__(self, memo: Dict[Any, Any]) -> List[int]: ... -def partition(predicate: Callable, values: List[Model]) -> Tuple[List[Model], List[Model]]: ... +_PartitionMember = TypeVar("_PartitionMember") + +def partition( + predicate: Callable, values: List[_PartitionMember] +) -> Tuple[List[_PartitionMember], List[_PartitionMember]]: ... diff --git a/mypy_django_plugin/lib/chk_helpers.py b/mypy_django_plugin/lib/chk_helpers.py index a8aae579a..3e62d2bb0 100644 --- a/mypy_django_plugin/lib/chk_helpers.py +++ b/mypy_django_plugin/lib/chk_helpers.py @@ -2,8 +2,9 @@ from mypy import checker from mypy.checker import TypeChecker +from mypy.mro import calculate_mro from mypy.nodes import ( - GDEF, MDEF, Expression, MypyFile, SymbolTableNode, TypeInfo, Var, + GDEF, MDEF, Block, ClassDef, Expression, MypyFile, SymbolTable, SymbolTableNode, TypeInfo, Var, ) from mypy.plugin import ( AttributeContext, CheckerPluginInterface, FunctionContext, MethodContext, @@ -21,9 +22,17 @@ def add_new_class_for_current_module(current_module: MypyFile, fields: Optional[Dict[str, MypyType]] = None ) -> TypeInfo: new_class_unique_name = checker.gen_unique_name(name, current_module.names) - new_typeinfo = helpers.new_typeinfo(new_class_unique_name, - bases=bases, - module_name=current_module.fullname) + + # make new class expression + classdef = ClassDef(new_class_unique_name, Block([])) + classdef.fullname = current_module.fullname + '.' + new_class_unique_name + + # make new TypeInfo + new_typeinfo = TypeInfo(SymbolTable(), classdef, current_module.fullname) + new_typeinfo.bases = bases + calculate_mro(new_typeinfo) + new_typeinfo.calculate_metaclass_type() + # add fields if fields: for field_name, field_type in fields.items(): @@ -32,8 +41,8 @@ def add_new_class_for_current_module(current_module: MypyFile, var._fullname = new_typeinfo.fullname + '.' + field_name new_typeinfo.names[field_name] = SymbolTableNode(MDEF, var, plugin_generated=True) + classdef.info = new_typeinfo current_module.names[new_class_unique_name] = SymbolTableNode(GDEF, new_typeinfo, plugin_generated=True) - current_module.defs.append(new_typeinfo.defn) return new_typeinfo diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 87bbcab65..55ca09bb1 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -6,14 +6,15 @@ from mypy.checker import TypeChecker from mypy.mro import calculate_mro from mypy.nodes import ( - Block, CallExpr, ClassDef, Context, Expression, MemberExpr, MypyFile, NameExpr, PlaceholderNode, StrExpr, - SymbolTable, SymbolTableNode, TypeInfo, Var, + GDEF, Argument, Block, CallExpr, ClassDef, Context, Expression, FuncDef, MemberExpr, MypyFile, NameExpr, + PlaceholderNode, StrExpr, SymbolTable, SymbolTableNode, TypeInfo, Var, ) from mypy.plugin import ( AttributeContext, ClassDefContext, DynamicClassDefContext, FunctionContext, MethodContext, ) +from mypy.plugins.common import add_method_to_class from mypy.semanal import SemanticAnalyzer -from mypy.types import AnyType, Instance, NoneTyp, ProperType +from mypy.types import AnyType, CallableType, Instance, NoneTyp, ProperType from mypy.types import Type as MypyType from mypy.types import TypeOfAny, UnionType @@ -94,6 +95,80 @@ def lookup_typeinfo_or_defer(self, fullname: str, *, return sym.node + def copy_method_to_another_class( + self, + ctx: ClassDefContext, + self_type: Instance, + new_method_name: str, + method_node: FuncDef) -> None: + if method_node.type is None: + if not self.defer_till_next_iteration(reason='method_node.type is None'): + raise new_helpers.TypeInfoNotFound(method_node.fullname) + + arguments, return_type = build_unannotated_method_args(method_node) + add_method_to_class( + ctx.api, + ctx.cls, + new_method_name, + args=arguments, + return_type=return_type, + self_type=self_type) + return + + method_type = cast(CallableType, method_node.type) + if not isinstance(method_type, CallableType) and not self.defer_till_next_iteration( + reason='method_node.type is not CallableType'): + raise new_helpers.TypeInfoNotFound(method_node.fullname) + + arguments = [] + bound_return_type = self.semanal_api.anal_type( + method_type.ret_type, + allow_placeholder=True) + + if bound_return_type is None and self.defer_till_next_iteration(): + raise new_helpers.TypeInfoNotFound(method_node.fullname + ' return type') + + assert bound_return_type is not None + + if isinstance(bound_return_type, PlaceholderNode): + raise new_helpers.TypeInfoNotFound('return type ' + method_node.fullname) + + for arg_name, arg_type, original_argument in zip( + method_type.arg_names[1:], + method_type.arg_types[1:], + method_node.arguments[1:]): + bound_arg_type = self.semanal_api.anal_type(arg_type, allow_placeholder=True) + if bound_arg_type is None and not self.defer_till_next_iteration(reason='bound_arg_type is None'): + error_msg = 'of {} argument of {}'.format(arg_name, method_node.fullname) + raise new_helpers.TypeInfoNotFound(error_msg) + + assert bound_arg_type is not None + + if isinstance(bound_arg_type, PlaceholderNode) and self.defer_till_next_iteration( + reason='bound_arg_type is None'): + raise new_helpers.TypeInfoNotFound('of ' + arg_name + ' argument of ' + method_node.fullname) + + var = Var( + name=original_argument.variable.name, + type=arg_type) + var.line = original_argument.variable.line + var.column = original_argument.variable.column + argument = Argument( + variable=var, + type_annotation=bound_arg_type, + initializer=original_argument.initializer, + kind=original_argument.kind) + argument.set_line(original_argument) + arguments.append(argument) + + add_method_to_class( + ctx.api, + ctx.cls, + new_method_name, + args=arguments, + return_type=bound_return_type, + self_type=self_type) + def new_typeinfo(self, name: str, bases: List[Instance], module_fullname: Optional[str] = None) -> TypeInfo: class_def = ClassDef(name, Block([])) class_def.fullname = self.semanal_api.qualified_name(name) @@ -118,11 +193,46 @@ def __call__(self, ctx: DynamicClassDefContext) -> None: self.semanal_api = cast(SemanticAnalyzer, ctx.api) self.create_new_dynamic_class() + def generate_manager_info_and_module(self, base_manager_info: TypeInfo) -> Tuple[TypeInfo, MypyFile]: + new_manager_info = self.semanal_api.basic_new_typeinfo( + self.class_name, + basetype_or_fallback=Instance( + base_manager_info, + [AnyType(TypeOfAny.unannotated)]) + ) + new_manager_info.line = self.call_expr.line + new_manager_info.defn.line = self.call_expr.line + new_manager_info.metaclass_type = new_manager_info.calculate_metaclass_type() + + current_module = self.semanal_api.cur_mod_node + current_module.names[self.class_name] = SymbolTableNode( + GDEF, + new_manager_info, + plugin_generated=True) + return new_manager_info, current_module + @abstractmethod def create_new_dynamic_class(self) -> None: raise NotImplementedError +class DynamicClassFromMethodCallback(DynamicClassPluginCallback): + callee: MemberExpr + + def __call__(self, ctx: DynamicClassDefContext) -> None: + self.class_name = ctx.name + self.call_expr = ctx.call + + assert ctx.call.callee is not None + if not isinstance(ctx.call.callee, MemberExpr): + # throw error? + return + self.callee = ctx.call.callee + + self.semanal_api = cast(SemanticAnalyzer, ctx.api) + self.create_new_dynamic_class() + + class ClassDefPluginCallback(SemanalPluginCallback): reason: Expression class_defn: ClassDef @@ -396,3 +506,12 @@ def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo] if metaclass_sym is not None and isinstance(metaclass_sym.node, TypeInfo): return metaclass_sym.node return None + + +def build_unannotated_method_args(method_node: FuncDef) -> Tuple[List[Argument], MypyType]: + prepared_arguments = [] + for argument in method_node.arguments[1:]: + argument.type_annotation = AnyType(TypeOfAny.unannotated) + prepared_arguments.append(argument) + return_type = AnyType(TypeOfAny.unannotated) + return prepared_arguments, return_type diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 13b7ff8af..24bba3736 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -6,7 +6,7 @@ from mypy.nodes import MypyFile, TypeInfo from mypy.options import Options from mypy.plugin import ( - AttributeContext, ClassDefContext, FunctionContext, MethodContext, Plugin, + AttributeContext, ClassDefContext, DynamicClassDefContext, FunctionContext, MethodContext, Plugin, ) from mypy.types import Type as MypyType @@ -19,6 +19,9 @@ from mypy_django_plugin.transformers.init_create import ( ModelCreateCallback, ModelInitCallback, ) +from mypy_django_plugin.transformers.managers import ( + ManagerFromQuerySetCallback, +) from mypy_django_plugin.transformers.meta import MetaGetFieldCallback from mypy_django_plugin.transformers.models import ModelCallback from mypy_django_plugin.transformers.orm_lookups import ( @@ -249,6 +252,15 @@ def get_attribute_hook(self, fullname: str return None + def get_dynamic_class_hook(self, fullname: str + ) -> Optional[Callable[[DynamicClassDefContext], None]]: + if fullname.endswith('from_queryset'): + class_name, _, _ = fullname.rpartition('.') + info = self._get_typeinfo_or_none(class_name) + if info and info.has_base(fullnames.BASE_MANAGER_CLASS_FULLNAME): + return ManagerFromQuerySetCallback(self) + return None + def plugin(version): return NewSemanalDjangoPlugin diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py new file mode 100644 index 000000000..edac1cce6 --- /dev/null +++ b/mypy_django_plugin/transformers/managers.py @@ -0,0 +1,76 @@ +from typing import List, Tuple + +from mypy.nodes import Argument, FuncDef, NameExpr, StrExpr, TypeInfo +from mypy.plugin import ClassDefContext +from mypy.types import AnyType, Instance +from mypy.types import Type as MypyType +from mypy.types import TypeOfAny + +from mypy_django_plugin.lib import fullnames, helpers + + +def build_unannotated_method_args(method_node: FuncDef) -> Tuple[List[Argument], MypyType]: + prepared_arguments = [] + for argument in method_node.arguments[1:]: + argument.type_annotation = AnyType(TypeOfAny.unannotated) + prepared_arguments.append(argument) + return_type = AnyType(TypeOfAny.unannotated) + return prepared_arguments, return_type + + +class ManagerFromQuerySetCallback(helpers.DynamicClassFromMethodCallback): + def create_new_dynamic_class(self) -> None: + + base_manager_info = self.callee.expr.node # type: ignore + + if base_manager_info is None and not self.defer_till_next_iteration(reason='base_manager_info is None'): + # what exception should be thrown here? + return + + assert isinstance(base_manager_info, TypeInfo) + + new_manager_info, current_module = self.generate_manager_info_and_module(base_manager_info) + + passed_queryset = self.call_expr.args[0] + assert isinstance(passed_queryset, NameExpr) + + derived_queryset_fullname = passed_queryset.fullname + assert derived_queryset_fullname is not None + + sym = self.semanal_api.lookup_fully_qualified_or_none(derived_queryset_fullname) + assert sym is not None + if sym.node is None and not self.defer_till_next_iteration(reason='sym.node is None'): + # inherit from Any to prevent false-positives, if queryset class cannot be resolved + new_manager_info.fallback_to_any = True + return + + derived_queryset_info = sym.node + assert isinstance(derived_queryset_info, TypeInfo) + + if len(self.call_expr.args) > 1: + expr = self.call_expr.args[1] + assert isinstance(expr, StrExpr) + custom_manager_generated_name = expr.value + else: + custom_manager_generated_name = base_manager_info.name + 'From' + derived_queryset_info.name + + custom_manager_generated_fullname = '.'.join(['django.db.models.manager', custom_manager_generated_name]) + if 'from_queryset_managers' not in base_manager_info.metadata: + base_manager_info.metadata['from_queryset_managers'] = {} + base_manager_info.metadata['from_queryset_managers'][ + custom_manager_generated_fullname] = new_manager_info.fullname + class_def_context = ClassDefContext( + cls=new_manager_info.defn, + reason=self.call_expr, api=self.semanal_api) + self_type = Instance(new_manager_info, []) + # we need to copy all methods in MRO before django.db.models.query.QuerySet + for class_mro_info in derived_queryset_info.mro: + if class_mro_info.fullname == fullnames.QUERYSET_CLASS_FULLNAME: + break + for name, sym in class_mro_info.names.items(): + if isinstance(sym.node, FuncDef): + self.copy_method_to_another_class( + class_def_context, + self_type, + new_method_name=name, + method_node=sym.node) diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index 795df5305..10a53dc66 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -1,12 +1,12 @@ from abc import abstractmethod -from typing import Optional, Type +from typing import Dict, Optional, Type from django.db import models from django.db.models.base import Model from django.db.models.fields import DateField, DateTimeField from django.db.models.fields.related import ForeignKey, OneToOneField from mypy.nodes import ( - ARG_STAR2, MDEF, Argument, SymbolTableNode, TypeInfo, Var, + ARG_STAR2, MDEF, Argument, FuncDef, SymbolTableNode, TypeInfo, Var, ) from mypy.plugin import ClassDefContext from mypy.plugins import common @@ -15,7 +15,7 @@ from mypy.types import Type as MypyType from mypy.types import TypeOfAny -from mypy_django_plugin.lib import fullnames, helpers +from mypy_django_plugin.lib import chk_helpers, fullnames, helpers from mypy_django_plugin.transformers import fields, new_helpers @@ -68,7 +68,11 @@ def modify_model_class_defn(self, runtime_model_cls: Type[Model]) -> None: runtime_manager_cls_fullname = new_helpers.get_class_fullname(runtime_default_manager_class) manager_cls_fullname = self.get_real_manager_fullname(runtime_manager_cls_fullname) - default_manager_info = self.lookup_typeinfo_or_defer(manager_cls_fullname) + try: + default_manager_info = self.lookup_typeinfo_or_defer(manager_cls_fullname) + except new_helpers.TypeInfoNotFound: + default_manager_info = None + if default_manager_info is None: if getattr(runtime_model_cls._meta.default_manager, '_built_with_as_manager', False): # it's a Model.as_manager() class and will cause TypeNotFound exception without proper support @@ -81,18 +85,110 @@ def modify_model_class_defn(self, runtime_model_cls: Type[Model]) -> None: class AddManagersCallback(TransformModelClassCallback): - def modify_model_class_defn(self, runtime_model_cls: Type[models.Model]) -> None: - for manager_name, manager in runtime_model_cls._meta.managers_map.items(): - if manager_name in self.class_defn.info.names: - # already defined on the current model class, in file or at a previous iteration - continue - manager_info = self.lookup_typeinfo_for_class_or_defer(manager.__class__) - if manager_info is None: + def is_any_parametrized_manager(self, typ: Instance) -> bool: + return typ.type.fullname in fullnames.MANAGER_CLASSES and isinstance(typ.args[0], AnyType) + + def get_generated_manager_mappings(self, base_manager_fullname: str) -> Dict[str, str]: + try: + base_manager_info = self.lookup_typeinfo_or_defer(base_manager_fullname) + except new_helpers.TypeInfoNotFound: + base_manager_info = None + + if (base_manager_info is None + or 'from_queryset_managers' not in base_manager_info.metadata): + return {} + return base_manager_info.metadata['from_queryset_managers'] + + def has_any_parametrized_manager_as_base(self, info: TypeInfo) -> bool: + for base in helpers.iter_bases(info): + if self.is_any_parametrized_manager(base): + return True + return False + + def create_new_model_parametrized_manager(self, name: str, base_manager_info: TypeInfo) -> Instance: + bases = [] + for original_base in base_manager_info.bases: + if self.is_any_parametrized_manager(original_base): + if original_base.type is None: + raise new_helpers.TypeInfoNotFound() + original_base = helpers.reparametrize_instance(original_base, + [Instance(self.class_defn.info, [])]) + bases.append(original_base) + + current_module = self.semanal_api.modules[self.class_defn.info.module_name] + new_manager_info = chk_helpers.add_new_class_for_current_module(current_module, name, bases) + + # copy fields to a new manager + new_cls_def_context = ClassDefContext(cls=new_manager_info.defn, + reason=self.ctx.reason, + api=self.semanal_api) + custom_manager_type = Instance(new_manager_info, [Instance(self.class_defn.info, [])]) + + for name, sym in base_manager_info.names.items(): + # replace self type with new class, if copying method + if isinstance(sym.node, FuncDef): + self.copy_method_to_another_class(new_cls_def_context, + self_type=custom_manager_type, + new_method_name=name, + method_node=sym.node) continue - manager_type = Instance(manager_info, [Instance(self.class_defn.info, [])]) - self.add_new_model_attribute(manager_name, manager_type) + new_sym = sym.copy() + if isinstance(new_sym.node, Var): + new_var = Var(name, type=sym.type) + new_var.info = new_manager_info + new_var._fullname = new_manager_info.fullname + '.' + name + new_sym.node = new_var + new_manager_info.names[name] = new_sym + return custom_manager_type + + def modify_model_class_defn(self, runtime_model_cls: Type[models.Model]) -> None: + for manager_name, manager in runtime_model_cls._meta.managers_map.items(): + manager_class_name = manager.__class__.__name__ + try: + manager_info = self.lookup_typeinfo_for_class_or_defer(manager.__class__) + if manager_info is None: + continue + except new_helpers.TypeInfoNotFound: + manager_info = None + + # creating custom manager class only if there's none in lookup and it's final iteration + if manager_info is None: + manager_fullname = helpers.get_class_fullname(manager.__class__) + base_manager_fullname = helpers.get_class_fullname(manager.__class__.__bases__[0]) + generated_managers = self.get_generated_manager_mappings(base_manager_fullname) + + if manager_fullname not in generated_managers: + continue + + real_manager_fullname = generated_managers[manager_fullname] + try: + manager_info = self.lookup_typeinfo_or_defer(real_manager_fullname) + except new_helpers.TypeInfoNotFound: + manager_info = None + + if manager_info is None: + continue + + manager_class_name = real_manager_fullname.rsplit('.', maxsplit=1)[1] + + if manager_name not in self.class_defn.info.names: + manager_type = Instance(manager_info, [Instance(self.class_defn.info, [])]) + chk_helpers.add_new_sym_for_info(self.class_defn.info, + name=manager_name, + sym_type=manager_type) + else: + if not self.has_any_parametrized_manager_as_base(manager_info): + continue + custom_model_manager_name = manager.model.__name__ + '_' + manager_class_name + try: + custom_manager_type = self.create_new_model_parametrized_manager(custom_model_manager_name, + base_manager_info=manager_info) + except new_helpers.TypeInfoNotFound: + continue + + chk_helpers.add_new_sym_for_info(self.class_defn.info, name=manager_name, sym_type=custom_manager_type) class AddPrimaryKeyIfDoesNotExist(TransformModelClassCallback): diff --git a/test-data/typecheck/contrib/admin/test_options.yml b/test-data/typecheck/contrib/admin/test_options.yml index ebcc3fe9a..1481080d2 100644 --- a/test-data/typecheck/contrib/admin/test_options.yml +++ b/test-data/typecheck/contrib/admin/test_options.yml @@ -123,4 +123,4 @@ pass class A(admin.ModelAdmin): - actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Callable[[ModelAdmin, HttpRequest, QuerySet[Any]], None]" + actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[ModelAdmin, HttpRequest, QuerySet[Any]], None], str]" diff --git a/test-data/typecheck/managers/querysets/test_from_queryset.yml b/test-data/typecheck/managers/querysets/test_from_queryset.yml new file mode 100644 index 000000000..e858ea4c4 --- /dev/null +++ b/test-data/typecheck/managers/querysets/test_from_queryset.yml @@ -0,0 +1,236 @@ +- case: from_queryset_with_base_manager + main: | + from myapp.models import MyModel + reveal_type(MyModel().objects) # N: Revealed type is 'myapp.models.MyModel_NewManager[myapp.models.MyModel]' + reveal_type(MyModel().objects.get()) # N: Revealed type is 'myapp.models.MyModel*' + reveal_type(MyModel().objects.queryset_method) # N: Revealed type is 'def () -> builtins.str' + reveal_type(MyModel().objects.queryset_method()) # N: Revealed type is 'builtins.str' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from django.db.models.manager import BaseManager + + class ModelQuerySet(models.QuerySet): + def queryset_method(self) -> str: + return 'hello' + NewManager = BaseManager.from_queryset(ModelQuerySet) + class MyModel(models.Model): + objects = NewManager() + +- case: from_queryset_with_manager + main: | + from myapp.models import MyModel + reveal_type(MyModel().objects) # N: Revealed type is 'myapp.models.MyModel_NewManager[myapp.models.MyModel]' + reveal_type(MyModel().objects.get()) # N: Revealed type is 'myapp.models.MyModel*' + reveal_type(MyModel().objects.queryset_method()) # N: Revealed type is 'builtins.str' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class ModelQuerySet(models.QuerySet): + def queryset_method(self) -> str: + return 'hello' + + NewManager = models.Manager.from_queryset(ModelQuerySet) + class MyModel(models.Model): + objects = NewManager() + +- case: from_queryset_returns_intersection_of_manager_and_queryset + main: | + from myapp.models import MyModel, NewManager + reveal_type(NewManager()) # N: Revealed type is 'myapp.models.NewManager' + reveal_type(MyModel.objects) # N: Revealed type is 'myapp.models.MyModel_NewManager[myapp.models.MyModel]' + reveal_type(MyModel.objects.get()) # N: Revealed type is 'Any' + reveal_type(MyModel.objects.manager_only_method()) # N: Revealed type is 'builtins.int' + reveal_type(MyModel.objects.manager_and_queryset_method()) # N: Revealed type is 'builtins.str' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class ModelBaseManager(models.Manager): + def manager_only_method(self) -> int: + return 1 + class ModelQuerySet(models.QuerySet): + def manager_and_queryset_method(self) -> str: + return 'hello' + + NewManager = ModelBaseManager.from_queryset(ModelQuerySet) + class MyModel(models.Model): + objects = NewManager() + +- case: from_queryset_with_class_name_provided + main: | + from myapp.models import MyModel, NewManager + reveal_type(NewManager()) # N: Revealed type is 'myapp.models.NewManager' + reveal_type(MyModel.objects) # N: Revealed type is 'myapp.models.MyModel_NewManager[myapp.models.MyModel]' + reveal_type(MyModel.objects.get()) # N: Revealed type is 'Any' + reveal_type(MyModel.objects.manager_only_method()) # N: Revealed type is 'builtins.int' + reveal_type(MyModel.objects.manager_and_queryset_method()) # N: Revealed type is 'builtins.str' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class ModelBaseManager(models.Manager): + def manager_only_method(self) -> int: + return 1 + class ModelQuerySet(models.QuerySet): + def manager_and_queryset_method(self) -> str: + return 'hello' + + NewManager = ModelBaseManager.from_queryset(ModelQuerySet, class_name='NewManager') + class MyModel(models.Model): + objects = NewManager() + +- case: from_queryset_with_class_inheritance + main: | + from myapp.models import MyModel + reveal_type(MyModel().objects) # N: Revealed type is 'myapp.models.MyModel_NewManager[myapp.models.MyModel]' + reveal_type(MyModel().objects.get()) # N: Revealed type is 'myapp.models.MyModel*' + reveal_type(MyModel().objects.queryset_method()) # N: Revealed type is 'builtins.str' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from django.db.models.manager import BaseManager + class BaseQuerySet(models.QuerySet): + def queryset_method(self) -> str: + return 'hello' + class ModelQuerySet(BaseQuerySet): + pass + + NewManager = BaseManager.from_queryset(ModelQuerySet) + class MyModel(models.Model): + objects = NewManager() + +- case: from_queryset_with_manager_in_another_directory_and_imports + main: | + from myapp.models import MyModel + reveal_type(MyModel().objects) # N: Revealed type is 'myapp.models.MyModel_NewManager[myapp.models.MyModel]' + reveal_type(MyModel().objects.get()) # N: Revealed type is 'myapp.models.MyModel*' + reveal_type(MyModel().objects.queryset_method) # N: Revealed type is 'def (param: Union[builtins.str, None] =) -> Union[builtins.str, None]' + reveal_type(MyModel().objects.queryset_method('str')) # N: Revealed type is 'Union[builtins.str, None]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from myapp.managers import NewManager + + class MyModel(models.Model): + objects = NewManager() + - path: myapp/managers.py + content: | + from typing import Optional + from django.db import models + + class ModelQuerySet(models.QuerySet): + def queryset_method(self, param: Optional[str] = None) -> Optional[str]: + return param + + NewManager = models.Manager.from_queryset(ModelQuerySet) + +- case: from_queryset_with_inherited_manager_and_typing_no_return + disable_cache: true + main: | + from myapp.models import MyModel + reveal_type(MyModel().objects) # N: Revealed type is 'myapp.models.MyModel_NewManager[myapp.models.MyModel]' + reveal_type(MyModel().objects.get()) # N: Revealed type is 'myapp.models.MyModel*' + reveal_type(MyModel().objects.base_queryset_method) # N: Revealed type is 'def (param: Union[builtins.int, builtins.str]) -> ' + reveal_type(MyModel().objects.base_queryset_method(2)) # N: Revealed type is '' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from myapp.managers import NewManager + class MyModel(models.Model): + objects = NewManager() + - path: myapp/managers.py + content: | + from django.db import models + from myapp.base_queryset import BaseQuerySet + class ModelQuerySet(BaseQuerySet): + pass + NewManager = models.Manager.from_queryset(ModelQuerySet) + - path: myapp/base_queryset.py + content: | + from typing import NoReturn, Union + from django.db import models + class BaseQuerySet(models.QuerySet): + def base_queryset_method(self, param: Union[int, str]) -> NoReturn: + raise ValueError + + +- case: from_queryset_with_inherited_manager_and_fk_to_auth_contrib + disable_cache: true + main: | + from myapp.base_queryset import BaseQuerySet + reveal_type(BaseQuerySet().base_queryset_method) # N: Revealed type is 'def (param: builtins.dict[builtins.str, Union[builtins.int, builtins.str]]) -> Union[builtins.int, builtins.str]' + + from django.contrib.auth.models import Permission + reveal_type(Permission().another_models) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.AnotherModelInProjectWithContribAuthM2M]' + + from myapp.managers import NewManager + reveal_type(NewManager()) # N: Revealed type is 'myapp.managers.NewManager' + reveal_type(NewManager().base_queryset_method) # N: Revealed type is 'def (param: builtins.dict[builtins.str, Union[builtins.int, builtins.str]]) -> Union[builtins.int, builtins.str]' + + from myapp.models import MyModel + reveal_type(MyModel().objects) # N: Revealed type is 'myapp.models.MyModel_NewManager[myapp.models.MyModel]' + reveal_type(MyModel().objects.get()) # N: Revealed type is 'myapp.models.MyModel*' + reveal_type(MyModel().objects.base_queryset_method) # N: Revealed type is 'def (param: builtins.dict[builtins.str, Union[builtins.int, builtins.str]]) -> Union[builtins.int, builtins.str]' + installed_apps: + - myapp + - django.contrib.auth + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from myapp.managers import NewManager + from django.contrib.auth.models import Permission + + class MyModel(models.Model): + objects = NewManager() + + class AnotherModelInProjectWithContribAuthM2M(models.Model): + permissions = models.ForeignKey( + Permission, + on_delete=models.PROTECT, + related_name='another_models' + ) + - path: myapp/managers.py + content: | + from django.db import models + from myapp.base_queryset import BaseQuerySet + from typing import Union, Dict + class ModelQuerySet(BaseQuerySet): + pass + NewManager = models.Manager.from_queryset(ModelQuerySet) + - path: myapp/base_queryset.py + content: | + from typing import Union, Dict + from django.db import models + class BaseQuerySet(models.QuerySet): + def base_queryset_method(self, param: Dict[str, Union[int, str]]) -> Union[int, str]: + return param["hello"] diff --git a/test-data/typecheck/managers/test_managers.yml b/test-data/typecheck/managers/test_managers.yml index d61a02183..01f09cc17 100644 --- a/test-data/typecheck/managers/test_managers.yml +++ b/test-data/typecheck/managers/test_managers.yml @@ -102,8 +102,8 @@ - case: manager_requires_type_annotation_to_be_set_if_generic_is_not_specified main: | from myapp.models import MyModel - reveal_type(MyModel.objects) # N: Revealed type is 'myapp.models.MyManager' - reveal_type(MyModel.objects.get()) # N: Revealed type is 'Any' + reveal_type(MyModel.objects) # N: Revealed type is 'myapp.models.MyModel_MyManager[myapp.models.MyModel]' + reveal_type(MyModel.objects.get()) # N: Revealed type is 'myapp.models.MyModel*' reveal_type(MyModel.objects2) # N: Revealed type is 'myapp.models.MyGenericManager[Any]' reveal_type(MyModel.objects2.get()) # N: Revealed type is 'Any' reveal_type(MyModel.objects3) # N: Revealed type is 'myapp.models.MyGenericManager[myapp.models.MyModel]' @@ -272,4 +272,25 @@ class MyManager(models.Manager['MyModel']): def mymethod(self) -> int: pass - objects = MyManager() \ No newline at end of file + objects = MyManager() + + +- case: manager_method_is_forward_reference + main: | + from myapp.models import ModelA + reveal_type(ModelA.objects.do_something()) # N: Revealed type is 'myapp.models.ModelB' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class ManagerA(models.Manager): + def do_something(self) -> "ModelB": + return ModelB.objects.create(movie="There's something about mypy") + class ModelA(models.Model): + title = models.TextField() + objects = ManagerA() + class ModelB(models.Model): + movie = models.TextField()