From 31ef26eee154c0dacd86ef0a60c2c166bea04adc Mon Sep 17 00:00:00 2001 From: Christian Bundy Date: Sun, 22 Jan 2023 15:55:17 -0800 Subject: [PATCH 01/12] Fix admin action return type --- django-stubs/contrib/admin/decorators.pyi | 9 +++++---- django-stubs/contrib/admin/options.pyi | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/django-stubs/contrib/admin/decorators.pyi b/django-stubs/contrib/admin/decorators.pyi index adcad67ad..e44da0fff 100644 --- a/django-stubs/contrib/admin/decorators.pyi +++ b/django-stubs/contrib/admin/decorators.pyi @@ -1,12 +1,12 @@ from collections.abc import Callable, Sequence -from typing import Any, TypeVar, Union, overload # noqa: Y037 +from typing import Any, Optional, TypeVar, Union, overload # noqa: Y037 from django.contrib.admin import ModelAdmin from django.contrib.admin.sites import AdminSite from django.db.models import Combinable, QuerySet from django.db.models.base import Model from django.db.models.expressions import BaseExpression -from django.http import HttpRequest +from django.http import HttpRequest, HttpResponse from django.utils.functional import _StrOrPromise from typing_extensions import TypeAlias @@ -17,20 +17,21 @@ _QuerySet = TypeVar("_QuerySet", bound=QuerySet) # This is deliberately different from _DisplayT defined in contrib.admin.options _DisplayCallable: TypeAlias = Union[Callable[[_ModelAdmin, _Model], Any], Callable[[_Model], Any]] # noqa: Y037 _DisplayCallableT = TypeVar("_DisplayCallableT", bound=_DisplayCallable) +_ActionReturn = TypeVar("_ActionReturn", bound=Optional[HttpResponse]) @overload def action( function: Callable[[_ModelAdmin, _Request, _QuerySet], None], permissions: Sequence[str] | None = ..., description: _StrOrPromise | None = ..., -) -> Callable[[_ModelAdmin, _Request, _QuerySet], None]: ... +) -> Callable[[_ModelAdmin, _Request, _QuerySet], Optional[HttpResponse]]: ... @overload def action( *, permissions: Sequence[str] | None = ..., description: _StrOrPromise | None = ..., ) -> Callable[ - [Callable[[_ModelAdmin, _Request, _QuerySet], None]], Callable[[_ModelAdmin, _Request, _QuerySet], None] + [Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn]], Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn] ]: ... @overload def display( diff --git a/django-stubs/contrib/admin/options.pyi b/django-stubs/contrib/admin/options.pyi index af42ea939..45a72abc7 100644 --- a/django-stubs/contrib/admin/options.pyi +++ b/django-stubs/contrib/admin/options.pyi @@ -131,7 +131,7 @@ class BaseModelAdmin(Generic[_ModelT]): _DisplayT: TypeAlias = _ListOrTuple[str | Callable[[_ModelT], str | bool]] _ModelAdmin = TypeVar("_ModelAdmin", bound=ModelAdmin) -_ActionCallable: TypeAlias = Callable[[_ModelAdmin, HttpRequest, QuerySet[_ModelT]], None] +_ActionCallable: TypeAlias = Callable[[_ModelAdmin, HttpRequest, QuerySet[_ModelT]], Optional[HttpResponse]] class ModelAdmin(BaseModelAdmin[_ModelT]): list_display: _DisplayT From c089372432405f5b22bf528f016ea930d2668a2f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 22 Jan 2023 23:58:31 +0000 Subject: [PATCH 02/12] [pre-commit.ci] auto fixes from pre-commit.com hooks --- django-stubs/contrib/admin/decorators.pyi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django-stubs/contrib/admin/decorators.pyi b/django-stubs/contrib/admin/decorators.pyi index e44da0fff..3d256d503 100644 --- a/django-stubs/contrib/admin/decorators.pyi +++ b/django-stubs/contrib/admin/decorators.pyi @@ -31,7 +31,8 @@ def action( permissions: Sequence[str] | None = ..., description: _StrOrPromise | None = ..., ) -> Callable[ - [Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn]], Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn] + [Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn]], + Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn], ]: ... @overload def display( From 3fe1a91fd92839dbdf938958181c8509f44e892b Mon Sep 17 00:00:00 2001 From: Christian Bundy Date: Sun, 22 Jan 2023 16:14:27 -0800 Subject: [PATCH 03/12] Use HttpResponseBase to support StreamingHttpResponse --- django-stubs/contrib/admin/decorators.pyi | 2 +- django-stubs/contrib/admin/options.pyi | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/django-stubs/contrib/admin/decorators.pyi b/django-stubs/contrib/admin/decorators.pyi index 3d256d503..bb11d9108 100644 --- a/django-stubs/contrib/admin/decorators.pyi +++ b/django-stubs/contrib/admin/decorators.pyi @@ -6,7 +6,7 @@ from django.contrib.admin.sites import AdminSite from django.db.models import Combinable, QuerySet from django.db.models.base import Model from django.db.models.expressions import BaseExpression -from django.http import HttpRequest, HttpResponse +from django.http import HttpRequest, HttpResponseBase from django.utils.functional import _StrOrPromise from typing_extensions import TypeAlias diff --git a/django-stubs/contrib/admin/options.pyi b/django-stubs/contrib/admin/options.pyi index 45a72abc7..ebf2d8bb7 100644 --- a/django-stubs/contrib/admin/options.pyi +++ b/django-stubs/contrib/admin/options.pyi @@ -27,7 +27,7 @@ from django.forms.models import ( ) from django.forms.widgets import Media from django.http.request import HttpRequest -from django.http.response import HttpResponse, HttpResponseRedirect +from django.http.response import HttpResponse, HttpResponseRedirect, HttpResponseBase from django.template.response import _TemplateForResponseT from django.urls.resolvers import URLPattern from django.utils.datastructures import _ListOrTuple @@ -131,7 +131,7 @@ class BaseModelAdmin(Generic[_ModelT]): _DisplayT: TypeAlias = _ListOrTuple[str | Callable[[_ModelT], str | bool]] _ModelAdmin = TypeVar("_ModelAdmin", bound=ModelAdmin) -_ActionCallable: TypeAlias = Callable[[_ModelAdmin, HttpRequest, QuerySet[_ModelT]], Optional[HttpResponse]] +_ActionCallable: TypeAlias = Callable[[_ModelAdmin, HttpRequest, QuerySet[_ModelT]], Optional[HttpResponseBase]] class ModelAdmin(BaseModelAdmin[_ModelT]): list_display: _DisplayT From 5bc0daafd839de18ad29e161b20b1088d8a65158 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Jan 2023 00:14:54 +0000 Subject: [PATCH 04/12] [pre-commit.ci] auto fixes from pre-commit.com hooks --- django-stubs/contrib/admin/options.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django-stubs/contrib/admin/options.pyi b/django-stubs/contrib/admin/options.pyi index ebf2d8bb7..9581fff66 100644 --- a/django-stubs/contrib/admin/options.pyi +++ b/django-stubs/contrib/admin/options.pyi @@ -27,7 +27,7 @@ from django.forms.models import ( ) from django.forms.widgets import Media from django.http.request import HttpRequest -from django.http.response import HttpResponse, HttpResponseRedirect, HttpResponseBase +from django.http.response import HttpResponse, HttpResponseBase, HttpResponseRedirect from django.template.response import _TemplateForResponseT from django.urls.resolvers import URLPattern from django.utils.datastructures import _ListOrTuple From a71a4249494943f7bfdc71b43340ab7623d1fc34 Mon Sep 17 00:00:00 2001 From: Christian Bundy Date: Sun, 22 Jan 2023 21:13:38 -0800 Subject: [PATCH 05/12] Fix usage --- django-stubs/contrib/admin/decorators.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django-stubs/contrib/admin/decorators.pyi b/django-stubs/contrib/admin/decorators.pyi index bb11d9108..e98e122f9 100644 --- a/django-stubs/contrib/admin/decorators.pyi +++ b/django-stubs/contrib/admin/decorators.pyi @@ -17,14 +17,14 @@ _QuerySet = TypeVar("_QuerySet", bound=QuerySet) # This is deliberately different from _DisplayT defined in contrib.admin.options _DisplayCallable: TypeAlias = Union[Callable[[_ModelAdmin, _Model], Any], Callable[[_Model], Any]] # noqa: Y037 _DisplayCallableT = TypeVar("_DisplayCallableT", bound=_DisplayCallable) -_ActionReturn = TypeVar("_ActionReturn", bound=Optional[HttpResponse]) +_ActionReturn = TypeVar("_ActionReturn", bound=Optional[HttpResponseBase]) @overload def action( function: Callable[[_ModelAdmin, _Request, _QuerySet], None], permissions: Sequence[str] | None = ..., description: _StrOrPromise | None = ..., -) -> Callable[[_ModelAdmin, _Request, _QuerySet], Optional[HttpResponse]]: ... +) -> Callable[[_ModelAdmin, _Request, _QuerySet], Optional[HttpResponseBase]]: ... @overload def action( *, From aa9dea3310f40c73c89da5bf39a786500b0aabfd Mon Sep 17 00:00:00 2001 From: Christian Bundy Date: Mon, 23 Jan 2023 08:46:06 -0800 Subject: [PATCH 06/12] Fix issues --- django-stubs/contrib/admin/decorators.pyi | 14 ++++++-------- tests/typecheck/contrib/admin/test_decorators.yml | 15 ++++++++++++++- tests/typecheck/contrib/admin/test_options.yml | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/django-stubs/contrib/admin/decorators.pyi b/django-stubs/contrib/admin/decorators.pyi index e98e122f9..2876e34c9 100644 --- a/django-stubs/contrib/admin/decorators.pyi +++ b/django-stubs/contrib/admin/decorators.pyi @@ -1,5 +1,5 @@ from collections.abc import Callable, Sequence -from typing import Any, Optional, TypeVar, Union, overload # noqa: Y037 +from typing import Any, TypeVar, Union, overload # noqa: Y037 from django.contrib.admin import ModelAdmin from django.contrib.admin.sites import AdminSite @@ -17,23 +17,21 @@ _QuerySet = TypeVar("_QuerySet", bound=QuerySet) # This is deliberately different from _DisplayT defined in contrib.admin.options _DisplayCallable: TypeAlias = Union[Callable[[_ModelAdmin, _Model], Any], Callable[[_Model], Any]] # noqa: Y037 _DisplayCallableT = TypeVar("_DisplayCallableT", bound=_DisplayCallable) -_ActionReturn = TypeVar("_ActionReturn", bound=Optional[HttpResponseBase]) +_ActionCallable: TypeAlias = Callable[[_ModelAdmin, _Request, _QuerySet], HttpResponseBase | None] # noqa: Y037 +_ActionCallableT = TypeVar("_ActionCallableT", bound=_ActionCallable) @overload def action( - function: Callable[[_ModelAdmin, _Request, _QuerySet], None], + function: _ActionCallableT, permissions: Sequence[str] | None = ..., description: _StrOrPromise | None = ..., -) -> Callable[[_ModelAdmin, _Request, _QuerySet], Optional[HttpResponseBase]]: ... +) -> _ActionCallableT: ... @overload def action( *, permissions: Sequence[str] | None = ..., description: _StrOrPromise | None = ..., -) -> Callable[ - [Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn]], - Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn], -]: ... +) -> Callable[[_ActionCallableT], _ActionCallableT,]: ... @overload def display( function: _DisplayCallableT, diff --git a/tests/typecheck/contrib/admin/test_decorators.yml b/tests/typecheck/contrib/admin/test_decorators.yml index 9f16d8f5f..28b3e0b67 100644 --- a/tests/typecheck/contrib/admin/test_decorators.yml +++ b/tests/typecheck/contrib/admin/test_decorators.yml @@ -60,7 +60,7 @@ from django.contrib import admin from django.db.models import QuerySet - from django.http import HttpRequest + from django.http import FileResponse, HttpRequest, HttpResponse class MyModel(models.Model): ... @@ -73,6 +73,11 @@ @admin.action(description="Some text here", permissions=["test"]) def freestanding_action_fancy(modeladmin: "MyModelAdmin", request: HttpRequest, queryset: QuerySet[MyModel]) -> None: ... + @admin.action + def freestanding_action_http_response(modeladmin: "MyModelAdmin", request: HttpRequest, queryset: QuerySet[MyModel]) -> HttpResponse: ... + + @admin.action + def freestanding_action_file_response(modeladmin: "MyModelAdmin", request: HttpRequest, queryset: QuerySet[MyModel]) -> FileResponse: ... @admin.register(MyModel) class MyModelAdmin(admin.ModelAdmin[MyModel]): @@ -84,6 +89,14 @@ @admin.action(description="Some text here", permissions=["test"]) def method_action_fancy(self, request: HttpRequest, queryset: QuerySet[MyModel]) -> None: ... + @admin.action(description="Some text here", permissions=["test"]) + def method_action_http_response(self, request: HttpRequest, queryset: QuerySet[MyModel]) -> HttpResponse: ... + + @admin.action(description="Some text here", permissions=["test"]) + def method_action_file_response(self, request: HttpRequest, queryset: QuerySet[MyModel]) -> FileResponse: ... + 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" diff --git a/tests/typecheck/contrib/admin/test_options.yml b/tests/typecheck/contrib/admin/test_options.yml index 4e9bd7e64..18cfeb19c 100644 --- a/tests/typecheck/contrib/admin/test_options.yml +++ b/tests/typecheck/contrib/admin/test_options.yml @@ -140,7 +140,7 @@ 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]], None], str]" + actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[Any, HttpRequest, _QuerySet[Any, Any]], Optional[HttpResponseBase]], str]" - case: errors_for_invalid_model_admin_generic main: | from django.contrib.admin import ModelAdmin From 92da2c19808422f3e7373b9c6f605c6120c043dd Mon Sep 17 00:00:00 2001 From: Christian Bundy Date: Mon, 23 Jan 2023 09:07:15 -0800 Subject: [PATCH 07/12] Fix test --- tests/typecheck/contrib/admin/test_decorators.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/typecheck/contrib/admin/test_decorators.yml b/tests/typecheck/contrib/admin/test_decorators.yml index 28b3e0b67..0a5ad159a 100644 --- a/tests/typecheck/contrib/admin/test_decorators.yml +++ b/tests/typecheck/contrib/admin/test_decorators.yml @@ -96,7 +96,7 @@ def method_action_file_response(self, request: HttpRequest, queryset: QuerySet[MyModel]) -> FileResponse: ... 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" From d004b1b721daf8003f2ad6b9f734698f51b4a302 Mon Sep 17 00:00:00 2001 From: Christian Bundy Date: Mon, 23 Jan 2023 14:49:31 -0800 Subject: [PATCH 08/12] Remove redundant comma Co-authored-by: Marti Raudsepp --- django-stubs/contrib/admin/decorators.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django-stubs/contrib/admin/decorators.pyi b/django-stubs/contrib/admin/decorators.pyi index 2876e34c9..03a15b3d8 100644 --- a/django-stubs/contrib/admin/decorators.pyi +++ b/django-stubs/contrib/admin/decorators.pyi @@ -31,7 +31,7 @@ def action( *, permissions: Sequence[str] | None = ..., description: _StrOrPromise | None = ..., -) -> Callable[[_ActionCallableT], _ActionCallableT,]: ... +) -> Callable[[_ActionCallableT], _ActionCallableT]: ... @overload def display( function: _DisplayCallableT, From 3fa57f03709828e43f576fee94325191976c3e9b Mon Sep 17 00:00:00 2001 From: Christian Bundy Date: Mon, 23 Jan 2023 14:50:00 -0800 Subject: [PATCH 09/12] Use `T | None` rather than `Optional[T]` Co-authored-by: Marti Raudsepp --- django-stubs/contrib/admin/options.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django-stubs/contrib/admin/options.pyi b/django-stubs/contrib/admin/options.pyi index 9581fff66..994485ba5 100644 --- a/django-stubs/contrib/admin/options.pyi +++ b/django-stubs/contrib/admin/options.pyi @@ -131,7 +131,7 @@ class BaseModelAdmin(Generic[_ModelT]): _DisplayT: TypeAlias = _ListOrTuple[str | Callable[[_ModelT], str | bool]] _ModelAdmin = TypeVar("_ModelAdmin", bound=ModelAdmin) -_ActionCallable: TypeAlias = Callable[[_ModelAdmin, HttpRequest, QuerySet[_ModelT]], Optional[HttpResponseBase]] +_ActionCallable: TypeAlias = Callable[[_ModelAdmin, HttpRequest, QuerySet[_ModelT]], HttpResponseBase | None] class ModelAdmin(BaseModelAdmin[_ModelT]): list_display: _DisplayT From 66e29c9c794bd70ed3fb44de90be21d02acce57c Mon Sep 17 00:00:00 2001 From: Christian Bundy Date: Mon, 23 Jan 2023 14:59:55 -0800 Subject: [PATCH 10/12] Add freestanding actions to test --- tests/typecheck/contrib/admin/test_decorators.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/typecheck/contrib/admin/test_decorators.yml b/tests/typecheck/contrib/admin/test_decorators.yml index 0a5ad159a..4e8335c9c 100644 --- a/tests/typecheck/contrib/admin/test_decorators.yml +++ b/tests/typecheck/contrib/admin/test_decorators.yml @@ -81,7 +81,7 @@ @admin.register(MyModel) class MyModelAdmin(admin.ModelAdmin[MyModel]): - actions = [freestanding_action_bare, freestanding_action_fancy, "method_action_bare", "method_action_fancy"] + actions = [freestanding_action_bare, freestanding_action_fancy, "method_action_bare", "method_action_fancy", freestanding_action_http_response, freestanding_action_file_response] @admin.action def method_action_bare(self, request: HttpRequest, queryset: QuerySet[MyModel]) -> None: ... From 0477a2641f70e5712b826385f1d024148ee788e0 Mon Sep 17 00:00:00 2001 From: Christian Bundy Date: Mon, 23 Jan 2023 15:03:53 -0800 Subject: [PATCH 11/12] Add negative test cases --- tests/typecheck/contrib/admin/test_decorators.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/typecheck/contrib/admin/test_decorators.yml b/tests/typecheck/contrib/admin/test_decorators.yml index 4e8335c9c..7f4fb63e2 100644 --- a/tests/typecheck/contrib/admin/test_decorators.yml +++ b/tests/typecheck/contrib/admin/test_decorators.yml @@ -79,6 +79,9 @@ @admin.action def freestanding_action_file_response(modeladmin: "MyModelAdmin", request: HttpRequest, queryset: QuerySet[MyModel]) -> FileResponse: ... + @admin.action # E: Value of type variable "_ActionCallableT" of "action" cannot be "Callable[[MyModelAdmin, HttpRequest, _QuerySet[MyModel, MyModel]], bool]" + def freestanding_action_invalid(modeladmin: "MyModelAdmin", request: HttpRequest, queryset: QuerySet[MyModel]) -> bool: ... + @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] @@ -95,6 +98,9 @@ @admin.action(description="Some text here", permissions=["test"]) def method_action_file_response(self, request: HttpRequest, queryset: QuerySet[MyModel]) -> FileResponse: ... + @admin.action(description="Some text here", permissions=["test"]) # E: Value of type variable "_ActionCallableT" of function cannot be "Callable[[MyModelAdmin, HttpRequest, _QuerySet[MyModel, MyModel]], int]" + def method_action_invalid(self, request: HttpRequest, queryset: QuerySet[MyModel]) -> int: ... + def method(self) -> None: 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])" From ea49719c6671d21ab71885f0c8cc0f0805686103 Mon Sep 17 00:00:00 2001 From: Christian Bundy Date: Mon, 23 Jan 2023 15:37:29 -0800 Subject: [PATCH 12/12] Add unhappier paths and fix problems --- django-stubs/contrib/admin/decorators.pyi | 12 +++++----- .../contrib/admin/test_decorators.yml | 22 ++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/django-stubs/contrib/admin/decorators.pyi b/django-stubs/contrib/admin/decorators.pyi index 03a15b3d8..66664f37f 100644 --- a/django-stubs/contrib/admin/decorators.pyi +++ b/django-stubs/contrib/admin/decorators.pyi @@ -17,21 +17,23 @@ _QuerySet = TypeVar("_QuerySet", bound=QuerySet) # This is deliberately different from _DisplayT defined in contrib.admin.options _DisplayCallable: TypeAlias = Union[Callable[[_ModelAdmin, _Model], Any], Callable[[_Model], Any]] # noqa: Y037 _DisplayCallableT = TypeVar("_DisplayCallableT", bound=_DisplayCallable) -_ActionCallable: TypeAlias = Callable[[_ModelAdmin, _Request, _QuerySet], HttpResponseBase | None] # noqa: Y037 -_ActionCallableT = TypeVar("_ActionCallableT", bound=_ActionCallable) +_ActionReturn = TypeVar("_ActionReturn", bound=HttpResponseBase | None) @overload def action( - function: _ActionCallableT, + function: Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn], permissions: Sequence[str] | None = ..., description: _StrOrPromise | None = ..., -) -> _ActionCallableT: ... +) -> Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn]: ... @overload def action( *, permissions: Sequence[str] | None = ..., description: _StrOrPromise | None = ..., -) -> Callable[[_ActionCallableT], _ActionCallableT]: ... +) -> Callable[ + [Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn]], + Callable[[_ModelAdmin, _Request, _QuerySet], _ActionReturn], +]: ... @overload def display( function: _DisplayCallableT, diff --git a/tests/typecheck/contrib/admin/test_decorators.yml b/tests/typecheck/contrib/admin/test_decorators.yml index 7f4fb63e2..2b21443e0 100644 --- a/tests/typecheck/contrib/admin/test_decorators.yml +++ b/tests/typecheck/contrib/admin/test_decorators.yml @@ -79,8 +79,11 @@ @admin.action def freestanding_action_file_response(modeladmin: "MyModelAdmin", request: HttpRequest, queryset: QuerySet[MyModel]) -> FileResponse: ... - @admin.action # E: Value of type variable "_ActionCallableT" of "action" cannot be "Callable[[MyModelAdmin, HttpRequest, _QuerySet[MyModel, MyModel]], bool]" - def freestanding_action_invalid(modeladmin: "MyModelAdmin", request: HttpRequest, queryset: QuerySet[MyModel]) -> bool: ... + @admin.action # E: Value of type variable "_ModelAdmin" of "action" cannot be "int" + 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" + def freestanding_action_invalid_fancy(modeladmin: int, request: HttpRequest, queryset: QuerySet[MyModel]) -> None: ... @admin.register(MyModel) class MyModelAdmin(admin.ModelAdmin[MyModel]): @@ -98,11 +101,14 @@ @admin.action(description="Some text here", permissions=["test"]) def method_action_file_response(self, request: HttpRequest, queryset: QuerySet[MyModel]) -> FileResponse: ... - @admin.action(description="Some text here", permissions=["test"]) # E: Value of type variable "_ActionCallableT" of function cannot be "Callable[[MyModelAdmin, HttpRequest, _QuerySet[MyModel, MyModel]], int]" - def method_action_invalid(self, request: HttpRequest, queryset: QuerySet[MyModel]) -> int: ... + @admin.action # E: Value of type variable "_QuerySet" of "action" cannot be "int" + 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" + 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 (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" + 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"