Skip to content

Commit 4debb57

Browse files
authored
Improve stubs for flatten and flatten_fieldsets (#2572)
* Add new `_ListDisplayT` type alias This renames `_DisplayT` to `_ListDisplayT` and creates a new `_DisplayT` alias with the contents of the `_ListOrTuple`. This will be used in the next commit. * Improve stubs for `flatten` and `flatten_fieldsets` These functions are implemented in a generic way that doesn't generally care about the content. The existing return type of `Callable | str` makes these awkward to use. I've changed the use of `Sequence` in the `_FieldGroups` alias to use `_ListOrTuple`. This is another case where `ModelAdmin.fields` and the `"fields"` item in fieldsets are presumed by Django to be a `list` or `tuple`, e.g. there are some system checks that ensure this. See https://github.com/django/django/blob/afbb8c709d40e77b3f71c152d363c5ad95ceec2d/django/contrib/admin/utils.py#L102-L120
1 parent 34f93f1 commit 4debb57

File tree

4 files changed

+103
-18
lines changed

4 files changed

+103
-18
lines changed

django-stubs/contrib/admin/options.pyi

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class IncorrectLookupParameters(Exception): ...
5656
FORMFIELD_FOR_DBFIELD_DEFAULTS: Any
5757
csrf_protect_m: Any
5858

59-
_FieldGroups: TypeAlias = Sequence[str | Sequence[str]]
59+
_FieldGroups: TypeAlias = _ListOrTuple[str | _ListOrTuple[str]]
6060

6161
@type_check_only
6262
class _OptionalFieldOpts(TypedDict, total=False):
@@ -67,9 +67,6 @@ class _OptionalFieldOpts(TypedDict, total=False):
6767
class _FieldOpts(_OptionalFieldOpts, total=True):
6868
fields: _FieldGroups
6969

70-
# Workaround for mypy issue, a Sequence type should be preferred here.
71-
# https://github.com/python/mypy/issues/8921
72-
# _FieldsetSpec = Sequence[Tuple[Optional[str], _FieldOpts]]
7370
_FieldsetSpec: TypeAlias = _ListOrTuple[tuple[_StrOrPromise | None, _FieldOpts]]
7471
_ListFilterT: TypeAlias = (
7572
type[ListFilter]
@@ -82,7 +79,8 @@ _ListFilterT: TypeAlias = (
8279
# Generic type specifically for models, for use in BaseModelAdmin and subclasses
8380
# https://github.com/typeddjango/django-stubs/issues/482
8481
_ModelT = TypeVar("_ModelT", bound=Model)
85-
_DisplayT: TypeAlias = _ListOrTuple[str | Callable[[_ModelT], str | bool]]
82+
_DisplayT: TypeAlias = str | Callable[[_ModelT], str | bool]
83+
_ListDisplayT: TypeAlias = _ListOrTuple[_DisplayT[_ModelT]]
8684

8785
# Options `form`, `list_display`, `list_display_links` and `actions` are not marked as `ClassVar` due to the
8886
# limitations of the current type system: `ClassVar` cannot contain type variables.
@@ -130,7 +128,7 @@ class BaseModelAdmin(Generic[_ModelT]):
130128
def get_readonly_fields(self, request: HttpRequest, obj: _ModelT | None = ...) -> _ListOrTuple[str]: ...
131129
def get_prepopulated_fields(self, request: HttpRequest, obj: _ModelT | None = ...) -> dict[str, Sequence[str]]: ...
132130
def get_queryset(self, request: HttpRequest) -> QuerySet[_ModelT]: ...
133-
def get_sortable_by(self, request: HttpRequest) -> _DisplayT[_ModelT]: ...
131+
def get_sortable_by(self, request: HttpRequest) -> _ListDisplayT[_ModelT]: ...
134132
@overload
135133
@deprecated("None value for the request parameter will be removed in Django 6.0.")
136134
def lookup_allowed(self, lookup: str, value: str, request: None = None) -> bool: ...
@@ -150,8 +148,8 @@ _ModelAdmin = TypeVar("_ModelAdmin", bound=ModelAdmin[Any])
150148
_ActionCallable: TypeAlias = Callable[[_ModelAdmin, HttpRequest, QuerySet[_ModelT]], HttpResponseBase | None]
151149

152150
class ModelAdmin(BaseModelAdmin[_ModelT]):
153-
list_display: _DisplayT[_ModelT]
154-
list_display_links: _DisplayT[_ModelT] | None
151+
list_display: _ListDisplayT[_ModelT]
152+
list_display_links: _ListDisplayT[_ModelT] | None
155153
list_filter: ClassVar[_ListOrTuple[_ListFilterT]]
156154
list_select_related: ClassVar[bool | _ListOrTuple[str]]
157155
list_per_page: ClassVar[int]
@@ -220,8 +218,10 @@ class ModelAdmin(BaseModelAdmin[_ModelT]):
220218
self, request: HttpRequest, default_choices: list[tuple[str, str]] = ...
221219
) -> list[tuple[str, str]]: ...
222220
def get_action(self, action: Callable | str) -> tuple[Callable[..., str], str, str] | None: ...
223-
def get_list_display(self, request: HttpRequest) -> _DisplayT[_ModelT]: ...
224-
def get_list_display_links(self, request: HttpRequest, list_display: _DisplayT[_ModelT]) -> _DisplayT[_ModelT]: ...
221+
def get_list_display(self, request: HttpRequest) -> _ListDisplayT[_ModelT]: ...
222+
def get_list_display_links(
223+
self, request: HttpRequest, list_display: _ListDisplayT[_ModelT]
224+
) -> _ListDisplayT[_ModelT]: ...
225225
def get_list_filter(self, request: HttpRequest) -> _ListOrTuple[_ListFilterT]: ...
226226
def get_list_select_related(self, request: HttpRequest) -> bool | _ListOrTuple[str]: ...
227227
def get_search_fields(self, request: HttpRequest) -> _ListOrTuple[str]: ...

django-stubs/contrib/admin/utils.pyi

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ from typing import Any, Literal, TypeVar, overload, type_check_only
55
from uuid import UUID
66

77
from _typeshed import Unused
8-
from django.contrib.admin.options import BaseModelAdmin
8+
from django.contrib.admin.options import BaseModelAdmin, _DisplayT, _FieldGroups, _FieldsetSpec, _ListDisplayT, _ModelT
99
from django.contrib.admin.sites import AdminSite
1010
from django.db.models.base import Model
1111
from django.db.models.deletion import Collector
@@ -31,8 +31,11 @@ def prepare_lookup_value(
3131
def build_q_object_from_lookup_parameters(parameters: dict[str, list[str]]) -> Q: ...
3232
def quote(s: int | str | UUID) -> str: ...
3333
def unquote(s: str) -> str: ...
34-
def flatten(fields: Any) -> list[Callable | str]: ...
35-
def flatten_fieldsets(fieldsets: Any) -> list[Callable | str]: ...
34+
@overload
35+
def flatten(fields: _FieldGroups) -> list[str]: ...
36+
@overload
37+
def flatten(fields: _ListDisplayT[_ModelT]) -> list[_DisplayT[_ModelT]]: ...
38+
def flatten_fieldsets(fieldsets: _FieldsetSpec) -> list[str]: ...
3639
def get_deleted_objects(
3740
objs: Sequence[Model | None] | QuerySet[Model], request: HttpRequest, admin_site: AdminSite
3841
) -> tuple[list[str], dict[str, int], set[str], list[str]]: ...

django-stubs/contrib/admin/views/main.pyi

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from typing import Any, Literal
33

44
from django import forms
55
from django.contrib.admin.filters import ListFilter
6-
from django.contrib.admin.options import ModelAdmin, _DisplayT, _ListFilterT
6+
from django.contrib.admin.options import ModelAdmin, _ListDisplayT, _ListFilterT
77
from django.db.models.base import Model
88
from django.db.models.expressions import Expression
99
from django.db.models.options import Options
@@ -26,8 +26,8 @@ class ChangeList:
2626
opts: Options
2727
lookup_opts: Options
2828
root_queryset: QuerySet
29-
list_display: _DisplayT
30-
list_display_links: _DisplayT
29+
list_display: _ListDisplayT
30+
list_display_links: _ListDisplayT
3131
list_filter: Sequence[_ListFilterT]
3232
date_hierarchy: Any
3333
search_fields: Sequence[str]
@@ -58,8 +58,8 @@ class ChangeList:
5858
self,
5959
request: HttpRequest,
6060
model: type[Model],
61-
list_display: _DisplayT,
62-
list_display_links: _DisplayT,
61+
list_display: _ListDisplayT,
62+
list_display_links: _ListDisplayT,
6363
list_filter: Sequence[_ListFilterT],
6464
date_hierarchy: str | None,
6565
search_fields: Sequence[str],
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from __future__ import annotations
2+
3+
from django import http
4+
from django.contrib import admin
5+
from django.contrib.admin.options import _DisplayT
6+
from django.contrib.admin.utils import flatten, flatten_fieldsets
7+
from django.db import models
8+
from typing_extensions import assert_type
9+
10+
11+
@admin.display(description="Name")
12+
def upper_case_name(obj: Person) -> str:
13+
return f"{obj.first_name} {obj.last_name}".upper() # pyright: ignore[reportUnknownMemberType]
14+
15+
16+
class Person(models.Model):
17+
first_name = models.CharField(max_length=None) # pyright: ignore[reportUnknownVariableType]
18+
last_name = models.CharField(max_length=None) # pyright: ignore[reportUnknownVariableType]
19+
birthday = models.DateField() # pyright: ignore[reportUnknownVariableType]
20+
21+
22+
class PersonListAdmin(admin.ModelAdmin[Person]):
23+
fields = [["first_name", "last_name"], "birthday"]
24+
list_display = [upper_case_name, "birthday"]
25+
26+
27+
class PersonTupleAdmin(admin.ModelAdmin[Person]):
28+
fields = (("first_name", "last_name"), "birthday")
29+
list_display = (upper_case_name, "birthday")
30+
31+
32+
class PersonFieldsetListAdmin(admin.ModelAdmin[Person]):
33+
fieldsets = [
34+
(
35+
"Personal Details",
36+
{
37+
"description": "Personal details of a person.",
38+
"fields": [["first_name", "last_name"], "birthday"],
39+
},
40+
)
41+
]
42+
43+
44+
class PersonFieldsetTupleAdmin(admin.ModelAdmin[Person]):
45+
fieldsets = (
46+
(
47+
"Personal Details",
48+
{
49+
"description": "Personal details of a person.",
50+
"fields": (("first_name", "last_name"), "birthday"),
51+
},
52+
),
53+
)
54+
55+
56+
request = http.HttpRequest()
57+
admin_site = admin.AdminSite()
58+
person_list_admin = PersonListAdmin(Person, admin_site)
59+
person_tuple_admin = PersonTupleAdmin(Person, admin_site)
60+
person_fieldset_list_admin = PersonFieldsetListAdmin(Person, admin_site)
61+
person_fieldset_tuple_admin = PersonFieldsetTupleAdmin(Person, admin_site)
62+
63+
# For some reason, pyright cannot see that these are not `None`.
64+
assert person_list_admin.fields is not None
65+
assert person_tuple_admin.fields is not None
66+
assert person_fieldset_list_admin.fieldsets is not None
67+
assert person_fieldset_tuple_admin.fieldsets is not None
68+
69+
assert_type(flatten(person_list_admin.fields), list[str])
70+
assert_type(flatten(person_list_admin.get_fields(request)), list[str])
71+
assert_type(flatten(person_tuple_admin.fields), list[str])
72+
assert_type(flatten(person_tuple_admin.get_fields(request)), list[str])
73+
74+
assert_type(flatten(person_list_admin.list_display), list[_DisplayT[Person]])
75+
assert_type(flatten(person_list_admin.get_list_display(request)), list[_DisplayT[Person]])
76+
assert_type(flatten(person_tuple_admin.list_display), list[_DisplayT[Person]])
77+
assert_type(flatten(person_tuple_admin.get_list_display(request)), list[_DisplayT[Person]])
78+
79+
assert_type(flatten_fieldsets(person_fieldset_list_admin.fieldsets), list[str])
80+
assert_type(flatten_fieldsets(person_fieldset_list_admin.get_fieldsets(request)), list[str])
81+
assert_type(flatten_fieldsets(person_fieldset_tuple_admin.fieldsets), list[str])
82+
assert_type(flatten_fieldsets(person_fieldset_tuple_admin.get_fieldsets(request)), list[str])

0 commit comments

Comments
 (0)