diff --git a/django-stubs/views/decorators/__init__.pyi b/django-stubs/views/decorators/__init__.pyi index e69de29bb..52ca1a727 100644 --- a/django-stubs/views/decorators/__init__.pyi +++ b/django-stubs/views/decorators/__init__.pyi @@ -0,0 +1,13 @@ +from typing import Any, Protocol, TypeVar + +from django.http.request import HttpRequest +from django.http.response import HttpResponseBase + +# `*args: Any, **kwargs: Any` means any extra argument(s) can be provided, or none. +class _View(Protocol): + def __call__(self, request: HttpRequest, /, *args: Any, **kwargs: Any) -> HttpResponseBase: ... + +class _AsyncView(Protocol): + async def __call__(self, request: HttpRequest, /, *args: Any, **kwargs: Any) -> HttpResponseBase: ... + +_ViewFuncT = TypeVar("_ViewFuncT", bound=_View | _AsyncView) # noqa: PYI018 diff --git a/django-stubs/views/decorators/cache.pyi b/django-stubs/views/decorators/cache.pyi index cb6e5a087..e9b2298d4 100644 --- a/django-stubs/views/decorators/cache.pyi +++ b/django-stubs/views/decorators/cache.pyi @@ -1,10 +1,10 @@ from collections.abc import Callable -from typing import Any, TypeVar +from typing import Any -_F = TypeVar("_F", bound=Callable[..., Any]) +from . import _ViewFuncT def cache_page( - timeout: float | None, *, cache: Any | None = ..., key_prefix: Any | None = ... -) -> Callable[[_F], _F]: ... -def cache_control(**kwargs: Any) -> Callable[[_F], _F]: ... -def never_cache(view_func: _F) -> _F: ... + timeout: float | None, *, cache: Any | None = ..., key_prefix: str | None = ... +) -> Callable[[_ViewFuncT], _ViewFuncT]: ... +def cache_control(**kwargs: Any) -> Callable[[_ViewFuncT], _ViewFuncT]: ... +def never_cache(view_func: _ViewFuncT, /) -> _ViewFuncT: ... diff --git a/django-stubs/views/decorators/clickjacking.pyi b/django-stubs/views/decorators/clickjacking.pyi index f0ba61c7e..b8ea31391 100644 --- a/django-stubs/views/decorators/clickjacking.pyi +++ b/django-stubs/views/decorators/clickjacking.pyi @@ -1,8 +1,5 @@ -from collections.abc import Callable -from typing import Any, TypeVar +from . import _ViewFuncT -_F = TypeVar("_F", bound=Callable[..., Any]) - -def xframe_options_deny(view_func: _F) -> _F: ... -def xframe_options_sameorigin(view_func: _F) -> _F: ... -def xframe_options_exempt(view_func: _F) -> _F: ... +def xframe_options_deny(view_func: _ViewFuncT, /) -> _ViewFuncT: ... +def xframe_options_sameorigin(view_func: _ViewFuncT, /) -> _ViewFuncT: ... +def xframe_options_exempt(view_func: _ViewFuncT, /) -> _ViewFuncT: ... diff --git a/django-stubs/views/decorators/common.pyi b/django-stubs/views/decorators/common.pyi index a129e01e7..c917c0292 100644 --- a/django-stubs/views/decorators/common.pyi +++ b/django-stubs/views/decorators/common.pyi @@ -1,6 +1,3 @@ -from collections.abc import Callable -from typing import Any, TypeVar +from . import _ViewFuncT -_C = TypeVar("_C", bound=Callable[..., Any]) - -def no_append_slash(view_func: _C) -> _C: ... +def no_append_slash(view_func: _ViewFuncT, /) -> _ViewFuncT: ... diff --git a/django-stubs/views/decorators/csrf.pyi b/django-stubs/views/decorators/csrf.pyi index a9e8b2bca..c3b9e368e 100644 --- a/django-stubs/views/decorators/csrf.pyi +++ b/django-stubs/views/decorators/csrf.pyi @@ -1,18 +1,14 @@ -from collections.abc import Callable -from typing import Any, TypeVar - from django.middleware.csrf import CsrfViewMiddleware -csrf_protect: Callable[[_F], _F] +from . import _ViewFuncT + +def csrf_protect(view_func: _ViewFuncT, /) -> _ViewFuncT: ... class _EnsureCsrfToken(CsrfViewMiddleware): ... -requires_csrf_token: Callable[[_F], _F] +def requires_csrf_token(view_func: _ViewFuncT, /) -> _ViewFuncT: ... class _EnsureCsrfCookie(CsrfViewMiddleware): ... -ensure_csrf_cookie: Callable[[_F], _F] - -_F = TypeVar("_F", bound=Callable[..., Any]) - -def csrf_exempt(view_func: _F) -> _F: ... +def ensure_csrf_cookie(view_func: _ViewFuncT, /) -> _ViewFuncT: ... +def csrf_exempt(view_func: _ViewFuncT, /) -> _ViewFuncT: ... diff --git a/django-stubs/views/decorators/gzip.pyi b/django-stubs/views/decorators/gzip.pyi index 9a9de9880..c7f939533 100644 --- a/django-stubs/views/decorators/gzip.pyi +++ b/django-stubs/views/decorators/gzip.pyi @@ -1,6 +1,3 @@ -from collections.abc import Callable -from typing import Any, TypeVar +from . import _ViewFuncT -_C = TypeVar("_C", bound=Callable[..., Any]) - -gzip_page: Callable[[_C], _C] +def gzip_page(view_func: _ViewFuncT, /) -> _ViewFuncT: ... diff --git a/django-stubs/views/decorators/http.pyi b/django-stubs/views/decorators/http.pyi index 9147bec1f..430962719 100644 --- a/django-stubs/views/decorators/http.pyi +++ b/django-stubs/views/decorators/http.pyi @@ -1,19 +1,15 @@ from collections.abc import Callable, Container from datetime import datetime -from typing import Any, TypeVar -_F = TypeVar("_F", bound=Callable[..., Any]) - -conditional_page: Callable[[_F], _F] - -def require_http_methods(request_method_list: Container[str]) -> Callable[[_F], _F]: ... - -require_GET: Callable[[_F], _F] -require_POST: Callable[[_F], _F] -require_safe: Callable[[_F], _F] +from . import _ViewFuncT +def conditional_page(view_func: _ViewFuncT, /) -> _ViewFuncT: ... +def require_http_methods(request_method_list: Container[str]) -> Callable[[_ViewFuncT], _ViewFuncT]: ... +def require_GET(func: _ViewFuncT, /) -> _ViewFuncT: ... +def require_POST(func: _ViewFuncT, /) -> _ViewFuncT: ... +def require_safe(func: _ViewFuncT, /) -> _ViewFuncT: ... def condition( etag_func: Callable[..., str | None] | None = ..., last_modified_func: Callable[..., datetime | None] | None = ... -) -> Callable[[_F], _F]: ... -def etag(etag_func: Callable[..., str | None]) -> Callable[[_F], _F]: ... -def last_modified(last_modified_func: Callable[..., datetime | None]) -> Callable[[_F], _F]: ... +) -> Callable[[_ViewFuncT], _ViewFuncT]: ... +def etag(etag_func: Callable[..., str | None]) -> Callable[[_ViewFuncT], _ViewFuncT]: ... +def last_modified(last_modified_func: Callable[..., datetime | None]) -> Callable[[_ViewFuncT], _ViewFuncT]: ... diff --git a/django-stubs/views/decorators/vary.pyi b/django-stubs/views/decorators/vary.pyi index 7c8b1d159..634001df7 100644 --- a/django-stubs/views/decorators/vary.pyi +++ b/django-stubs/views/decorators/vary.pyi @@ -1,7 +1,6 @@ from collections.abc import Callable -from typing import Any, TypeVar -_F = TypeVar("_F", bound=Callable[..., Any]) +from . import _ViewFuncT -def vary_on_headers(*headers: str) -> Callable[[_F], _F]: ... -def vary_on_cookie(func: _F) -> _F: ... +def vary_on_headers(*headers: str) -> Callable[[_ViewFuncT], _ViewFuncT]: ... +def vary_on_cookie(func: _ViewFuncT, /) -> _ViewFuncT: ... diff --git a/tests/assert_type/views/decorators/test_csrf.py b/tests/assert_type/views/decorators/test_csrf.py new file mode 100644 index 000000000..242ef5a25 --- /dev/null +++ b/tests/assert_type/views/decorators/test_csrf.py @@ -0,0 +1,46 @@ +from typing import Callable + +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.views.decorators.csrf import csrf_protect +from typing_extensions import assert_type + + +@csrf_protect +def good_view_positional(request: HttpRequest, /) -> HttpResponse: + return HttpResponse() + + +# `assert_type` can only be used when `request` is pos. only. +assert_type(good_view_positional, Callable[[HttpRequest], HttpResponse]) + + +# The decorator works too if `request` is not explicitly pos. only. +@csrf_protect +def good_view(request: HttpRequest) -> HttpResponse: + return HttpResponse() + + +@csrf_protect +async def good_async_view(request: HttpRequest) -> HttpResponse: + return HttpResponse() + + +@csrf_protect +def good_view_with_arguments(request: HttpRequest, other: int, args: str) -> HttpResponse: + return HttpResponse() + + +@csrf_protect +async def good_async_view_with_arguments(request: HttpRequest, other: int, args: str) -> HttpResponse: + return HttpResponse() + + +@csrf_protect # type: ignore[type-var] # pyright: ignore[reportArgumentType, reportUntypedFunctionDecorator] +def bad_view(request: int) -> str: + return "" + + +@csrf_protect # type: ignore[type-var] # pyright: ignore[reportArgumentType, reportUntypedFunctionDecorator] +def bad_view_no_arguments() -> HttpResponse: + return HttpResponse()