Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic forms.ModelChoiceField #1889

Merged
merged 4 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)