Skip to content

Commit

Permalink
Generic forms.ModelChoiceField (#1889)
Browse files Browse the repository at this point in the history
* Add failing test

* Add generic types to ModelMultipleChoiceField

* Add generic monkey patch entry
  • Loading branch information
UnknownPlatypus authored Feb 15, 2024
1 parent 34522da commit 0b8b124
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 11 deletions.
20 changes: 10 additions & 10 deletions django-stubs/forms/models.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -251,20 +251,20 @@ class ModelChoiceIterator:
def __bool__(self) -> bool: ...
def choice(self, obj: Model) -> tuple[ModelChoiceIteratorValue, str]: ...

class ModelChoiceField(ChoiceField):
class ModelChoiceField(ChoiceField, Generic[_M]):
disabled: bool
help_text: _StrOrPromise
required: bool
show_hidden_initial: bool
validators: list[Any]
iterator: type[ModelChoiceIterator]
empty_label: _StrOrPromise | None
queryset: QuerySet[models.Model] | None
queryset: QuerySet[_M] | None
limit_choices_to: _AllLimitChoicesTo | None
to_field_name: str | None
def __init__(
self,
queryset: None | Manager[models.Model] | QuerySet[models.Model],
queryset: Manager[_M] | QuerySet[_M] | None,
*,
empty_label: _StrOrPromise | None = ...,
required: bool = ...,
Expand All @@ -278,27 +278,27 @@ class ModelChoiceField(ChoiceField):
**kwargs: Any,
) -> None: ...
def get_limit_choices_to(self) -> _LimitChoicesTo: ...
def label_from_instance(self, obj: Model) -> str: ...
def label_from_instance(self, obj: _M) -> str: ...
choices: _PropertyDescriptor[
_FieldChoices | _ChoicesCallable | CallableChoiceIterator,
_FieldChoices | CallableChoiceIterator | ModelChoiceIterator,
]
def prepare_value(self, value: Any) -> Any: ...
def to_python(self, value: Any | None) -> Model | None: ...
def validate(self, value: Model | None) -> None: ...
def to_python(self, value: Any | None) -> _M | None: ...
def validate(self, value: _M | None) -> None: ...
def has_changed(self, initial: Model | int | str | UUID | None, data: int | str | None) -> bool: ...

class ModelMultipleChoiceField(ModelChoiceField):
class ModelMultipleChoiceField(ModelChoiceField[_M]):
disabled: bool
empty_label: _StrOrPromise | None
help_text: _StrOrPromise
required: bool
show_hidden_initial: bool
widget: _ClassLevelWidgetT
hidden_widget: type[Widget]
def __init__(self, queryset: None | Manager[Model] | QuerySet[Model], **kwargs: Any) -> None: ...
def to_python(self, value: Any) -> list[Model]: ... # type: ignore[override]
def clean(self, value: Any) -> QuerySet[Model]: ...
def __init__(self, queryset: Manager[_M] | QuerySet[_M] | None, **kwargs: Any) -> None: ...
def to_python(self, value: Any) -> list[_M]: ... # type: ignore[override]
def clean(self, value: Any) -> QuerySet[_M]: ...
def prepare_value(self, value: Any) -> Any: ...
def has_changed(self, initial: Collection[Any] | None, data: Collection[Any] | None) -> bool: ... # type: ignore[override]

Expand Down
3 changes: 2 additions & 1 deletion ext/django_stubs_ext/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,6 +63,7 @@ def __repr__(self) -> str:
MPGeneric(BaseFormSet),
MPGeneric(BaseModelForm),
MPGeneric(BaseModelFormSet),
MPGeneric(ModelChoiceField),
MPGeneric(Feed),
MPGeneric(Sitemap),
MPGeneric(SuccessMessageMixin),
Expand Down
41 changes: 41 additions & 0 deletions tests/typecheck/test_forms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 0b8b124

Please sign in to comment.