Skip to content

Commit ffb6551

Browse files
author
proxy
authored
make BaseModelAdmin generic to properly type methods dealing with models (#504)
* make BaseModelAdmin generic to properly type the `obj` argument of ModelAdmin.delete_model closes #482 * turn BaseModelAdmin into bound generic, run black * add test for generic ModelAdmin
1 parent 40c8bfa commit ffb6551

File tree

2 files changed

+70
-43
lines changed

2 files changed

+70
-43
lines changed

django-stubs/contrib/admin/options.pyi

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
from collections import OrderedDict
2-
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union, Mapping, TypeVar
2+
from typing import (
3+
Any,
4+
Callable,
5+
Dict,
6+
Generic,
7+
Iterator,
8+
List,
9+
Optional,
10+
Sequence,
11+
Set,
12+
Tuple,
13+
Type,
14+
Union,
15+
Mapping,
16+
TypeVar,
17+
)
318

419
from django.forms.forms import BaseForm
520
from django.forms.formsets import BaseFormSet
@@ -57,7 +72,11 @@ _T = TypeVar("_T")
5772
_ListOrTuple = Union[Tuple[_T, ...], List[_T]]
5873
_FieldsetSpec = _ListOrTuple[Tuple[Optional[str], _FieldOpts]]
5974

60-
class BaseModelAdmin:
75+
# Generic type specifically for models, for use in BaseModelAdmin and subclasses
76+
# https://github.com/typeddjango/django-stubs/issues/482
77+
_ModelT = TypeVar("_ModelT", bound=Model)
78+
79+
class BaseModelAdmin(Generic[_ModelT]):
6180
autocomplete_fields: Sequence[str] = ...
6281
raw_id_fields: Sequence[str] = ...
6382
fields: Sequence[Union[str, Sequence[str]]] = ...
@@ -69,7 +88,7 @@ class BaseModelAdmin:
6988
radio_fields: Mapping[str, _Direction] = ...
7089
prepopulated_fields: Mapping[str, Sequence[str]] = ...
7190
formfield_overrides: Mapping[Type[Field], Mapping[str, Any]] = ...
72-
readonly_fields: Sequence[Union[str, Callable[[Model], Any]]] = ...
91+
readonly_fields: Sequence[Union[str, Callable[[_ModelT], Any]]] = ...
7392
ordering: Sequence[str] = ...
7493
sortable_by: Sequence[str] = ...
7594
view_on_site: bool = ...
@@ -92,28 +111,28 @@ class BaseModelAdmin:
92111
self, db_field: ManyToManyField, request: Optional[HttpRequest], **kwargs: Any
93112
) -> ModelMultipleChoiceField: ...
94113
def get_autocomplete_fields(self, request: HttpRequest) -> Tuple: ...
95-
def get_view_on_site_url(self, obj: Optional[Model] = ...) -> Optional[str]: ...
114+
def get_view_on_site_url(self, obj: Optional[_ModelT] = ...) -> Optional[str]: ...
96115
def get_empty_value_display(self) -> SafeText: ...
97-
def get_exclude(self, request: HttpRequest, obj: Optional[Model] = ...) -> Any: ...
98-
def get_fields(self, request: HttpRequest, obj: Optional[Model] = ...) -> Sequence[Union[Callable, str]]: ...
116+
def get_exclude(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> Any: ...
117+
def get_fields(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> Sequence[Union[Callable, str]]: ...
99118
def get_fieldsets(
100-
self, request: HttpRequest, obj: Optional[Model] = ...
119+
self, request: HttpRequest, obj: Optional[_ModelT] = ...
101120
) -> List[Tuple[Optional[str], Dict[str, Any]]]: ...
102121
def get_ordering(self, request: HttpRequest) -> Union[List[str], Tuple]: ...
103-
def get_readonly_fields(self, request: HttpRequest, obj: Optional[Model] = ...) -> Union[List[str], Tuple]: ...
104-
def get_prepopulated_fields(self, request: HttpRequest, obj: Optional[Model] = ...) -> Dict[str, Tuple[str]]: ...
122+
def get_readonly_fields(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> Union[List[str], Tuple]: ...
123+
def get_prepopulated_fields(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> Dict[str, Tuple[str]]: ...
105124
def get_queryset(self, request: HttpRequest) -> QuerySet: ...
106125
def get_sortable_by(self, request: HttpRequest) -> Union[List[Callable], List[str], Tuple]: ...
107126
def lookup_allowed(self, lookup: str, value: str) -> bool: ...
108127
def to_field_allowed(self, request: HttpRequest, to_field: str) -> bool: ...
109128
def has_add_permission(self, request: HttpRequest) -> bool: ...
110-
def has_change_permission(self, request: HttpRequest, obj: Optional[Model] = ...) -> bool: ...
111-
def has_delete_permission(self, request: HttpRequest, obj: Optional[Model] = ...) -> bool: ...
112-
def has_view_permission(self, request: HttpRequest, obj: Optional[Model] = ...) -> bool: ...
129+
def has_change_permission(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> bool: ...
130+
def has_delete_permission(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> bool: ...
131+
def has_view_permission(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> bool: ...
113132
def has_module_permission(self, request: HttpRequest) -> bool: ...
114133

115-
class ModelAdmin(BaseModelAdmin):
116-
list_display: Sequence[Union[str, Callable[[Model], Any]]] = ...
134+
class ModelAdmin(BaseModelAdmin[_ModelT]):
135+
list_display: Sequence[Union[str, Callable[[_ModelT], Any]]] = ...
117136
list_display_links: Optional[Sequence[Union[str, Callable]]] = ...
118137
list_filter: Sequence[Union[str, Type[ListFilter], Tuple[str, Type[ListFilter]]]] = ...
119138
list_select_related: Union[bool, Sequence[str]] = ...
@@ -140,24 +159,24 @@ class ModelAdmin(BaseModelAdmin):
140159
actions_on_top: bool = ...
141160
actions_on_bottom: bool = ...
142161
actions_selection_counter: bool = ...
143-
model: Type[Model] = ...
162+
model: Type[_ModelT] = ...
144163
opts: Options = ...
145164
admin_site: AdminSite = ...
146-
def __init__(self, model: Type[Model], admin_site: Optional[AdminSite]) -> None: ...
147-
def get_inline_instances(self, request: HttpRequest, obj: Optional[Model] = ...) -> List[InlineModelAdmin]: ...
165+
def __init__(self, model: Type[_ModelT], admin_site: Optional[AdminSite]) -> None: ...
166+
def get_inline_instances(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> List[InlineModelAdmin]: ...
148167
def get_urls(self) -> List[URLPattern]: ...
149168
@property
150169
def urls(self) -> List[URLPattern]: ...
151170
@property
152171
def media(self) -> Media: ...
153172
def get_model_perms(self, request: HttpRequest) -> Dict[str, bool]: ...
154-
def get_form(self, request: Any, obj: Optional[Any] = ..., change: bool = ..., **kwargs: Any): ...
173+
def get_form(self, request: Any, obj: Optional[_ModelT] = ..., change: bool = ..., **kwargs: Any): ...
155174
def get_changelist(self, request: HttpRequest, **kwargs: Any) -> Type[ChangeList]: ...
156175
def get_changelist_instance(self, request: HttpRequest) -> ChangeList: ...
157-
def get_object(self, request: HttpRequest, object_id: str, from_field: None = ...) -> Optional[Model]: ...
176+
def get_object(self, request: HttpRequest, object_id: str, from_field: None = ...) -> Optional[_ModelT]: ...
158177
def get_changelist_form(self, request: Any, **kwargs: Any): ...
159178
def get_changelist_formset(self, request: Any, **kwargs: Any): ...
160-
def get_formsets_with_inlines(self, request: HttpRequest, obj: Optional[Model] = ...) -> Iterator[Any]: ...
179+
def get_formsets_with_inlines(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> Iterator[Any]: ...
161180
def get_paginator(
162181
self,
163182
request: HttpRequest,
@@ -166,10 +185,10 @@ class ModelAdmin(BaseModelAdmin):
166185
orphans: int = ...,
167186
allow_empty_first_page: bool = ...,
168187
) -> Paginator: ...
169-
def log_addition(self, request: HttpRequest, object: Model, message: Any) -> LogEntry: ...
170-
def log_change(self, request: HttpRequest, object: Model, message: Any) -> LogEntry: ...
171-
def log_deletion(self, request: HttpRequest, object: Model, object_repr: str) -> LogEntry: ...
172-
def action_checkbox(self, obj: Model) -> SafeText: ...
188+
def log_addition(self, request: HttpRequest, object: _ModelT, message: Any) -> LogEntry: ...
189+
def log_change(self, request: HttpRequest, object: _ModelT, message: Any) -> LogEntry: ...
190+
def log_deletion(self, request: HttpRequest, object: _ModelT, object_repr: str) -> LogEntry: ...
191+
def action_checkbox(self, obj: _ModelT) -> SafeText: ...
173192
def get_actions(self, request: HttpRequest) -> OrderedDict: ...
174193
def get_action_choices(
175194
self, request: HttpRequest, default_choices: List[Tuple[str, str]] = ...
@@ -198,8 +217,8 @@ class ModelAdmin(BaseModelAdmin):
198217
fail_silently: bool = ...,
199218
) -> None: ...
200219
def save_form(self, request: Any, form: Any, change: Any): ...
201-
def save_model(self, request: Any, obj: Any, form: Any, change: Any) -> None: ...
202-
def delete_model(self, request: HttpRequest, obj: Model) -> None: ...
220+
def save_model(self, request: Any, obj: _ModelT, form: Any, change: Any) -> None: ...
221+
def delete_model(self, request: HttpRequest, obj: _ModelT) -> None: ...
203222
def delete_queryset(self, request: HttpRequest, queryset: QuerySet) -> None: ...
204223
def save_formset(self, request: Any, form: Any, formset: Any, change: Any) -> None: ...
205224
def save_related(self, request: Any, form: Any, formsets: Any, change: Any) -> None: ...
@@ -210,19 +229,19 @@ class ModelAdmin(BaseModelAdmin):
210229
add: bool = ...,
211230
change: bool = ...,
212231
form_url: str = ...,
213-
obj: Optional[Any] = ...,
232+
obj: Optional[_ModelT] = ...,
214233
): ...
215234
def response_add(
216-
self, request: HttpRequest, obj: Model, post_url_continue: Optional[str] = ...
235+
self, request: HttpRequest, obj: _ModelT, post_url_continue: Optional[str] = ...
217236
) -> HttpResponse: ...
218-
def response_change(self, request: HttpRequest, obj: Model) -> HttpResponse: ...
219-
def response_post_save_add(self, request: HttpRequest, obj: Model) -> HttpResponseRedirect: ...
220-
def response_post_save_change(self, request: HttpRequest, obj: Model) -> HttpResponseRedirect: ...
237+
def response_change(self, request: HttpRequest, obj: _ModelT) -> HttpResponse: ...
238+
def response_post_save_add(self, request: HttpRequest, obj: _ModelT) -> HttpResponseRedirect: ...
239+
def response_post_save_change(self, request: HttpRequest, obj: _ModelT) -> HttpResponseRedirect: ...
221240
def response_action(self, request: HttpRequest, queryset: QuerySet) -> Optional[HttpResponseBase]: ...
222241
def response_delete(self, request: HttpRequest, obj_display: str, obj_id: int) -> HttpResponse: ...
223242
def render_delete_form(self, request: Any, context: Any): ...
224243
def get_inline_formsets(
225-
self, request: HttpRequest, formsets: List[Any], inline_instances: List[Any], obj: Optional[Model] = ...
244+
self, request: HttpRequest, formsets: List[Any], inline_instances: List[Any], obj: Optional[_ModelT] = ...
226245
) -> List[Any]: ...
227246
def get_changeform_initial_data(self, request: HttpRequest) -> Dict[str, str]: ...
228247
def changeform_view(
@@ -246,8 +265,8 @@ class ModelAdmin(BaseModelAdmin):
246265
def delete_view(self, request: HttpRequest, object_id: str, extra_context: None = ...) -> Any: ...
247266
def history_view(self, request: HttpRequest, object_id: str, extra_context: None = ...) -> HttpResponse: ...
248267

249-
class InlineModelAdmin(BaseModelAdmin):
250-
model: Type[Model] = ...
268+
class InlineModelAdmin(BaseModelAdmin[_ModelT]):
269+
model: Type[_ModelT] = ...
251270
fk_name: str = ...
252271
formset: BaseFormSet = ...
253272
extra: int = ...
@@ -263,13 +282,13 @@ class InlineModelAdmin(BaseModelAdmin):
263282
parent_model: Any = ...
264283
opts: Any = ...
265284
has_registered_model: Any = ...
266-
def __init__(self, parent_model: Union[Type[Model], Model], admin_site: AdminSite) -> None: ...
285+
def __init__(self, parent_model: Union[Type[_ModelT], _ModelT], admin_site: AdminSite) -> None: ...
267286
@property
268287
def media(self) -> Media: ...
269-
def get_extra(self, request: HttpRequest, obj: Optional[Model] = ..., **kwargs: Any) -> int: ...
270-
def get_min_num(self, request: HttpRequest, obj: Optional[Model] = ..., **kwargs: Any) -> Optional[int]: ...
271-
def get_max_num(self, request: HttpRequest, obj: Optional[Model] = ..., **kwargs: Any) -> Optional[int]: ...
272-
def get_formset(self, request: Any, obj: Optional[Any] = ..., **kwargs: Any): ...
288+
def get_extra(self, request: HttpRequest, obj: Optional[_ModelT] = ..., **kwargs: Any) -> int: ...
289+
def get_min_num(self, request: HttpRequest, obj: Optional[_ModelT] = ..., **kwargs: Any) -> Optional[int]: ...
290+
def get_max_num(self, request: HttpRequest, obj: Optional[_ModelT] = ..., **kwargs: Any) -> Optional[int]: ...
291+
def get_formset(self, request: Any, obj: Optional[_ModelT] = ..., **kwargs: Any): ...
273292

274-
class StackedInline(InlineModelAdmin): ...
275-
class TabularInline(InlineModelAdmin): ...
293+
class StackedInline(InlineModelAdmin[_ModelT]): ...
294+
class TabularInline(InlineModelAdmin[_ModelT]): ...

test-data/typecheck/contrib/admin/test_options.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
def an_action(modeladmin: admin.ModelAdmin, request: HttpRequest, queryset: QuerySet) -> None:
1515
pass
1616
17-
class A(admin.ModelAdmin):
17+
class TestModel(models.Model):
18+
pass
19+
20+
class A(admin.ModelAdmin[TestModel]):
1821
# BaseModelAdmin
1922
autocomplete_fields = ("strs",)
2023
raw_id_fields = ["strs"]
@@ -71,6 +74,11 @@
7174
actions_selection_counter = True
7275
admin_site = AdminSite()
7376
77+
# test generic ModelAdmin
78+
# https://github.com/typeddjango/django-stubs/pull/504
79+
# this will fail if `model` has a type other than the generic specified in the class declaration
80+
model = TestModel
81+
7482
def a_method_action(self, request, queryset):
7583
pass
7684
@@ -127,4 +135,4 @@
127135
pass
128136
129137
class A(admin.ModelAdmin):
130-
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[ModelAdmin, HttpRequest, QuerySet[Any]], None], str]"
138+
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[ModelAdmin[Any], HttpRequest, QuerySet[Any]], None], str]"

0 commit comments

Comments
 (0)