-
-
Notifications
You must be signed in to change notification settings - Fork 460
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve typing of view decorators #2164
base: master
Are you sure you want to change the base?
Changes from all commits
3d7a572
a7827f9
40e0a36
a65d50d
7b47826
74e07c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: ... | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like defining a Protocol for I'm open to just hinting as |
||
|
||
class _AsyncView(Protocol): | ||
async def __call__(self, request: HttpRequest, /, *args: Any, **kwargs: Any) -> HttpResponseBase: ... | ||
|
||
_ViewFuncT = TypeVar("_ViewFuncT", bound=_View | _AsyncView) # noqa: PYI018 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: ... |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went for the assert types. Unfortunately I can't do: assert_type(good_view, Callable[[HttpRequest], HttpResponse]) As Considering the decorators are defined with the type vars in a straightforward way I don't think it is that much of an issue to not be able to test this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I say that we should prefer the type over the name of an argument and thus add an You should be able to use @csrf_protect
-def good_view(request: HttpRequest) -> HttpResponse:
+def good_view(request: HttpRequest, /) -> HttpResponse:
return HttpResponse()
+assert_type(good_view, Callable[[HttpRequest], HttpResponse]) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: | ||
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ignoring
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to look into what could be done about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Problem is probably the forced positional of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed. Removing the We can fix this by duplicating both View protocols for both positional/non-positional cases. I think that would be fine. Unless there are better solutions? class _View(Protocol):
def __call__(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase: ...
class _ViewPositionalRequest(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: ...
class _AsyncViewPositionalRequest(Protocol):
async def __call__(self, request: HttpRequest, /, *args: Any, **kwargs: Any) -> HttpResponseBase: ...
_ViewFuncT = TypeVar(
"_ViewFuncT", bound=_View | _ViewPositionalRequest | _AsyncView | _AsyncViewPositionalRequest
) # noqa: PYI018
This comment was marked as outdated.
Sorry, something went wrong. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know what is best to do here really.. Django runs all this with a first, "forced" positional, An additional thing to keep in mind with the protocols here is that if the i.e. trying something like below will yield an error: @csrf_protect
def someview(req: HttpRequest) -> HttpResponse: ... An additional approach here could be to dig deeper into what attributes the decorator expects. For instance, I think, I'm not sure how much or if that can help, I think we still would have to decide on forced positional or not, but just wanted to mention it as an alternate approach to the |
||
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a test for the hack as well: from django.contrib.auth.models import User
class AuthenticatedHttpRequest(HttpRequest):
user: User
@csrf_protect
def view_hack_authenticated_request(request: AuthenticatedHttpRequest) -> HttpResponse:
return HttpResponse() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add
@type_check_only
to all protocols.