Skip to content

Commit bda1956

Browse files
Update models's fields' _Choices type (#2476)
Django 5 allows model field choices to be callables, mappings or subclasses of models.Choices. This commit introduces these options in the stubs. Co-authored-by: João Seckler <[email protected]>
1 parent 008c6e6 commit bda1956

File tree

3 files changed

+105
-3
lines changed

3 files changed

+105
-3
lines changed

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +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 Model
12+
from django.db.models import Choices, 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.forms import Widget
17-
from django.utils.choices import BlankChoiceIterator, _Choice, _ChoiceNamedGroup, _Choices, _ChoicesCallable
17+
from django.utils.choices import BlankChoiceIterator, _Choice, _ChoiceNamedGroup, _ChoicesCallable, _ChoicesMapping
18+
from django.utils.choices import _Choices as _ChoicesSequence
1819
from django.utils.datastructures import DictWrapper
1920
from django.utils.functional import _Getter, _StrOrPromise, cached_property
2021
from typing_extensions import Self, TypeAlias
@@ -27,6 +28,9 @@ BLANK_CHOICE_DASH: list[tuple[str, str]]
2728
_ChoicesList: TypeAlias = Sequence[_Choice] | Sequence[_ChoiceNamedGroup]
2829
_LimitChoicesTo: TypeAlias = Q | dict[str, Any]
2930
_LimitChoicesToCallable: TypeAlias = Callable[[], _LimitChoicesTo]
31+
_Choices: TypeAlias = (
32+
_ChoicesSequence | _ChoicesMapping | type[Choices] | Callable[[], _ChoicesSequence | _ChoicesMapping]
33+
)
3034

3135
_F = TypeVar("_F", bound=Field, covariant=True)
3236

django-stubs/utils/choices.pyi

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

44
from typing_extensions import TypeAlias
55

66
_Choice: TypeAlias = tuple[Any, Any]
77
_ChoiceNamedGroup: TypeAlias = tuple[str, Iterable[_Choice]]
88
_Choices: TypeAlias = Iterable[_Choice | _ChoiceNamedGroup]
9+
_ChoicesMapping: TypeAlias = Mapping[Any, Any] # noqa: PYI047
910

1011
@type_check_only
1112
class _ChoicesCallable(Protocol):
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
- case: db_models_charfield_invalid_choices
2+
main: |
3+
from django.db import models
4+
5+
class MyModel(models.Model):
6+
char1 = models.CharField(max_length=200, choices='test')
7+
out: |
8+
main:4: error: Argument "choices" to "CharField" has incompatible type "str"; expected "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]]], None]" [arg-type]
9+
main:4: note: Following member(s) of "str" have conflicts:
10+
main:4: note: Expected:
11+
main:4: note: def __iter__(self) -> Iterator[Union[Tuple[Any, Any], Tuple[str, Iterable[Tuple[Any, Any]]]]]
12+
main:4: note: Got:
13+
main:4: note: def __iter__(self) -> Iterator[str]
14+
15+
- case: db_models_integerfield_invalid_choices
16+
main: |
17+
from django.db import models
18+
19+
class MyModel(models.Model):
20+
int1 = models.IntegerField(choices='test')
21+
out: |
22+
main:4: error: Argument "choices" to "IntegerField" has incompatible type "str"; expected "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]]], None]" [arg-type]
23+
main:4: note: Following member(s) of "str" have conflicts:
24+
main:4: note: Expected:
25+
main:4: note: def __iter__(self) -> Iterator[Union[Tuple[Any, Any], Tuple[str, Iterable[Tuple[Any, Any]]]]]
26+
main:4: note: Got:
27+
main:4: note: def __iter__(self) -> Iterator[str]
28+
29+
- case: db_models_valid_choices
30+
main: |
31+
from collections.abc import Callable, Mapping, Sequence
32+
from typing import TypeVar
33+
34+
from django.db import models
35+
from typing_extensions import assert_type
36+
37+
_T = TypeVar("_T")
38+
39+
40+
def to_named_seq(func: Callable[[], _T]) -> Callable[[], Sequence[tuple[str, _T]]]:
41+
def inner() -> Sequence[tuple[str, _T]]:
42+
return [("title", func())]
43+
44+
return inner
45+
46+
47+
def to_named_mapping(func: Callable[[], _T]) -> Callable[[], Mapping[str, _T]]:
48+
def inner() -> Mapping[str, _T]:
49+
return {"title": func()}
50+
51+
return inner
52+
53+
54+
def str_tuple() -> Sequence[tuple[str, str]]:
55+
return (("foo", "bar"), ("fuzz", "bazz"))
56+
57+
58+
def str_mapping() -> Mapping[str, str]:
59+
return {"foo": "bar", "fuzz": "bazz"}
60+
61+
62+
def int_tuple() -> Sequence[tuple[int, str]]:
63+
return ((1, "bar"), (2, "bazz"))
64+
65+
66+
def int_mapping() -> Mapping[int, str]:
67+
return {3: "bar", 4: "bazz"}
68+
69+
70+
class TestModel(models.Model):
71+
class TextChoices(models.TextChoices):
72+
FIRST = "foo", "bar"
73+
SECOND = "foo2", "bar"
74+
75+
class IntegerChoices(models.IntegerChoices):
76+
FIRST = 1, "bar"
77+
SECOND = 2, "bar"
78+
79+
char1 = models.CharField(max_length=5, choices=TextChoices, default="foo")
80+
char2 = models.CharField(max_length=5, choices=str_tuple, default="foo")
81+
char3 = models.CharField(max_length=5, choices=str_mapping, default="foo")
82+
char4 = models.CharField(max_length=5, choices=str_tuple(), default="foo")
83+
char5 = models.CharField(max_length=5, choices=str_mapping(), default="foo")
84+
char6 = models.CharField(max_length=5, choices=to_named_seq(str_tuple), default="foo")
85+
char7 = models.CharField(max_length=5, choices=to_named_mapping(str_mapping), default="foo")
86+
char8 = models.CharField(max_length=5, choices=to_named_seq(str_tuple)(), default="foo")
87+
char9 = models.CharField(max_length=5, choices=to_named_mapping(str_mapping)(), default="foo")
88+
89+
int1 = models.IntegerField(choices=IntegerChoices, default=1)
90+
int2 = models.IntegerField(choices=int_tuple, default=1)
91+
int3 = models.IntegerField(choices=int_mapping, default=1)
92+
int4 = models.IntegerField(choices=int_tuple(), default=1)
93+
int5 = models.IntegerField(choices=int_mapping(), default=1)
94+
int6 = models.IntegerField(choices=to_named_seq(int_tuple), default=1)
95+
int7 = models.IntegerField(choices=to_named_seq(int_mapping), default=1)
96+
int8 = models.IntegerField(choices=to_named_seq(int_tuple)(), default=1)
97+
int9 = models.IntegerField(choices=to_named_seq(int_mapping)(), default=1)

0 commit comments

Comments
 (0)