Skip to content

Commit 1b988b2

Browse files
Fix FormMixin plugin (#2746)
1 parent 14ddbf7 commit 1b988b2

File tree

4 files changed

+52
-64
lines changed

4 files changed

+52
-64
lines changed

mypy_django_plugin/main.py

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,6 @@
5353
from mypy_django_plugin.transformers.request import check_querydict_is_mutable
5454

5555

56-
def transform_form_class(ctx: ClassDefContext) -> None:
57-
sym = ctx.api.lookup_fully_qualified_or_none(fullnames.BASEFORM_CLASS_FULLNAME)
58-
if sym is not None and isinstance(sym.node, TypeInfo):
59-
bases = helpers.get_django_metadata_bases(sym.node, "baseform_bases")
60-
bases[ctx.cls.fullname] = 1
61-
62-
forms.make_meta_nested_class_inherit_from_any(ctx)
63-
64-
6556
class NewSemanalDjangoPlugin(Plugin):
6657
def __init__(self, options: Options) -> None:
6758
super().__init__(options)
@@ -182,16 +173,6 @@ def get_method_hook(self, fullname: str) -> Callable[[MethodContext], MypyType]
182173
if info and info.has_base(fullnames.QUERYDICT_CLASS_FULLNAME):
183174
return partial(check_querydict_is_mutable, django_context=self.django_context)
184175

185-
elif method_name == "get_form_class":
186-
info = self._get_typeinfo_or_none(class_fullname)
187-
if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME):
188-
return forms.extract_proper_type_for_get_form_class
189-
190-
elif method_name == "get_form":
191-
info = self._get_typeinfo_or_none(class_fullname)
192-
if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME):
193-
return forms.extract_proper_type_for_get_form
194-
195176
elif method_name == "__get__":
196177
hooks = {
197178
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
237218

238219
# Base class is a Form class definition
239220
if fullname in self._get_current_form_bases():
240-
return transform_form_class
221+
return forms.transform_form_class
241222

242223
# Base class is a QuerySet class definition
243224
if sym is not None and isinstance(sym.node, TypeInfo) and sym.node.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
from mypy.plugin import ClassDefContext, MethodContext
2-
from mypy.types import CallableType, Instance, NoneTyp, TypeType, get_proper_type
3-
from mypy.types import Type as MypyType
1+
from mypy.nodes import TypeInfo
2+
from mypy.plugin import ClassDefContext
43

5-
from mypy_django_plugin.lib import helpers
4+
from mypy_django_plugin.lib import fullnames, helpers
65

76

87
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:
1413
meta_node.fallback_to_any = True
1514

1615

17-
def get_specified_form_class(object_type: Instance) -> TypeType | None:
18-
form_class_sym = object_type.type.get("form_class")
19-
if form_class_sym:
20-
form_class_type = get_proper_type(form_class_sym.type)
21-
if isinstance(form_class_type, CallableType):
22-
return TypeType(form_class_type.ret_type)
23-
return None
16+
def transform_form_class(ctx: ClassDefContext) -> None:
17+
sym = ctx.api.lookup_fully_qualified_or_none(fullnames.BASEFORM_CLASS_FULLNAME)
18+
if sym is not None and isinstance(sym.node, TypeInfo):
19+
bases = helpers.get_django_metadata_bases(sym.node, "baseform_bases")
20+
bases[ctx.cls.fullname] = 1
2421

25-
26-
def extract_proper_type_for_get_form(ctx: MethodContext) -> MypyType:
27-
object_type = ctx.type
28-
assert isinstance(object_type, Instance)
29-
30-
form_class_type = get_proper_type(helpers.get_call_argument_type_by_name(ctx, "form_class"))
31-
if form_class_type is None or isinstance(form_class_type, NoneTyp):
32-
form_class_type = get_specified_form_class(object_type)
33-
34-
if isinstance(form_class_type, TypeType) and isinstance(form_class_type.item, Instance):
35-
return form_class_type.item
36-
37-
if isinstance(form_class_type, CallableType):
38-
form_class_ret_type = get_proper_type(form_class_type.ret_type)
39-
if isinstance(form_class_ret_type, Instance):
40-
return form_class_ret_type
41-
42-
return ctx.default_return_type
43-
44-
45-
def extract_proper_type_for_get_form_class(ctx: MethodContext) -> MypyType:
46-
object_type = ctx.type
47-
assert isinstance(object_type, Instance)
48-
49-
form_class_type = get_specified_form_class(object_type)
50-
if form_class_type is None:
51-
return ctx.default_return_type
52-
53-
return form_class_type
22+
make_meta_nested_class_inherit_from_any(ctx)

tests/typecheck/test_forms.yml

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,55 @@
3636
pass
3737
class MyForm2(forms.ModelForm):
3838
pass
39-
class MyView(FormView):
39+
40+
# FormView generic param provided
41+
class MyView(FormView[MyForm]):
4042
form_class = MyForm
4143
def post(self, request: HttpRequest, *args: Any, **kwds: Any) -> HttpResponse:
4244
form_class = self.get_form_class()
4345
reveal_type(form_class) # N: Revealed type is "type[main.MyForm]"
4446
reveal_type(self.get_form(None)) # N: Revealed type is "main.MyForm"
4547
reveal_type(self.get_form()) # N: Revealed type is "main.MyForm"
4648
reveal_type(self.get_form(form_class)) # N: Revealed type is "main.MyForm"
47-
reveal_type(self.get_form(MyForm2)) # N: Revealed type is "main.MyForm2"
49+
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]
50+
return HttpResponse()
51+
52+
# FormView generic param omitted -- fallback to TypeVar default
53+
class MyView2(FormView):
54+
form_class = MyForm
55+
def post(self, request: HttpRequest, *args: Any, **kwds: Any) -> HttpResponse:
56+
form_class = self.get_form_class()
57+
reveal_type(form_class) # N: Revealed type is "type[Any]"
58+
reveal_type(self.get_form(None)) # N: Revealed type is "Any"
59+
reveal_type(self.get_form()) # N: Revealed type is "Any"
60+
reveal_type(self.get_form(form_class)) # N: Revealed type is "Any"
61+
reveal_type(self.get_form(MyForm2)) # N: Revealed type is "Any"
4862
return HttpResponse()
4963
64+
- case: formview_form_valid_proper_type
65+
main: |
66+
from django.http import HttpRequest, HttpResponse
67+
from django.forms import models
68+
from django.views.generic.edit import FormView
69+
70+
class RegistrationForm(models.ModelForm): ...
71+
72+
class RegistrationViewNoGeneric(FormView):
73+
form_class = RegistrationForm
74+
template_name = 'web_app/registration.html'
75+
76+
def form_valid(self, form: RegistrationForm) -> HttpResponse:
77+
form.save()
78+
return super().form_valid(form)
79+
80+
class RegistrationViewWithGeneric(FormView[RegistrationForm]):
81+
form_class = RegistrationForm
82+
template_name = 'web_app/registration.html'
83+
84+
def form_valid(self, form: RegistrationForm) -> HttpResponse:
85+
form.save()
86+
return super().form_valid(form)
87+
5088
- case: updateview_form_valid_has_form_save
5189
main: |
5290
from django import forms

tests/typecheck/views/generic/test_edit.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@
8282
class MyCreateView(CreateView[Article, ArticleModelForm]):
8383
def some(self) -> None:
8484
reveal_type(self.get_form()) # N: Revealed type is "main.ArticleModelForm"
85-
reveal_type(self.get_form(SubArticleModelForm)) # N: Revealed type is "main.SubArticleModelForm"
86-
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]
85+
reveal_type(self.get_form(SubArticleModelForm)) # N: Revealed type is "main.ArticleModelForm"
86+
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]
8787
installed_apps:
8888
- myapp
8989
files:

0 commit comments

Comments
 (0)