Increase accuracy of ModelAdmin types (#375)

* Increase accuracy of ModelAdmin types

* Add comment for regression test
This commit is contained in:
Anton Agestam
2020-05-29 23:24:47 +02:00
committed by GitHub
parent 64cbb0f70e
commit d5c1bfb12a
2 changed files with 179 additions and 33 deletions

View File

@@ -1,5 +1,9 @@
from collections import OrderedDict
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union, Mapping, TypeVar
from django.forms.forms import BaseForm
from django.forms.formsets import BaseFormSet
from typing_extensions import Literal, TypedDict
from django.contrib.admin.filters import ListFilter
from django.contrib.admin.models import LogEntry
@@ -26,8 +30,10 @@ from django.db.models.fields import Field
IS_POPUP_VAR: str
TO_FIELD_VAR: str
HORIZONTAL: Any
VERTICAL: Any
HORIZONTAL: Literal[1] = ...
VERTICAL: Literal[2] = ...
_Direction = Union[Literal[1], Literal[2]]
def get_content_type_for_model(obj: Union[Type[Model], Model]) -> ContentType: ...
def get_ul_class(radio_style: int) -> str: ...
@@ -37,21 +43,35 @@ class IncorrectLookupParameters(Exception): ...
FORMFIELD_FOR_DBFIELD_DEFAULTS: Any
csrf_protect_m: Any
class _OptionalFieldOpts(TypedDict, total=False):
classes: Sequence[str]
description: str
class _FieldOpts(_OptionalFieldOpts, total=True):
fields: Sequence[Union[str, Sequence[str]]]
# Workaround for mypy issue, a Sequence type should be preferred here.
# https://github.com/python/mypy/issues/8921
# _FieldsetSpec = Sequence[Tuple[Optional[str], _FieldOpts]]
_T = TypeVar("_T")
_ListOrTuple = Union[Tuple[_T, ...], List[_T]]
_FieldsetSpec = _ListOrTuple[Tuple[Optional[str], _FieldOpts]]
class BaseModelAdmin:
autocomplete_fields: Any = ...
raw_id_fields: Any = ...
fields: Any = ...
exclude: Any = ...
fieldsets: Any = ...
form: Any = ...
filter_vertical: Any = ...
filter_horizontal: Any = ...
radio_fields: Any = ...
prepopulated_fields: Any = ...
formfield_overrides: Any = ...
readonly_fields: Any = ...
ordering: Any = ...
sortable_by: Any = ...
autocomplete_fields: Sequence[str] = ...
raw_id_fields: Sequence[str] = ...
fields: Sequence[Union[str, Sequence[str]]] = ...
exclude: Sequence[str] = ...
fieldsets: _FieldsetSpec = ...
form: Type[BaseForm] = ...
filter_vertical: Sequence[str] = ...
filter_horizontal: Sequence[str] = ...
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]]] = ...
ordering: Sequence[str] = ...
sortable_by: Sequence[str] = ...
view_on_site: bool = ...
show_full_result_count: bool = ...
checks_class: Any = ...
@@ -93,7 +113,7 @@ class BaseModelAdmin:
def has_module_permission(self, request: HttpRequest) -> bool: ...
class ModelAdmin(BaseModelAdmin):
list_display: Sequence[Union[str, Callable]] = ...
list_display: Sequence[Union[str, Callable[[Model], 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]] = ...
@@ -101,21 +121,21 @@ class ModelAdmin(BaseModelAdmin):
list_max_show_all: int = ...
list_editable: Sequence[str] = ...
search_fields: Sequence[str] = ...
date_hierarchy: Optional[Any] = ...
date_hierarchy: Optional[str] = ...
save_as: bool = ...
save_as_continue: bool = ...
save_on_top: bool = ...
paginator: Any = ...
paginator: Type = ...
preserve_filters: bool = ...
inlines: Sequence[Type[InlineModelAdmin]] = ...
add_form_template: Any = ...
change_form_template: Any = ...
change_list_template: Any = ...
delete_confirmation_template: Any = ...
delete_selected_confirmation_template: Any = ...
object_history_template: Any = ...
popup_response_template: Any = ...
actions: Any = ...
add_form_template: str = ...
change_form_template: str = ...
change_list_template: str = ...
delete_confirmation_template: str = ...
delete_selected_confirmation_template: str = ...
object_history_template: str = ...
popup_response_template: str = ...
actions: Sequence[Callable[[ModelAdmin, HttpRequest, QuerySet], None]] = ...
action_form: Any = ...
actions_on_top: bool = ...
actions_on_bottom: bool = ...
@@ -227,9 +247,9 @@ class ModelAdmin(BaseModelAdmin):
def history_view(self, request: HttpRequest, object_id: str, extra_context: None = ...) -> HttpResponse: ...
class InlineModelAdmin(BaseModelAdmin):
model: Any = ...
fk_name: Any = ...
formset: Any = ...
model: Type[Model] = ...
fk_name: str = ...
formset: BaseFormSet = ...
extra: int = ...
min_num: Optional[int] = ...
max_num: Optional[int] = ...
@@ -238,8 +258,8 @@ class InlineModelAdmin(BaseModelAdmin):
verbose_name_plural: Optional[str] = ...
can_delete: bool = ...
show_change_link: bool = ...
classes: Any = ...
admin_site: Any = ...
classes: Optional[Sequence[str]] = ...
admin_site: AdminSite = ...
parent_model: Any = ...
opts: Any = ...
has_registered_model: Any = ...

View File

@@ -0,0 +1,126 @@
# "Happy path" test for model admin, trying to cover as many valid
# configurations as possible.
- case: test_full_admin
main: |
from django.contrib import admin
from django.forms import Form, Textarea
from django.db import models
from django.core.paginator import Paginator
from django.contrib.admin.sites import AdminSite
from django.db.models.options import Options
from django.http.request import HttpRequest
from django.db.models.query import QuerySet
def an_action(modeladmin: admin.ModelAdmin, request: HttpRequest, queryset: QuerySet) -> None:
pass
class A(admin.ModelAdmin):
# BaseModelAdmin
autocomplete_fields = ("strs",)
raw_id_fields = ["strs"]
fields = (
"a field",
["a", "list of", "fields"],
)
exclude = ("a", "b")
fieldsets = [
(None, {"fields": ["a", "b"]}),
("group", {"fields": ("c",), "classes": ("a",), "description": "foo"}),
]
form = Form
filter_vertical = ("fields",)
filter_horizontal = ("plenty", "of", "fields")
radio_fields = {
"some_field": admin.VERTICAL,
"another_field": admin.HORIZONTAL,
}
prepopulated_fields = {"slug": ("title",)}
formfield_overrides = {models.TextField: {"widget": Textarea}}
readonly_fields = ("date_modified",)
ordering = ("-pk", "date_modified")
sortable_by = ["pk"]
view_on_site = True
show_full_result_count = False
# ModelAdmin
list_display = ("pk",)
list_display_links = ("str",)
list_filter = ("str", admin.SimpleListFilter, ("str", admin.SimpleListFilter))
list_select_related = True
list_per_page = 1
list_max_show_all = 2
list_editable = ("a", "b")
search_fields = ("c", "d")
date_hirearchy = "f"
save_as = False
save_as_continue = True
save_on_top = False
paginator = Paginator
presserve_filters = False
inlines = (admin.TabularInline, admin.StackedInline)
add_form_template = "template"
change_form_template = "template"
change_list_template = "template"
delete_confirmation_template = "template"
delete_selected_confirmation_template = "template"
object_history_template = "template"
popup_response_template = "template"
actions = (an_action,)
actions_on_top = True
actions_on_bottom = False
actions_selection_counter = True
admin_site = AdminSite()
# This test is here to make sure we're not running into a mypy issue which is
# worked around using a somewhat complicated _ListOrTuple union type. Once the
# issue is solved upstream this test should pass even with the workaround
# replaced by a simpler Sequence type.
# https://github.com/python/mypy/issues/8921
- case: test_fieldset_workaround_regression
main: |
from django.contrib import admin
class A(admin.ModelAdmin):
fieldsets = (
(None, {
'fields': ('name',),
}),
)
- case: errors_on_omitting_fields_from_fieldset_opts
main: |
from django.contrib import admin
class A(admin.ModelAdmin):
fieldsets = [ # type: ignore
(None, {}), # E: Key 'fields' missing for TypedDict "_FieldOpts"
]
- case: errors_on_invalid_radio_fields
main: |
from django.contrib import admin
class A(admin.ModelAdmin):
radio_fields = {"some_field": 0} # E: Dict entry 0 has incompatible type "str": "Literal[0]"; expected "str": "Union[Literal[1], Literal[2]]"
class B(admin.ModelAdmin):
radio_fields = {1: admin.VERTICAL} # E: Dict entry 0 has incompatible type "int": "Literal[2]"; expected "str": "Union[Literal[1], Literal[2]]"
- case: errors_for_invalid_formfield_overrides
main: |
from django.contrib import admin
from django.forms import Textarea
class A(admin.ModelAdmin):
formfield_overrides = {
"not a field": { # E: Dict entry 0 has incompatible type "str": "Dict[str, Any]"; expected "Type[Field[Any, Any]]": "Mapping[str, Any]"
"widget": Textarea
}
}
- case: errors_for_invalid_action_signature
main: |
from django.contrib import admin
from django.http.request import HttpRequest
from django.db.models.query import QuerySet
def an_action(modeladmin: None) -> None:
pass
class A(admin.ModelAdmin):
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Callable[[ModelAdmin, HttpRequest, QuerySet[Any]], None]"