Repository: typeddjango/django-stubs

Issue:
ModelChoiceField could use a type argument
# Bug report

## What's wrong

When subclassing `django.forms.ModelChoiceField` and overriding `label_from_instance()`, the type of the method argument is `Model`, but the implementation likely needs to know which concrete model type is being passed.

## How is that should be

While it is possible to work around this using `cast()` or `assert isinstance()`, having a type argument for the model type (`_M`) on `ModelChoiceField` would be a more elegant solution.

## System information

- `django` version: 3.2.6
- `django-stubs` version: 1.8.0



Discussion:
In this case we would need to update both `django-stubs` (types) and `django-stubs-ext` (runtime `__class_getattr__`).


Resolving patch:
diff --git a/ext/django_stubs_ext/patch.py b/ext/django_stubs_ext/patch.py
--- a/ext/django_stubs_ext/patch.py
+++ b/ext/django_stubs_ext/patch.py
@@ -16,7 +16,7 @@
 from django.db.models.manager import BaseManager
 from django.db.models.query import QuerySet
 from django.forms.formsets import BaseFormSet
-from django.forms.models import BaseModelForm, BaseModelFormSet
+from django.forms.models import BaseModelForm, BaseModelFormSet, ModelChoiceField
 from django.utils.connection import BaseConnectionHandler
 from django.views.generic.detail import SingleObjectMixin
 from django.views.generic.edit import DeletionMixin, FormMixin
@@ -63,6 +63,7 @@ def __repr__(self) -> str:
     MPGeneric(BaseFormSet),
     MPGeneric(BaseModelForm),
     MPGeneric(BaseModelFormSet),
+    MPGeneric(ModelChoiceField),
     MPGeneric(Feed),
     MPGeneric(Sitemap),
     MPGeneric(SuccessMessageMixin),


Test patch:
diff --git a/tests/typecheck/test_forms.yml b/tests/typecheck/test_forms.yml
--- a/tests/typecheck/test_forms.yml
+++ b/tests/typecheck/test_forms.yml
@@ -71,3 +71,44 @@
 
         class SuccessMessageFirstView(FormMixin, SuccessMessageMixin):
             pass
+
+-   case: generic_modelchoicefield_label_from_instance
+    main: |
+        from django import forms
+        from myapp.models import Article, Category
+
+        class ArticleChoiceField(forms.ModelChoiceField[Article]):
+            def label_from_instance(self, obj: Article) -> str:
+                return obj.name
+
+        class BrokenArticleChoiceField(forms.ModelChoiceField[Article]):
+            def label_from_instance(self, obj: Article) -> str:
+                return obj.title # E: "Article" has no attribute "title"  [attr-defined]
+
+        class ArticleMultipleChoiceField(forms.ModelMultipleChoiceField[Article]):
+            def label_from_instance(self, obj: Article) -> str:
+                return obj.name
+
+        class ChooseArticleForm(forms.Form):
+            articles = ArticleMultipleChoiceField(
+                queryset=Article.objects.none(),
+            )
+            best_article = ArticleChoiceField(
+                queryset=Article.objects.none(),
+            )
+            best_category = ArticleChoiceField(
+                queryset=Category.objects.none(), # E: Argument "queryset" to "ArticleChoiceField" has incompatible type "_QuerySet[Category, Category]"; expected "Union[Manager[Article], _QuerySet[Article, Article], None]"  [arg-type]
+            )
+    installed_apps:
+        - myapp
+    files:
+        -   path: myapp/__init__.py
+        -   path: myapp/models.py
+            content: |
+                from django.db import models
+
+                class Category(models.Model):
+                    title = models.CharField(max_length=128)
+
+                class Article(models.Model):
+                    name = models.CharField(max_length=128)