Skip to content

Commit d1a968e

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 7e02f44 commit d1a968e

File tree

3 files changed

+89
-7
lines changed

3 files changed

+89
-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, _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]]: ...
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)