Skip to content

Commit bca1c4d

Browse files
committed
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 9186f2d commit bca1c4d

File tree

3 files changed

+96
-7
lines changed

3 files changed

+96
-7
lines changed

django-stubs/contrib/admin/options.pyi

Lines changed: 1 addition & 4 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]

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
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) -> list[_DisplayT]: ...
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]]: ...
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from django import http
6+
from django.contrib import admin
7+
from django.contrib.admin.options import _DisplayT, _ListDisplayT
8+
from django.contrib.admin.utils import flatten, flatten_fieldsets
9+
from django.db import models
10+
from typing_extensions import assert_type
11+
12+
13+
@admin.display(description="Name")
14+
def upper_case_name(obj: Person) -> str:
15+
return f"{obj.first_name} {obj.last_name}".upper() # pyright: ignore[reportUnknownMemberType]
16+
17+
18+
class Person(models.Model):
19+
first_name = models.CharField(max_length=None) # pyright: ignore[reportUnknownVariableType]
20+
last_name = models.CharField(max_length=None) # pyright: ignore[reportUnknownVariableType]
21+
birthday = models.DateField() # pyright: ignore[reportUnknownVariableType]
22+
23+
24+
class PersonListAdmin(admin.ModelAdmin[Person]):
25+
fields = [["first_name", "last_name"], "birthday"]
26+
list_display = [upper_case_name, "birthday"]
27+
28+
29+
class PersonTupleAdmin(admin.ModelAdmin[Person]):
30+
fields = (("first_name", "last_name"), "birthday")
31+
# Explicit typing required in this case where we have a `tuple`.
32+
# Without it, this effectively becomes `tuple[object, ...]`.
33+
# This is because of the whole mypy join vs union issue.
34+
# See https://github.com/python/mypy/issues?q=is%3Aissue%20state%3Aopen%20label%3Atopic-join-v-union
35+
list_display: _ListDisplayT = (upper_case_name, "birthday")
36+
37+
38+
class PersonFieldsetListAdmin(admin.ModelAdmin[Person]):
39+
fieldsets = [
40+
(
41+
"Personal Details",
42+
{
43+
"description": "Personal details of a person.",
44+
"fields": [["first_name", "last_name"], "birthday"],
45+
},
46+
)
47+
]
48+
49+
50+
class PersonFieldsetTupleAdmin(admin.ModelAdmin[Person]):
51+
fieldsets = (
52+
(
53+
"Personal Details",
54+
{
55+
"description": "Personal details of a person.",
56+
"fields": (("first_name", "last_name"), "birthday"),
57+
},
58+
),
59+
)
60+
61+
62+
request = http.HttpRequest()
63+
admin_site = admin.AdminSite()
64+
person_list_admin = PersonListAdmin(Person, admin_site)
65+
person_tuple_admin = PersonTupleAdmin(Person, admin_site)
66+
person_fieldset_list_admin = PersonFieldsetListAdmin(Person, admin_site)
67+
person_fieldset_tuple_admin = PersonFieldsetTupleAdmin(Person, admin_site)
68+
69+
# For some reason, pyright cannot see that these are defined.
70+
assert person_list_admin.fields is not None
71+
assert person_tuple_admin.fields is not None
72+
assert person_fieldset_list_admin.fieldsets is not None
73+
assert person_fieldset_tuple_admin.fieldsets is not None
74+
75+
assert_type(flatten(person_list_admin.fields), list[str])
76+
assert_type(flatten(person_list_admin.get_fields(request)), list[str])
77+
assert_type(flatten(person_tuple_admin.fields), list[str])
78+
assert_type(flatten(person_tuple_admin.get_fields(request)), list[str])
79+
80+
# Currently `_ModelT` appears to be `Any` instead of `Person`.
81+
assert_type(flatten(person_list_admin.list_display), list[_DisplayT[Any]])
82+
assert_type(flatten(person_list_admin.get_list_display(request)), list[_DisplayT[Any]])
83+
assert_type(flatten(person_tuple_admin.list_display), list[_DisplayT[Any]])
84+
assert_type(flatten(person_tuple_admin.get_list_display(request)), list[_DisplayT[Any]])
85+
86+
assert_type(flatten_fieldsets(person_fieldset_list_admin.fieldsets), list[str])
87+
assert_type(flatten_fieldsets(person_fieldset_list_admin.get_fieldsets(request)), list[str])
88+
assert_type(flatten_fieldsets(person_fieldset_tuple_admin.fieldsets), list[str])
89+
assert_type(flatten_fieldsets(person_fieldset_tuple_admin.get_fieldsets(request)), list[str])

0 commit comments

Comments
 (0)