mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-07 04:34:29 +08:00
Increase accuracy of ModelAdmin types (#375)
* Increase accuracy of ModelAdmin types * Add comment for regression test
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
from collections import OrderedDict
|
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.filters import ListFilter
|
||||||
from django.contrib.admin.models import LogEntry
|
from django.contrib.admin.models import LogEntry
|
||||||
@@ -26,8 +30,10 @@ from django.db.models.fields import Field
|
|||||||
|
|
||||||
IS_POPUP_VAR: str
|
IS_POPUP_VAR: str
|
||||||
TO_FIELD_VAR: str
|
TO_FIELD_VAR: str
|
||||||
HORIZONTAL: Any
|
HORIZONTAL: Literal[1] = ...
|
||||||
VERTICAL: Any
|
VERTICAL: Literal[2] = ...
|
||||||
|
|
||||||
|
_Direction = Union[Literal[1], Literal[2]]
|
||||||
|
|
||||||
def get_content_type_for_model(obj: Union[Type[Model], Model]) -> ContentType: ...
|
def get_content_type_for_model(obj: Union[Type[Model], Model]) -> ContentType: ...
|
||||||
def get_ul_class(radio_style: int) -> str: ...
|
def get_ul_class(radio_style: int) -> str: ...
|
||||||
@@ -37,21 +43,35 @@ class IncorrectLookupParameters(Exception): ...
|
|||||||
FORMFIELD_FOR_DBFIELD_DEFAULTS: Any
|
FORMFIELD_FOR_DBFIELD_DEFAULTS: Any
|
||||||
csrf_protect_m: 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:
|
class BaseModelAdmin:
|
||||||
autocomplete_fields: Any = ...
|
autocomplete_fields: Sequence[str] = ...
|
||||||
raw_id_fields: Any = ...
|
raw_id_fields: Sequence[str] = ...
|
||||||
fields: Any = ...
|
fields: Sequence[Union[str, Sequence[str]]] = ...
|
||||||
exclude: Any = ...
|
exclude: Sequence[str] = ...
|
||||||
fieldsets: Any = ...
|
fieldsets: _FieldsetSpec = ...
|
||||||
form: Any = ...
|
form: Type[BaseForm] = ...
|
||||||
filter_vertical: Any = ...
|
filter_vertical: Sequence[str] = ...
|
||||||
filter_horizontal: Any = ...
|
filter_horizontal: Sequence[str] = ...
|
||||||
radio_fields: Any = ...
|
radio_fields: Mapping[str, _Direction] = ...
|
||||||
prepopulated_fields: Any = ...
|
prepopulated_fields: Mapping[str, Sequence[str]] = ...
|
||||||
formfield_overrides: Any = ...
|
formfield_overrides: Mapping[Type[Field], Mapping[str, Any]] = ...
|
||||||
readonly_fields: Any = ...
|
readonly_fields: Sequence[Union[str, Callable[[Model], Any]]] = ...
|
||||||
ordering: Any = ...
|
ordering: Sequence[str] = ...
|
||||||
sortable_by: Any = ...
|
sortable_by: Sequence[str] = ...
|
||||||
view_on_site: bool = ...
|
view_on_site: bool = ...
|
||||||
show_full_result_count: bool = ...
|
show_full_result_count: bool = ...
|
||||||
checks_class: Any = ...
|
checks_class: Any = ...
|
||||||
@@ -93,7 +113,7 @@ class BaseModelAdmin:
|
|||||||
def has_module_permission(self, request: HttpRequest) -> bool: ...
|
def has_module_permission(self, request: HttpRequest) -> bool: ...
|
||||||
|
|
||||||
class ModelAdmin(BaseModelAdmin):
|
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_display_links: Optional[Sequence[Union[str, Callable]]] = ...
|
||||||
list_filter: Sequence[Union[str, Type[ListFilter], Tuple[str, Type[ListFilter]]]] = ...
|
list_filter: Sequence[Union[str, Type[ListFilter], Tuple[str, Type[ListFilter]]]] = ...
|
||||||
list_select_related: Union[bool, Sequence[str]] = ...
|
list_select_related: Union[bool, Sequence[str]] = ...
|
||||||
@@ -101,21 +121,21 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
list_max_show_all: int = ...
|
list_max_show_all: int = ...
|
||||||
list_editable: Sequence[str] = ...
|
list_editable: Sequence[str] = ...
|
||||||
search_fields: Sequence[str] = ...
|
search_fields: Sequence[str] = ...
|
||||||
date_hierarchy: Optional[Any] = ...
|
date_hierarchy: Optional[str] = ...
|
||||||
save_as: bool = ...
|
save_as: bool = ...
|
||||||
save_as_continue: bool = ...
|
save_as_continue: bool = ...
|
||||||
save_on_top: bool = ...
|
save_on_top: bool = ...
|
||||||
paginator: Any = ...
|
paginator: Type = ...
|
||||||
preserve_filters: bool = ...
|
preserve_filters: bool = ...
|
||||||
inlines: Sequence[Type[InlineModelAdmin]] = ...
|
inlines: Sequence[Type[InlineModelAdmin]] = ...
|
||||||
add_form_template: Any = ...
|
add_form_template: str = ...
|
||||||
change_form_template: Any = ...
|
change_form_template: str = ...
|
||||||
change_list_template: Any = ...
|
change_list_template: str = ...
|
||||||
delete_confirmation_template: Any = ...
|
delete_confirmation_template: str = ...
|
||||||
delete_selected_confirmation_template: Any = ...
|
delete_selected_confirmation_template: str = ...
|
||||||
object_history_template: Any = ...
|
object_history_template: str = ...
|
||||||
popup_response_template: Any = ...
|
popup_response_template: str = ...
|
||||||
actions: Any = ...
|
actions: Sequence[Callable[[ModelAdmin, HttpRequest, QuerySet], None]] = ...
|
||||||
action_form: Any = ...
|
action_form: Any = ...
|
||||||
actions_on_top: bool = ...
|
actions_on_top: bool = ...
|
||||||
actions_on_bottom: 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: ...
|
def history_view(self, request: HttpRequest, object_id: str, extra_context: None = ...) -> HttpResponse: ...
|
||||||
|
|
||||||
class InlineModelAdmin(BaseModelAdmin):
|
class InlineModelAdmin(BaseModelAdmin):
|
||||||
model: Any = ...
|
model: Type[Model] = ...
|
||||||
fk_name: Any = ...
|
fk_name: str = ...
|
||||||
formset: Any = ...
|
formset: BaseFormSet = ...
|
||||||
extra: int = ...
|
extra: int = ...
|
||||||
min_num: Optional[int] = ...
|
min_num: Optional[int] = ...
|
||||||
max_num: Optional[int] = ...
|
max_num: Optional[int] = ...
|
||||||
@@ -238,8 +258,8 @@ class InlineModelAdmin(BaseModelAdmin):
|
|||||||
verbose_name_plural: Optional[str] = ...
|
verbose_name_plural: Optional[str] = ...
|
||||||
can_delete: bool = ...
|
can_delete: bool = ...
|
||||||
show_change_link: bool = ...
|
show_change_link: bool = ...
|
||||||
classes: Any = ...
|
classes: Optional[Sequence[str]] = ...
|
||||||
admin_site: Any = ...
|
admin_site: AdminSite = ...
|
||||||
parent_model: Any = ...
|
parent_model: Any = ...
|
||||||
opts: Any = ...
|
opts: Any = ...
|
||||||
has_registered_model: Any = ...
|
has_registered_model: Any = ...
|
||||||
|
|||||||
126
test-data/typecheck/contrib/admin/test_options.yml
Normal file
126
test-data/typecheck/contrib/admin/test_options.yml
Normal 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]"
|
||||||
Reference in New Issue
Block a user