Skip to content

Commit 865b668

Browse files
authored
Update _Choices type for forms (#2565)
* Update _Choices type for forms form.ChoiceField and other related fields accept the same input as the model's choices argument (https://docs.djangoproject.com/en/5.2/ref/forms/fields/#django.forms.ChoiceField.choices) Ref: #2476 * Remove alias from _ChoiceInput
1 parent 5083b2a commit 865b668

File tree

5 files changed

+108
-29
lines changed

5 files changed

+108
-29
lines changed

django-stubs/db/models/fields/__init__.pyi

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@ from django import forms
99
from django.core import validators # due to weird mypy.stubtest error
1010
from django.core.checks import CheckMessage
1111
from django.db.backends.base.base import BaseDatabaseWrapper
12-
from django.db.models import Choices, Model
12+
from django.db.models import Model
1313
from django.db.models.expressions import Col, Combinable, Expression, Func
1414
from django.db.models.fields.reverse_related import ForeignObjectRel
1515
from django.db.models.query_utils import Q, RegisterLookupMixin
1616
from django.db.models.sql.compiler import SQLCompiler, _AsSqlType, _ParamsT
1717
from django.forms import Widget
18-
from django.utils.choices import BlankChoiceIterator, _Choice, _ChoiceNamedGroup, _ChoicesCallable, _ChoicesMapping
19-
from django.utils.choices import _Choices as _ChoicesSequence
18+
from django.utils.choices import BlankChoiceIterator, _Choice, _ChoiceNamedGroup, _ChoicesCallable, _ChoicesInput
2019
from django.utils.datastructures import DictWrapper
2120
from django.utils.functional import _Getter, _StrOrPromise, cached_property
2221
from typing_extensions import Self, TypeAlias
@@ -29,9 +28,6 @@ BLANK_CHOICE_DASH: list[tuple[str, str]]
2928
_ChoicesList: TypeAlias = Sequence[_Choice] | Sequence[_ChoiceNamedGroup]
3029
_LimitChoicesTo: TypeAlias = Q | dict[str, Any]
3130
_LimitChoicesToCallable: TypeAlias = Callable[[], _LimitChoicesTo]
32-
_Choices: TypeAlias = (
33-
_ChoicesSequence | _ChoicesMapping | type[Choices] | Callable[[], _ChoicesSequence | _ChoicesMapping]
34-
)
3531

3632
_F = TypeVar("_F", bound=Field, covariant=True)
3733

@@ -174,7 +170,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
174170
unique_for_date: str | None = None,
175171
unique_for_month: str | None = None,
176172
unique_for_year: str | None = None,
177-
choices: _Choices | None = None,
173+
choices: _ChoicesInput | None = None,
178174
help_text: _StrOrPromise = "",
179175
db_column: str | None = None,
180176
db_tablespace: str | None = None,
@@ -289,7 +285,7 @@ class DecimalField(Field[_ST, _GT]):
289285
editable: bool = ...,
290286
auto_created: bool = ...,
291287
serialize: bool = ...,
292-
choices: _Choices | None = ...,
288+
choices: _ChoicesInput | None = ...,
293289
help_text: _StrOrPromise = ...,
294290
db_column: str | None = ...,
295291
db_comment: str | None = ...,
@@ -321,7 +317,7 @@ class CharField(Field[_ST, _GT]):
321317
unique_for_date: str | None = ...,
322318
unique_for_month: str | None = ...,
323319
unique_for_year: str | None = ...,
324-
choices: _Choices | None = ...,
320+
choices: _ChoicesInput | None = ...,
325321
help_text: _StrOrPromise = ...,
326322
db_column: str | None = ...,
327323
db_comment: str | None = ...,
@@ -351,7 +347,7 @@ class SlugField(CharField[_ST, _GT]):
351347
unique_for_date: str | None = ...,
352348
unique_for_month: str | None = ...,
353349
unique_for_year: str | None = ...,
354-
choices: _Choices | None = ...,
350+
choices: _ChoicesInput | None = ...,
355351
help_text: _StrOrPromise = ...,
356352
db_column: str | None = ...,
357353
db_comment: str | None = ...,
@@ -387,7 +383,7 @@ class URLField(CharField[_ST, _GT]):
387383
unique_for_date: str | None = ...,
388384
unique_for_month: str | None = ...,
389385
unique_for_year: str | None = ...,
390-
choices: _Choices | None = ...,
386+
choices: _ChoicesInput | None = ...,
391387
help_text: _StrOrPromise = ...,
392388
db_column: str | None = ...,
393389
db_comment: str | None = ...,
@@ -420,7 +416,7 @@ class TextField(Field[_ST, _GT]):
420416
unique_for_date: str | None = ...,
421417
unique_for_month: str | None = ...,
422418
unique_for_year: str | None = ...,
423-
choices: _Choices | None = ...,
419+
choices: _ChoicesInput | None = ...,
424420
help_text: _StrOrPromise = ...,
425421
db_column: str | None = ...,
426422
db_comment: str | None = ...,
@@ -468,7 +464,7 @@ class GenericIPAddressField(Field[_ST, _GT]):
468464
editable: bool = ...,
469465
auto_created: bool = ...,
470466
serialize: bool = ...,
471-
choices: _Choices | None = ...,
467+
choices: _ChoicesInput | None = ...,
472468
help_text: _StrOrPromise = ...,
473469
db_column: str | None = ...,
474470
db_comment: str | None = ...,
@@ -503,7 +499,7 @@ class DateField(DateTimeCheckMixin, Field[_ST, _GT]):
503499
editable: bool = ...,
504500
auto_created: bool = ...,
505501
serialize: bool = ...,
506-
choices: _Choices | None = ...,
502+
choices: _ChoicesInput | None = ...,
507503
help_text: _StrOrPromise = ...,
508504
db_column: str | None = ...,
509505
db_comment: str | None = ...,
@@ -534,7 +530,7 @@ class TimeField(DateTimeCheckMixin, Field[_ST, _GT]):
534530
editable: bool = ...,
535531
auto_created: bool = ...,
536532
serialize: bool = ...,
537-
choices: _Choices | None = ...,
533+
choices: _ChoicesInput | None = ...,
538534
help_text: _StrOrPromise = ...,
539535
db_column: str | None = ...,
540536
db_comment: str | None = ...,
@@ -571,7 +567,7 @@ class UUIDField(Field[_ST, _GT]):
571567
unique_for_date: str | None = ...,
572568
unique_for_month: str | None = ...,
573569
unique_for_year: str | None = ...,
574-
choices: _Choices | None = ...,
570+
choices: _ChoicesInput | None = ...,
575571
help_text: _StrOrPromise = ...,
576572
db_column: str | None = ...,
577573
db_comment: str | None = ...,
@@ -608,7 +604,7 @@ class FilePathField(Field[_ST, _GT]):
608604
editable: bool = ...,
609605
auto_created: bool = ...,
610606
serialize: bool = ...,
611-
choices: _Choices | None = ...,
607+
choices: _ChoicesInput | None = ...,
612608
help_text: _StrOrPromise = ...,
613609
db_column: str | None = ...,
614610
db_comment: str | None = ...,

django-stubs/forms/fields.pyi

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ from django.db.models.fields import _ErrorMessagesDict, _ErrorMessagesMapping
1111
from django.forms.boundfield import BoundField
1212
from django.forms.forms import BaseForm
1313
from django.forms.widgets import Widget
14-
from django.utils.choices import CallableChoiceIterator, _Choices, _ChoicesCallable
14+
from django.utils.choices import CallableChoiceIterator, _ChoicesCallable, _ChoicesInput
1515
from django.utils.datastructures import _PropertyDescriptor
1616
from django.utils.functional import _StrOrPromise
1717
from typing_extensions import TypeAlias
@@ -317,14 +317,14 @@ class NullBooleanField(BooleanField):
317317

318318
class ChoiceField(Field):
319319
choices: _PropertyDescriptor[
320-
_Choices | _ChoicesCallable | CallableChoiceIterator,
321-
_Choices | CallableChoiceIterator,
320+
_ChoicesInput | _ChoicesCallable | CallableChoiceIterator,
321+
_ChoicesInput | CallableChoiceIterator,
322322
]
323323
widget: _ClassLevelWidgetT
324324
def __init__(
325325
self,
326326
*,
327-
choices: _Choices | _ChoicesCallable = (),
327+
choices: _ChoicesInput | _ChoicesCallable = (),
328328
required: bool = ...,
329329
widget: Widget | type[Widget] | None = ...,
330330
label: _StrOrPromise | None = ...,
@@ -355,7 +355,7 @@ class TypedChoiceField(ChoiceField):
355355
*,
356356
coerce: _CoerceCallable = ...,
357357
empty_value: str | None = "",
358-
choices: _Choices | _ChoicesCallable = ...,
358+
choices: _ChoicesInput | _ChoicesCallable = ...,
359359
required: bool = ...,
360360
widget: Widget | type[Widget] | None = ...,
361361
label: _StrOrPromise | None = ...,
@@ -383,7 +383,7 @@ class TypedMultipleChoiceField(MultipleChoiceField):
383383
*,
384384
coerce: _CoerceCallable = ...,
385385
empty_value: list[Any] | None = ...,
386-
choices: _Choices | _ChoicesCallable = ...,
386+
choices: _ChoicesInput | _ChoicesCallable = ...,
387387
required: bool = ...,
388388
widget: Widget | type[Widget] | None = ...,
389389
label: _StrOrPromise | None = ...,
@@ -459,7 +459,7 @@ class FilePathField(ChoiceField):
459459
recursive: bool = False,
460460
allow_files: bool = True,
461461
allow_folders: bool = False,
462-
choices: _Choices | _ChoicesCallable = ...,
462+
choices: _ChoicesInput | _ChoicesCallable = ...,
463463
required: bool = ...,
464464
widget: Widget | type[Widget] | None = ...,
465465
label: _StrOrPromise | None = ...,

django-stubs/forms/models.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ from django.forms.formsets import BaseFormSet
1414
from django.forms.renderers import BaseRenderer
1515
from django.forms.utils import ErrorList, _DataT, _FilesT
1616
from django.forms.widgets import Widget
17-
from django.utils.choices import BaseChoiceIterator, CallableChoiceIterator, _Choices, _ChoicesCallable
17+
from django.utils.choices import BaseChoiceIterator, CallableChoiceIterator, _ChoicesCallable, _ChoicesInput
1818
from django.utils.datastructures import _PropertyDescriptor
1919
from django.utils.functional import _StrOrPromise
2020
from typing_extensions import TypeAlias
@@ -283,8 +283,8 @@ class ModelChoiceField(ChoiceField, Generic[_M]):
283283
def get_limit_choices_to(self) -> _LimitChoicesTo: ...
284284
def label_from_instance(self, obj: _M) -> str: ...
285285
choices: _PropertyDescriptor[
286-
_Choices | _ChoicesCallable | CallableChoiceIterator,
287-
_Choices | CallableChoiceIterator | ModelChoiceIterator,
286+
_ChoicesInput | _ChoicesCallable | CallableChoiceIterator,
287+
_ChoicesInput | CallableChoiceIterator | ModelChoiceIterator,
288288
]
289289
def prepare_value(self, value: Any) -> Any: ...
290290
def to_python(self, value: Any | None) -> _M | None: ...

django-stubs/utils/choices.pyi

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
from collections.abc import Iterable, Iterator, Mapping
1+
from collections.abc import Callable, Iterable, Iterator, Mapping
22
from typing import Any, Protocol, TypeVar, type_check_only
33

4+
from django.db.models import Choices
45
from typing_extensions import TypeAlias
56

67
_Choice: TypeAlias = tuple[Any, Any]
78
_ChoiceNamedGroup: TypeAlias = tuple[str, Iterable[_Choice]]
89
_Choices: TypeAlias = Iterable[_Choice | _ChoiceNamedGroup]
9-
_ChoicesMapping: TypeAlias = Mapping[Any, Any] # noqa: PYI047
10+
_ChoicesMapping: TypeAlias = Mapping[Any, Any]
11+
_ChoicesInput: TypeAlias = _Choices | _ChoicesMapping | type[Choices] | Callable[[], _Choices | _ChoicesMapping] # noqa: PYI047
1012

1113
@type_check_only
1214
class _ChoicesCallable(Protocol):
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
- case: forms_choicefield_invalid_choices
2+
main: |
3+
from django import forms
4+
5+
class MyForm(forms.Form):
6+
my_choice = forms.ChoiceField(choices='test')
7+
out: |
8+
main:4: error: Argument "choices" to "ChoiceField" has incompatible type "str"; expected "Union[Union[Iterable[Union[Tuple[Any, Any], Tuple[str, Iterable[Tuple[Any, Any]]]]], Mapping[Any, Any], Type[Choices], Callable[[], Union[Iterable[Union[Tuple[Any, Any], Tuple[str, Iterable[Tuple[Any, Any]]]]], Mapping[Any, Any]]]], _ChoicesCallable]" [arg-type]
9+
main:4: note: "str" is missing following "_ChoicesCallable" protocol member:
10+
main:4: note: __call__
11+
12+
- case: forms_choicefield_valid_choices
13+
main: |
14+
from collections.abc import Callable, Mapping, Sequence
15+
from typing import TypeVar
16+
17+
from django import forms
18+
from django.db import models
19+
from typing_extensions import assert_type
20+
21+
_T = TypeVar("_T")
22+
23+
24+
def to_named_seq(func: Callable[[], _T]) -> Callable[[], Sequence[tuple[str, _T]]]:
25+
def inner() -> Sequence[tuple[str, _T]]:
26+
return [("title", func())]
27+
28+
return inner
29+
30+
31+
def to_named_mapping(func: Callable[[], _T]) -> Callable[[], Mapping[str, _T]]:
32+
def inner() -> Mapping[str, _T]:
33+
return {"title": func()}
34+
35+
return inner
36+
37+
38+
def str_tuple() -> Sequence[tuple[str, str]]:
39+
return (("foo", "bar"), ("fuzz", "bazz"))
40+
41+
42+
def str_mapping() -> Mapping[str, str]:
43+
return {"foo": "bar", "fuzz": "bazz"}
44+
45+
46+
def int_tuple() -> Sequence[tuple[int, str]]:
47+
return ((1, "bar"), (2, "bazz"))
48+
49+
50+
def int_mapping() -> Mapping[int, str]:
51+
return {3: "bar", 4: "bazz"}
52+
53+
54+
class TestForm(forms.Form):
55+
class TextChoices(models.TextChoices):
56+
FIRST = "foo", "bar"
57+
SECOND = "foo2", "bar"
58+
59+
class IntegerChoices(models.IntegerChoices):
60+
FIRST = 1, "bar"
61+
SECOND = 2, "bar"
62+
63+
char1 = forms.ChoiceField(choices=TextChoices)
64+
char2 = forms.ChoiceField(choices=str_tuple)
65+
char3 = forms.ChoiceField(choices=str_mapping)
66+
char4 = forms.ChoiceField(choices=str_tuple())
67+
char5 = forms.ChoiceField(choices=str_mapping())
68+
char6 = forms.ChoiceField(choices=to_named_seq(str_tuple))
69+
char7 = forms.ChoiceField(choices=to_named_mapping(str_mapping))
70+
char8 = forms.ChoiceField(choices=to_named_seq(str_tuple)())
71+
char9 = forms.ChoiceField(choices=to_named_mapping(str_mapping)())
72+
73+
int1 = forms.ChoiceField(choices=IntegerChoices)
74+
int2 = forms.ChoiceField(choices=int_tuple)
75+
int3 = forms.ChoiceField(choices=int_mapping)
76+
int4 = forms.ChoiceField(choices=int_tuple())
77+
int5 = forms.ChoiceField(choices=int_mapping())
78+
int6 = forms.ChoiceField(choices=to_named_seq(int_tuple))
79+
int7 = forms.ChoiceField(choices=to_named_seq(int_mapping))
80+
int8 = forms.ChoiceField(choices=to_named_seq(int_tuple)())
81+
int9 = forms.ChoiceField(choices=to_named_seq(int_mapping)())

0 commit comments

Comments
 (0)