Skip to content

Commit

Permalink
Allow any function for admin's display and action decorators (#2210)
Browse files Browse the repository at this point in the history
I realised that:
- The `display` decorator isn't limited to any specific type of
  callable, it accepts anything really.
- The `action` decorator was trying to dictate a callable type that the
  `ModelAdmin.actions` expects. But that's the responsibility of
  `ModelAdmin`.
  • Loading branch information
flaeppe authored Jun 6, 2024
1 parent 5632dd3 commit 2d05cfc
Show file tree
Hide file tree
Showing 4 changed files with 20 additions and 42 deletions.
28 changes: 8 additions & 20 deletions django-stubs/contrib/admin/decorators.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,41 @@ from typing import Any, TypeVar, overload

from django.contrib.admin import ModelAdmin
from django.contrib.admin.sites import AdminSite
from django.db.models import QuerySet
from django.db.models.base import Model
from django.db.models.expressions import BaseExpression, Combinable
from django.http import HttpRequest, HttpResponseBase
from django.utils.functional import _StrOrPromise
from typing_extensions import TypeAlias

_Model = TypeVar("_Model", bound=Model)
_ModelAdmin = TypeVar("_ModelAdmin", bound=ModelAdmin)
_Request = TypeVar("_Request", bound=HttpRequest)
_QuerySet = TypeVar("_QuerySet", bound=QuerySet)
# This is deliberately different from _DisplayT defined in contrib.admin.options
_DisplayCallable: TypeAlias = Callable[[_ModelAdmin, _Model], Any] | Callable[[_Model], Any]
_DisplayCallableT = TypeVar("_DisplayCallableT", bound=_DisplayCallable)
_ActionReturn = TypeVar("_ActionReturn", bound=HttpResponseBase | None)
_ModelAdmin = TypeVar("_ModelAdmin", bound=ModelAdmin[Any])
_F = TypeVar("_F", bound=Callable[..., Any])

@overload
def action(
function: Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn],
function: _F,
permissions: Sequence[str] | None = ...,
description: _StrOrPromise | None = ...,
) -> Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn]: ...
) -> _F: ...
@overload
def action(
*,
permissions: Sequence[str] | None = ...,
description: _StrOrPromise | None = ...,
) -> Callable[
[Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn]],
Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn],
]: ...
) -> Callable[[_F], _F]: ...
@overload
def display(
function: _DisplayCallableT,
function: _F,
boolean: bool | None = ...,
ordering: str | Combinable | BaseExpression | None = ...,
description: _StrOrPromise | None = ...,
empty_value: str | None = ...,
) -> _DisplayCallableT: ...
) -> _F: ...
@overload
def display(
*,
boolean: bool | None = ...,
ordering: str | Combinable | BaseExpression | None = ...,
description: _StrOrPromise | None = ...,
empty_value: str | None = ...,
) -> Callable[[_DisplayCallableT], _DisplayCallableT]: ...
) -> Callable[[_F], _F]: ...
def register(
*models: type[Model], site: AdminSite | None = ...
) -> Callable[[type[_ModelAdmin]], type[_ModelAdmin]]: ...
6 changes: 3 additions & 3 deletions django-stubs/contrib/admin/options.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ from django.urls.resolvers import URLPattern
from django.utils.datastructures import _ListOrTuple
from django.utils.functional import _StrOrPromise
from django.utils.safestring import SafeString
from typing_extensions import TypeAlias, TypedDict
from typing_extensions import Self, TypeAlias, TypedDict

IS_POPUP_VAR: str
TO_FIELD_VAR: str
Expand Down Expand Up @@ -139,7 +139,7 @@ class BaseModelAdmin(Generic[_ModelT]):
@property
def view_on_site(self) -> Callable[[_ModelT], str] | bool: ...

_ModelAdmin = TypeVar("_ModelAdmin", bound=ModelAdmin)
_ModelAdmin = TypeVar("_ModelAdmin", bound=ModelAdmin[Any])
_ActionCallable: TypeAlias = Callable[[_ModelAdmin, HttpRequest, QuerySet[_ModelT]], HttpResponseBase | None]

class ModelAdmin(BaseModelAdmin[_ModelT]):
Expand Down Expand Up @@ -167,7 +167,7 @@ class ModelAdmin(BaseModelAdmin[_ModelT]):
delete_selected_confirmation_template: _TemplateForResponseT | None
object_history_template: _TemplateForResponseT | None
popup_response_template: _TemplateForResponseT | None
actions: Sequence[_ActionCallable[Any, _ModelT] | str] | None
actions: Sequence[_ActionCallable[Self, _ModelT] | str] | None
action_form: Any
actions_on_top: bool
actions_on_bottom: bool
Expand Down
20 changes: 4 additions & 16 deletions tests/typecheck/contrib/admin/test_decorators.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,6 @@
@admin.action
def freestanding_action_file_response(modeladmin: "MyModelAdmin", request: HttpRequest, queryset: QuerySet[MyModel]) -> FileResponse: ...
@admin.action # E: Value of type variable "_ModelAdmin" of "action" cannot be "int" [type-var]
def freestanding_action_invalid_bare(modeladmin: int, request: HttpRequest, queryset: QuerySet[MyModel]) -> None: ...
@admin.action(description="Some text here", permissions=["test"]) # E: Value of type variable "_ModelAdmin" of function cannot be "int" [type-var]
def freestanding_action_invalid_fancy(modeladmin: int, request: HttpRequest, queryset: QuerySet[MyModel]) -> None: ...
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin[MyModel]):
actions = [freestanding_action_bare, freestanding_action_fancy, "method_action_bare", "method_action_fancy", freestanding_action_http_response, freestanding_action_file_response]
Expand All @@ -101,14 +95,8 @@
@admin.action(description="Some text here", permissions=["test"])
def method_action_file_response(self, request: HttpRequest, queryset: QuerySet[MyModel]) -> FileResponse: ...
@admin.action # E: Value of type variable "_QuerySet" of "action" cannot be "int" [type-var]
def method_action_invalid_bare(self, request: HttpRequest, queryset: int) -> None: ...
@admin.action(description="Some text here", permissions=["test"]) # E: Value of type variable "_QuerySet" of function cannot be "int" [type-var]
def method_action_invalid_fancy(self, request: HttpRequest, queryset: int) -> None: ...
def method(self) -> None:
reveal_type(self.method_action_bare) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query.QuerySet[main.MyModel, main.MyModel])"
reveal_type(self.method_action_fancy) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query.QuerySet[main.MyModel, main.MyModel])"
reveal_type(self.method_action_http_response) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query.QuerySet[main.MyModel, main.MyModel]) -> django.http.response.HttpResponse"
reveal_type(self.method_action_file_response) # N: Revealed type is "def (django.http.request.HttpRequest, django.db.models.query.QuerySet[main.MyModel, main.MyModel]) -> django.http.response.FileResponse"
reveal_type(self.method_action_bare) # N: Revealed type is "def (request: django.http.request.HttpRequest, queryset: django.db.models.query.QuerySet[main.MyModel, main.MyModel])"
reveal_type(self.method_action_fancy) # N: Revealed type is "def (request: django.http.request.HttpRequest, queryset: django.db.models.query.QuerySet[main.MyModel, main.MyModel])"
reveal_type(self.method_action_http_response) # N: Revealed type is "def (request: django.http.request.HttpRequest, queryset: django.db.models.query.QuerySet[main.MyModel, main.MyModel]) -> django.http.response.HttpResponse"
reveal_type(self.method_action_file_response) # N: Revealed type is "def (request: django.http.request.HttpRequest, queryset: django.db.models.query.QuerySet[main.MyModel, main.MyModel]) -> django.http.response.FileResponse"
8 changes: 5 additions & 3 deletions tests/typecheck/contrib/admin/test_options.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,15 @@
main: |
from django.contrib import admin
from django.http.request import HttpRequest
from django.db.models.query import QuerySet
from django.db import models
class MyModel(models.Model): ...
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 "Union[Callable[[Any, HttpRequest, QuerySet[Any, Any]], Optional[HttpResponseBase]], str]" [list-item]
class A(admin.ModelAdmin[MyModel]):
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[A, HttpRequest, QuerySet[MyModel, MyModel]], Optional[HttpResponseBase]], str]" [list-item]
- case: errors_for_invalid_model_admin_generic
main: |
from django.contrib.admin import ModelAdmin
Expand Down

0 comments on commit 2d05cfc

Please sign in to comment.