Skip to content

Commit 3531bd4

Browse files
Update models's fields' _Choices type
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: Stian Jensen <[email protected]>
1 parent 93a6ef7 commit 3531bd4

File tree

4 files changed

+110
-3
lines changed

4 files changed

+110
-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: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from collections.abc import Callable, Mapping, Sequence
2+
from typing import TypeVar
3+
4+
from django.db import models
5+
from typing_extensions import assert_type
6+
7+
_T = TypeVar("_T")
8+
9+
10+
def to_named_seq(func: Callable[[], _T]) -> Callable[[], Sequence[tuple[str, _T]]]:
11+
def inner() -> Sequence[tuple[str, _T]]:
12+
return [("title", func())]
13+
14+
return inner
15+
16+
17+
def to_named_mapping(func: Callable[[], _T]) -> Callable[[], Mapping[str, _T]]:
18+
def inner() -> Mapping[str, _T]:
19+
return {"title": func()}
20+
21+
return inner
22+
23+
24+
def str_tuple() -> Sequence[tuple[str, str]]:
25+
return (("foo", "bar"), ("fuzz", "bazz"))
26+
27+
28+
def str_mapping() -> Mapping[str, str]:
29+
return {"foo": "bar", "fuzz": "bazz"}
30+
31+
32+
def int_tuple() -> Sequence[tuple[int, str]]:
33+
return ((1, "bar"), (2, "bazz"))
34+
35+
36+
def int_mapping() -> Mapping[int, str]:
37+
return {3: "bar", 4: "bazz"}
38+
39+
40+
class TestModel(models.Model):
41+
class TextChoices(models.TextChoices):
42+
FIRST = "foo", "bar"
43+
SECOND = "foo2", "bar"
44+
45+
class IntegerChoices(models.IntegerChoices):
46+
FIRST = 1, "bar"
47+
SECOND = 2, "bar"
48+
49+
char1 = models.CharField[str, str](max_length=5, choices=TextChoices, default="foo")
50+
char2 = models.CharField[str, str](max_length=5, choices=str_tuple, default="foo")
51+
char3 = models.CharField[str, str](max_length=5, choices=str_mapping, default="foo")
52+
char4 = models.CharField[str, str](max_length=5, choices=str_tuple(), default="foo")
53+
char5 = models.CharField[str, str](max_length=5, choices=str_mapping(), default="foo")
54+
char6 = models.CharField[str, str](max_length=5, choices=to_named_seq(str_tuple), default="foo")
55+
char7 = models.CharField[str, str](max_length=5, choices=to_named_mapping(str_mapping), default="foo")
56+
char8 = models.CharField[str, str](max_length=5, choices=to_named_seq(str_tuple)(), default="foo")
57+
char9 = models.CharField[str, str](max_length=5, choices=to_named_mapping(str_mapping)(), default="foo")
58+
59+
int1 = models.IntegerField[int, int](choices=IntegerChoices, default=1)
60+
int2 = models.IntegerField[int, int](choices=int_tuple, default=1)
61+
int3 = models.IntegerField[int, int](choices=int_mapping, default=1)
62+
int4 = models.IntegerField[int, int](choices=int_tuple(), default=1)
63+
int5 = models.IntegerField[int, int](choices=int_mapping(), default=1)
64+
int6 = models.IntegerField[int, int](choices=to_named_seq(int_tuple), default=1)
65+
int7 = models.IntegerField[int, int](choices=to_named_seq(int_mapping), default=1)
66+
int8 = models.IntegerField[int, int](choices=to_named_seq(int_tuple)(), default=1)
67+
int9 = models.IntegerField[int, int](choices=to_named_seq(int_mapping)(), default=1)
68+
69+
70+
instance = TestModel()
71+
assert_type(instance.char1, str)
72+
assert_type(instance.char2, str)
73+
assert_type(instance.char3, str)
74+
assert_type(instance.char4, str)
75+
assert_type(instance.char5, str)
76+
assert_type(instance.char6, str)
77+
assert_type(instance.char7, str)
78+
assert_type(instance.char8, str)
79+
assert_type(instance.char9, str)
80+
81+
assert_type(instance.int1, int)
82+
assert_type(instance.int2, int)
83+
assert_type(instance.int3, int)
84+
assert_type(instance.int4, int)
85+
assert_type(instance.int5, int)
86+
assert_type(instance.int6, int)
87+
assert_type(instance.int7, int)
88+
assert_type(instance.int8, int)
89+
assert_type(instance.int9, int)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
- case: db_models_fields_choices
2+
main: |
3+
from django.db import models
4+
5+
class MyModel(models.Model):
6+
char1 = models.CharField[str, str](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]

0 commit comments

Comments
 (0)