From 2d05cfc315c1809f1d92de2f2481f39d9d92e256 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Thu, 6 Jun 2024 22:44:25 +0200 Subject: [PATCH] Allow any function for admin's `display` and `action` decorators (#2210) 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`. --- django-stubs/contrib/admin/decorators.pyi | 28 ++++++------------- django-stubs/contrib/admin/options.pyi | 6 ++-- .../contrib/admin/test_decorators.yml | 20 +++---------- .../typecheck/contrib/admin/test_options.yml | 8 ++++-- 4 files changed, 20 insertions(+), 42 deletions(-) diff --git a/django-stubs/contrib/admin/decorators.pyi b/django-stubs/contrib/admin/decorators.pyi index e7bef0b45..ef15b8eac 100644 --- a/django-stubs/contrib/admin/decorators.pyi +++ b/django-stubs/contrib/admin/decorators.pyi @@ -3,45 +3,33 @@ 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( *, @@ -49,7 +37,7 @@ def display( 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]]: ... diff --git a/django-stubs/contrib/admin/options.pyi b/django-stubs/contrib/admin/options.pyi index 096d840d9..f3b5b9448 100644 --- a/django-stubs/contrib/admin/options.pyi +++ b/django-stubs/contrib/admin/options.pyi @@ -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 @@ -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]): @@ -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 diff --git a/tests/typecheck/contrib/admin/test_decorators.yml b/tests/typecheck/contrib/admin/test_decorators.yml index f3543eae7..e43f4a993 100644 --- a/tests/typecheck/contrib/admin/test_decorators.yml +++ b/tests/typecheck/contrib/admin/test_decorators.yml @@ -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] @@ -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" diff --git a/tests/typecheck/contrib/admin/test_options.yml b/tests/typecheck/contrib/admin/test_options.yml index af206da6b..a0cd2b3bf 100644 --- a/tests/typecheck/contrib/admin/test_options.yml +++ b/tests/typecheck/contrib/admin/test_options.yml @@ -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