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
This commit is contained in:
proxy
2020-10-27 18:02:43 -04:00
committed by GitHub
parent 40c8bfa510
commit ffb6551eb4
2 changed files with 70 additions and 43 deletions

View File

@@ -1,5 +1,20 @@
from collections import OrderedDict
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union, Mapping, TypeVar
from typing import (
Any,
Callable,
Dict,
Generic,
Iterator,
List,
Optional,
Sequence,
Set,
Tuple,
Type,
Union,
Mapping,
TypeVar,
)
from django.forms.forms import BaseForm
from django.forms.formsets import BaseFormSet
@@ -57,7 +72,11 @@ _T = TypeVar("_T")
_ListOrTuple = Union[Tuple[_T, ...], List[_T]]
_FieldsetSpec = _ListOrTuple[Tuple[Optional[str], _FieldOpts]]
class BaseModelAdmin:
# Generic type specifically for models, for use in BaseModelAdmin and subclasses
# https://github.com/typeddjango/django-stubs/issues/482
_ModelT = TypeVar("_ModelT", bound=Model)
class BaseModelAdmin(Generic[_ModelT]):
autocomplete_fields: Sequence[str] = ...
raw_id_fields: Sequence[str] = ...
fields: Sequence[Union[str, Sequence[str]]] = ...
@@ -69,7 +88,7 @@ class BaseModelAdmin:
radio_fields: Mapping[str, _Direction] = ...
prepopulated_fields: Mapping[str, Sequence[str]] = ...
formfield_overrides: Mapping[Type[Field], Mapping[str, Any]] = ...
readonly_fields: Sequence[Union[str, Callable[[Model], Any]]] = ...
readonly_fields: Sequence[Union[str, Callable[[_ModelT], Any]]] = ...
ordering: Sequence[str] = ...
sortable_by: Sequence[str] = ...
view_on_site: bool = ...
@@ -92,28 +111,28 @@ class BaseModelAdmin:
self, db_field: ManyToManyField, request: Optional[HttpRequest], **kwargs: Any
) -> ModelMultipleChoiceField: ...
def get_autocomplete_fields(self, request: HttpRequest) -> Tuple: ...
def get_view_on_site_url(self, obj: Optional[Model] = ...) -> Optional[str]: ...
def get_view_on_site_url(self, obj: Optional[_ModelT] = ...) -> Optional[str]: ...
def get_empty_value_display(self) -> SafeText: ...
def get_exclude(self, request: HttpRequest, obj: Optional[Model] = ...) -> Any: ...
def get_fields(self, request: HttpRequest, obj: Optional[Model] = ...) -> Sequence[Union[Callable, str]]: ...
def get_exclude(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> Any: ...
def get_fields(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> Sequence[Union[Callable, str]]: ...
def get_fieldsets(
self, request: HttpRequest, obj: Optional[Model] = ...
self, request: HttpRequest, obj: Optional[_ModelT] = ...
) -> List[Tuple[Optional[str], Dict[str, Any]]]: ...
def get_ordering(self, request: HttpRequest) -> Union[List[str], Tuple]: ...
def get_readonly_fields(self, request: HttpRequest, obj: Optional[Model] = ...) -> Union[List[str], Tuple]: ...
def get_prepopulated_fields(self, request: HttpRequest, obj: Optional[Model] = ...) -> Dict[str, Tuple[str]]: ...
def get_readonly_fields(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> Union[List[str], Tuple]: ...
def get_prepopulated_fields(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> Dict[str, Tuple[str]]: ...
def get_queryset(self, request: HttpRequest) -> QuerySet: ...
def get_sortable_by(self, request: HttpRequest) -> Union[List[Callable], List[str], Tuple]: ...
def lookup_allowed(self, lookup: str, value: str) -> bool: ...
def to_field_allowed(self, request: HttpRequest, to_field: str) -> bool: ...
def has_add_permission(self, request: HttpRequest) -> bool: ...
def has_change_permission(self, request: HttpRequest, obj: Optional[Model] = ...) -> bool: ...
def has_delete_permission(self, request: HttpRequest, obj: Optional[Model] = ...) -> bool: ...
def has_view_permission(self, request: HttpRequest, obj: Optional[Model] = ...) -> bool: ...
def has_change_permission(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> bool: ...
def has_delete_permission(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> bool: ...
def has_view_permission(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> bool: ...
def has_module_permission(self, request: HttpRequest) -> bool: ...
class ModelAdmin(BaseModelAdmin):
list_display: Sequence[Union[str, Callable[[Model], Any]]] = ...
class ModelAdmin(BaseModelAdmin[_ModelT]):
list_display: Sequence[Union[str, Callable[[_ModelT], Any]]] = ...
list_display_links: Optional[Sequence[Union[str, Callable]]] = ...
list_filter: Sequence[Union[str, Type[ListFilter], Tuple[str, Type[ListFilter]]]] = ...
list_select_related: Union[bool, Sequence[str]] = ...
@@ -140,24 +159,24 @@ class ModelAdmin(BaseModelAdmin):
actions_on_top: bool = ...
actions_on_bottom: bool = ...
actions_selection_counter: bool = ...
model: Type[Model] = ...
model: Type[_ModelT] = ...
opts: Options = ...
admin_site: AdminSite = ...
def __init__(self, model: Type[Model], admin_site: Optional[AdminSite]) -> None: ...
def get_inline_instances(self, request: HttpRequest, obj: Optional[Model] = ...) -> List[InlineModelAdmin]: ...
def __init__(self, model: Type[_ModelT], admin_site: Optional[AdminSite]) -> None: ...
def get_inline_instances(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> List[InlineModelAdmin]: ...
def get_urls(self) -> List[URLPattern]: ...
@property
def urls(self) -> List[URLPattern]: ...
@property
def media(self) -> Media: ...
def get_model_perms(self, request: HttpRequest) -> Dict[str, bool]: ...
def get_form(self, request: Any, obj: Optional[Any] = ..., change: bool = ..., **kwargs: Any): ...
def get_form(self, request: Any, obj: Optional[_ModelT] = ..., change: bool = ..., **kwargs: Any): ...
def get_changelist(self, request: HttpRequest, **kwargs: Any) -> Type[ChangeList]: ...
def get_changelist_instance(self, request: HttpRequest) -> ChangeList: ...
def get_object(self, request: HttpRequest, object_id: str, from_field: None = ...) -> Optional[Model]: ...
def get_object(self, request: HttpRequest, object_id: str, from_field: None = ...) -> Optional[_ModelT]: ...
def get_changelist_form(self, request: Any, **kwargs: Any): ...
def get_changelist_formset(self, request: Any, **kwargs: Any): ...
def get_formsets_with_inlines(self, request: HttpRequest, obj: Optional[Model] = ...) -> Iterator[Any]: ...
def get_formsets_with_inlines(self, request: HttpRequest, obj: Optional[_ModelT] = ...) -> Iterator[Any]: ...
def get_paginator(
self,
request: HttpRequest,
@@ -166,10 +185,10 @@ class ModelAdmin(BaseModelAdmin):
orphans: int = ...,
allow_empty_first_page: bool = ...,
) -> Paginator: ...
def log_addition(self, request: HttpRequest, object: Model, message: Any) -> LogEntry: ...
def log_change(self, request: HttpRequest, object: Model, message: Any) -> LogEntry: ...
def log_deletion(self, request: HttpRequest, object: Model, object_repr: str) -> LogEntry: ...
def action_checkbox(self, obj: Model) -> SafeText: ...
def log_addition(self, request: HttpRequest, object: _ModelT, message: Any) -> LogEntry: ...
def log_change(self, request: HttpRequest, object: _ModelT, message: Any) -> LogEntry: ...
def log_deletion(self, request: HttpRequest, object: _ModelT, object_repr: str) -> LogEntry: ...
def action_checkbox(self, obj: _ModelT) -> SafeText: ...
def get_actions(self, request: HttpRequest) -> OrderedDict: ...
def get_action_choices(
self, request: HttpRequest, default_choices: List[Tuple[str, str]] = ...
@@ -198,8 +217,8 @@ class ModelAdmin(BaseModelAdmin):
fail_silently: bool = ...,
) -> None: ...
def save_form(self, request: Any, form: Any, change: Any): ...
def save_model(self, request: Any, obj: Any, form: Any, change: Any) -> None: ...
def delete_model(self, request: HttpRequest, obj: Model) -> None: ...
def save_model(self, request: Any, obj: _ModelT, form: Any, change: Any) -> None: ...
def delete_model(self, request: HttpRequest, obj: _ModelT) -> None: ...
def delete_queryset(self, request: HttpRequest, queryset: QuerySet) -> None: ...
def save_formset(self, request: Any, form: Any, formset: Any, change: Any) -> None: ...
def save_related(self, request: Any, form: Any, formsets: Any, change: Any) -> None: ...
@@ -210,19 +229,19 @@ class ModelAdmin(BaseModelAdmin):
add: bool = ...,
change: bool = ...,
form_url: str = ...,
obj: Optional[Any] = ...,
obj: Optional[_ModelT] = ...,
): ...
def response_add(
self, request: HttpRequest, obj: Model, post_url_continue: Optional[str] = ...
self, request: HttpRequest, obj: _ModelT, post_url_continue: Optional[str] = ...
) -> HttpResponse: ...
def response_change(self, request: HttpRequest, obj: Model) -> HttpResponse: ...
def response_post_save_add(self, request: HttpRequest, obj: Model) -> HttpResponseRedirect: ...
def response_post_save_change(self, request: HttpRequest, obj: Model) -> HttpResponseRedirect: ...
def response_change(self, request: HttpRequest, obj: _ModelT) -> HttpResponse: ...
def response_post_save_add(self, request: HttpRequest, obj: _ModelT) -> HttpResponseRedirect: ...
def response_post_save_change(self, request: HttpRequest, obj: _ModelT) -> HttpResponseRedirect: ...
def response_action(self, request: HttpRequest, queryset: QuerySet) -> Optional[HttpResponseBase]: ...
def response_delete(self, request: HttpRequest, obj_display: str, obj_id: int) -> HttpResponse: ...
def render_delete_form(self, request: Any, context: Any): ...
def get_inline_formsets(
self, request: HttpRequest, formsets: List[Any], inline_instances: List[Any], obj: Optional[Model] = ...
self, request: HttpRequest, formsets: List[Any], inline_instances: List[Any], obj: Optional[_ModelT] = ...
) -> List[Any]: ...
def get_changeform_initial_data(self, request: HttpRequest) -> Dict[str, str]: ...
def changeform_view(
@@ -246,8 +265,8 @@ class ModelAdmin(BaseModelAdmin):
def delete_view(self, request: HttpRequest, object_id: str, extra_context: None = ...) -> Any: ...
def history_view(self, request: HttpRequest, object_id: str, extra_context: None = ...) -> HttpResponse: ...
class InlineModelAdmin(BaseModelAdmin):
model: Type[Model] = ...
class InlineModelAdmin(BaseModelAdmin[_ModelT]):
model: Type[_ModelT] = ...
fk_name: str = ...
formset: BaseFormSet = ...
extra: int = ...
@@ -263,13 +282,13 @@ class InlineModelAdmin(BaseModelAdmin):
parent_model: Any = ...
opts: Any = ...
has_registered_model: Any = ...
def __init__(self, parent_model: Union[Type[Model], Model], admin_site: AdminSite) -> None: ...
def __init__(self, parent_model: Union[Type[_ModelT], _ModelT], admin_site: AdminSite) -> None: ...
@property
def media(self) -> Media: ...
def get_extra(self, request: HttpRequest, obj: Optional[Model] = ..., **kwargs: Any) -> int: ...
def get_min_num(self, request: HttpRequest, obj: Optional[Model] = ..., **kwargs: Any) -> Optional[int]: ...
def get_max_num(self, request: HttpRequest, obj: Optional[Model] = ..., **kwargs: Any) -> Optional[int]: ...
def get_formset(self, request: Any, obj: Optional[Any] = ..., **kwargs: Any): ...
def get_extra(self, request: HttpRequest, obj: Optional[_ModelT] = ..., **kwargs: Any) -> int: ...
def get_min_num(self, request: HttpRequest, obj: Optional[_ModelT] = ..., **kwargs: Any) -> Optional[int]: ...
def get_max_num(self, request: HttpRequest, obj: Optional[_ModelT] = ..., **kwargs: Any) -> Optional[int]: ...
def get_formset(self, request: Any, obj: Optional[_ModelT] = ..., **kwargs: Any): ...
class StackedInline(InlineModelAdmin): ...
class TabularInline(InlineModelAdmin): ...
class StackedInline(InlineModelAdmin[_ModelT]): ...
class TabularInline(InlineModelAdmin[_ModelT]): ...

View File

@@ -14,7 +14,10 @@
def an_action(modeladmin: admin.ModelAdmin, request: HttpRequest, queryset: QuerySet) -> None:
pass
class A(admin.ModelAdmin):
class TestModel(models.Model):
pass
class A(admin.ModelAdmin[TestModel]):
# BaseModelAdmin
autocomplete_fields = ("strs",)
raw_id_fields = ["strs"]
@@ -71,6 +74,11 @@
actions_selection_counter = True
admin_site = AdminSite()
# test generic ModelAdmin
# https://github.com/typeddjango/django-stubs/pull/504
# this will fail if `model` has a type other than the generic specified in the class declaration
model = TestModel
def a_method_action(self, request, queryset):
pass
@@ -127,4 +135,4 @@
pass
class A(admin.ModelAdmin):
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[ModelAdmin, HttpRequest, QuerySet[Any]], None], str]"
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[ModelAdmin[Any], HttpRequest, QuerySet[Any]], None], str]"