Repository: typeddjango/django-stubs

Issue:
Incorrect returntype of FormMixin.get_form
# Bug report

## What's wrong

FormMixin's get_form says it returns in instance of BaseForm

## How is that should be

It should return either an instance of the class given to the method or the class returned by get_form_class

## System information

- OS:
- `python` version: 3.9
- `django` version: 3.2.9
- `mypy` version: -
- `django-stubs` version: 1.10.1
- `django-stubs-ext` version: 0.4.0



Discussion:

Patch:
diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py
index 66093f3f1..60adca412 100644
--- a/mypy_django_plugin/main.py
+++ b/mypy_django_plugin/main.py
@@ -53,15 +53,6 @@
 from mypy_django_plugin.transformers.request import check_querydict_is_mutable
 
 
-def transform_form_class(ctx: ClassDefContext) -> None:
-    sym = ctx.api.lookup_fully_qualified_or_none(fullnames.BASEFORM_CLASS_FULLNAME)
-    if sym is not None and isinstance(sym.node, TypeInfo):
-        bases = helpers.get_django_metadata_bases(sym.node, "baseform_bases")
-        bases[ctx.cls.fullname] = 1
-
-    forms.make_meta_nested_class_inherit_from_any(ctx)
-
-
 class NewSemanalDjangoPlugin(Plugin):
     def __init__(self, options: Options) -> None:
         super().__init__(options)
@@ -182,16 +173,6 @@ def get_method_hook(self, fullname: str) -> Callable[[MethodContext], MypyType]
             if info and info.has_base(fullnames.QUERYDICT_CLASS_FULLNAME):
                 return partial(check_querydict_is_mutable, django_context=self.django_context)
 
-        elif method_name == "get_form_class":
-            info = self._get_typeinfo_or_none(class_fullname)
-            if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME):
-                return forms.extract_proper_type_for_get_form_class
-
-        elif method_name == "get_form":
-            info = self._get_typeinfo_or_none(class_fullname)
-            if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME):
-                return forms.extract_proper_type_for_get_form
-
         elif method_name == "__get__":
             hooks = {
                 fullnames.MANYTOMANY_FIELD_FULLNAME: manytomany.refine_many_to_many_related_manager,
@@ -237,7 +218,7 @@ def get_base_class_hook(self, fullname: str) -> Callable[[ClassDefContext], None
 
         # Base class is a Form class definition
         if fullname in self._get_current_form_bases():
-            return transform_form_class
+            return forms.transform_form_class
 
         # Base class is a QuerySet class definition
         if sym is not None and isinstance(sym.node, TypeInfo) and sym.node.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
diff --git a/mypy_django_plugin/transformers/forms.py b/mypy_django_plugin/transformers/forms.py
index 38be5e6e4..4c14c4ac4 100644
--- a/mypy_django_plugin/transformers/forms.py
+++ b/mypy_django_plugin/transformers/forms.py
@@ -1,8 +1,7 @@
-from mypy.plugin import ClassDefContext, MethodContext
-from mypy.types import CallableType, Instance, NoneTyp, TypeType, get_proper_type
-from mypy.types import Type as MypyType
+from mypy.nodes import TypeInfo
+from mypy.plugin import ClassDefContext
 
-from mypy_django_plugin.lib import helpers
+from mypy_django_plugin.lib import fullnames, helpers
 
 
 def make_meta_nested_class_inherit_from_any(ctx: ClassDefContext) -> None:
@@ -14,40 +13,10 @@ def make_meta_nested_class_inherit_from_any(ctx: ClassDefContext) -> None:
         meta_node.fallback_to_any = True
 
 
-def get_specified_form_class(object_type: Instance) -> TypeType | None:
-    form_class_sym = object_type.type.get("form_class")
-    if form_class_sym:
-        form_class_type = get_proper_type(form_class_sym.type)
-        if isinstance(form_class_type, CallableType):
-            return TypeType(form_class_type.ret_type)
-    return None
+def transform_form_class(ctx: ClassDefContext) -> None:
+    sym = ctx.api.lookup_fully_qualified_or_none(fullnames.BASEFORM_CLASS_FULLNAME)
+    if sym is not None and isinstance(sym.node, TypeInfo):
+        bases = helpers.get_django_metadata_bases(sym.node, "baseform_bases")
+        bases[ctx.cls.fullname] = 1
 
-
-def extract_proper_type_for_get_form(ctx: MethodContext) -> MypyType:
-    object_type = ctx.type
-    assert isinstance(object_type, Instance)
-
-    form_class_type = get_proper_type(helpers.get_call_argument_type_by_name(ctx, "form_class"))
-    if form_class_type is None or isinstance(form_class_type, NoneTyp):
-        form_class_type = get_specified_form_class(object_type)
-
-    if isinstance(form_class_type, TypeType) and isinstance(form_class_type.item, Instance):
-        return form_class_type.item
-
-    if isinstance(form_class_type, CallableType):
-        form_class_ret_type = get_proper_type(form_class_type.ret_type)
-        if isinstance(form_class_ret_type, Instance):
-            return form_class_ret_type
-
-    return ctx.default_return_type
-
-
-def extract_proper_type_for_get_form_class(ctx: MethodContext) -> MypyType:
-    object_type = ctx.type
-    assert isinstance(object_type, Instance)
-
-    form_class_type = get_specified_form_class(object_type)
-    if form_class_type is None:
-        return ctx.default_return_type
-
-    return form_class_type
+    make_meta_nested_class_inherit_from_any(ctx)


Test patch:
diff --git a/tests/typecheck/test_forms.yml b/tests/typecheck/test_forms.yml
index 2d680cfb2..ecdedbb6f 100644
--- a/tests/typecheck/test_forms.yml
+++ b/tests/typecheck/test_forms.yml
@@ -36,7 +36,9 @@
             pass
         class MyForm2(forms.ModelForm):
             pass
-        class MyView(FormView):
+
+        # FormView generic param provided
+        class MyView(FormView[MyForm]):
             form_class = MyForm
             def post(self, request: HttpRequest, *args: Any, **kwds: Any) -> HttpResponse:
                 form_class = self.get_form_class()
@@ -44,9 +46,45 @@
                 reveal_type(self.get_form(None))  # N: Revealed type is "main.MyForm"
                 reveal_type(self.get_form())  # N: Revealed type is "main.MyForm"
                 reveal_type(self.get_form(form_class))  # N: Revealed type is "main.MyForm"
-                reveal_type(self.get_form(MyForm2))  # N: Revealed type is "main.MyForm2"
+                reveal_type(self.get_form(MyForm2))  # N: Revealed type is "main.MyForm" # E: Argument 1 to "get_form" of "FormMixin" has incompatible type "type[MyForm2]"; expected "type[MyForm] | None"  [arg-type]
+                return HttpResponse()
+
+        # FormView generic param omitted -- fallback to TypeVar default
+        class MyView2(FormView):
+            form_class = MyForm
+            def post(self, request: HttpRequest, *args: Any, **kwds: Any) -> HttpResponse:
+                form_class = self.get_form_class()
+                reveal_type(form_class)  # N: Revealed type is "type[Any]"
+                reveal_type(self.get_form(None))  # N: Revealed type is "Any"
+                reveal_type(self.get_form())  # N: Revealed type is "Any"
+                reveal_type(self.get_form(form_class))  # N: Revealed type is "Any"
+                reveal_type(self.get_form(MyForm2))  # N: Revealed type is "Any"
                 return HttpResponse()
 
+-   case: formview_form_valid_proper_type
+    main: |
+        from django.http import HttpRequest, HttpResponse
+        from django.forms import models
+        from django.views.generic.edit import FormView
+
+        class RegistrationForm(models.ModelForm): ...
+
+        class RegistrationViewNoGeneric(FormView):
+            form_class = RegistrationForm
+            template_name = 'web_app/registration.html'
+
+            def form_valid(self, form: RegistrationForm) -> HttpResponse:
+                 form.save()
+                 return super().form_valid(form)
+
+        class RegistrationViewWithGeneric(FormView[RegistrationForm]):
+            form_class = RegistrationForm
+            template_name = 'web_app/registration.html'
+
+            def form_valid(self, form: RegistrationForm) -> HttpResponse:
+                 form.save()
+                 return super().form_valid(form)
+
 -   case: updateview_form_valid_has_form_save
     main: |
         from django import forms
diff --git a/tests/typecheck/views/generic/test_edit.yml b/tests/typecheck/views/generic/test_edit.yml
index d482ca28b..7538bd6a8 100644
--- a/tests/typecheck/views/generic/test_edit.yml
+++ b/tests/typecheck/views/generic/test_edit.yml
@@ -82,8 +82,8 @@
         class MyCreateView(CreateView[Article, ArticleModelForm]):
             def some(self) -> None:
                 reveal_type(self.get_form())  # N: Revealed type is "main.ArticleModelForm"
-                reveal_type(self.get_form(SubArticleModelForm))  # N: Revealed type is "main.SubArticleModelForm"
-                reveal_type(self.get_form(AnotherArticleModelForm))  # N: Revealed type is "main.AnotherArticleModelForm"  # E: Argument 1 to "get_form" of "FormMixin" has incompatible type "type[AnotherArticleModelForm]"; expected "type[ArticleModelForm] | None"  [arg-type]
+                reveal_type(self.get_form(SubArticleModelForm))  # N: Revealed type is "main.ArticleModelForm"
+                reveal_type(self.get_form(AnotherArticleModelForm))  # N: Revealed type is "main.ArticleModelForm"  # E: Argument 1 to "get_form" of "FormMixin" has incompatible type "type[AnotherArticleModelForm]"; expected "type[ArticleModelForm] | None"  [arg-type]
     installed_apps:
         - myapp
     files: