mirror of https://github.com/django/django.git
Refs #31949 -- Made @never_cache and @cache_control() decorators to work with async functions.
Thanks Carlton Gibson and Mariusz Felisiak for reviews.
This commit is contained in:
parent
a14ddc8cfc
commit
4dfc6ff8a8
|
@ -1,5 +1,7 @@
|
|||
from functools import wraps
|
||||
|
||||
from asgiref.sync import iscoroutinefunction
|
||||
|
||||
from django.middleware.cache import CacheMiddleware
|
||||
from django.utils.cache import add_never_cache_headers, patch_cache_control
|
||||
from django.utils.decorators import decorator_from_middleware_with_args
|
||||
|
@ -26,22 +28,34 @@ def cache_page(timeout, *, cache=None, key_prefix=None):
|
|||
)
|
||||
|
||||
|
||||
def _check_request(request, decorator_name):
|
||||
# Ensure argument looks like a request.
|
||||
if not hasattr(request, "META"):
|
||||
raise TypeError(
|
||||
f"{decorator_name} didn't receive an HttpRequest. If you are "
|
||||
"decorating a classmethod, be sure to use @method_decorator."
|
||||
)
|
||||
|
||||
|
||||
def cache_control(**kwargs):
|
||||
def _cache_controller(viewfunc):
|
||||
@wraps(viewfunc)
|
||||
def _cache_controlled(request, *args, **kw):
|
||||
# Ensure argument looks like a request.
|
||||
if not hasattr(request, "META"):
|
||||
raise TypeError(
|
||||
"cache_control didn't receive an HttpRequest. If you are "
|
||||
"decorating a classmethod, be sure to use "
|
||||
"@method_decorator."
|
||||
)
|
||||
response = viewfunc(request, *args, **kw)
|
||||
patch_cache_control(response, **kwargs)
|
||||
return response
|
||||
if iscoroutinefunction(viewfunc):
|
||||
|
||||
return _cache_controlled
|
||||
async def _view_wrapper(request, *args, **kw):
|
||||
_check_request(request, "cache_control")
|
||||
response = await viewfunc(request, *args, **kw)
|
||||
patch_cache_control(response, **kwargs)
|
||||
return response
|
||||
|
||||
else:
|
||||
|
||||
def _view_wrapper(request, *args, **kw):
|
||||
_check_request(request, "cache_control")
|
||||
response = viewfunc(request, *args, **kw)
|
||||
patch_cache_control(response, **kwargs)
|
||||
return response
|
||||
|
||||
return wraps(viewfunc)(_view_wrapper)
|
||||
|
||||
return _cache_controller
|
||||
|
||||
|
@ -51,16 +65,20 @@ def never_cache(view_func):
|
|||
Decorator that adds headers to a response so that it will never be cached.
|
||||
"""
|
||||
|
||||
@wraps(view_func)
|
||||
def _wrapper_view_func(request, *args, **kwargs):
|
||||
# Ensure argument looks like a request.
|
||||
if not hasattr(request, "META"):
|
||||
raise TypeError(
|
||||
"never_cache didn't receive an HttpRequest. If you are "
|
||||
"decorating a classmethod, be sure to use @method_decorator."
|
||||
)
|
||||
response = view_func(request, *args, **kwargs)
|
||||
add_never_cache_headers(response)
|
||||
return response
|
||||
if iscoroutinefunction(view_func):
|
||||
|
||||
return _wrapper_view_func
|
||||
async def _view_wrapper(request, *args, **kwargs):
|
||||
_check_request(request, "never_cache")
|
||||
response = await view_func(request, *args, **kwargs)
|
||||
add_never_cache_headers(response)
|
||||
return response
|
||||
|
||||
else:
|
||||
|
||||
def _view_wrapper(request, *args, **kwargs):
|
||||
_check_request(request, "never_cache")
|
||||
response = view_func(request, *args, **kwargs)
|
||||
add_never_cache_headers(response)
|
||||
return response
|
||||
|
||||
return wraps(view_func)(_view_wrapper)
|
||||
|
|
|
@ -214,7 +214,9 @@ CSRF
|
|||
Decorators
|
||||
~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
* The :func:`~django.views.decorators.cache.cache_control` and
|
||||
:func:`~django.views.decorators.cache.never_cache` decorators now support
|
||||
wrapping asynchronous view functions.
|
||||
|
||||
Email
|
||||
~~~~~
|
||||
|
|
|
@ -73,6 +73,31 @@ from an async view, you will trigger Django's
|
|||
:ref:`asynchronous safety protection <async-safety>` to protect your data from
|
||||
corruption.
|
||||
|
||||
Decorators
|
||||
----------
|
||||
|
||||
.. versionadded:: 5.0
|
||||
|
||||
The following decorators can be used with both synchronous and asynchronous
|
||||
view functions:
|
||||
|
||||
* :func:`~django.views.decorators.cache.cache_control`
|
||||
* :func:`~django.views.decorators.cache.never_cache`
|
||||
|
||||
For example::
|
||||
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
|
||||
@never_cache
|
||||
def my_sync_view(request):
|
||||
...
|
||||
|
||||
|
||||
@never_cache
|
||||
async def my_async_view(request):
|
||||
...
|
||||
|
||||
Queries & the ORM
|
||||
-----------------
|
||||
|
||||
|
|
|
@ -117,6 +117,10 @@ client-side caching.
|
|||
:func:`~django.utils.cache.patch_cache_control` for the details of the
|
||||
transformation.
|
||||
|
||||
.. versionchanged:: 5.0
|
||||
|
||||
Support for wrapping asynchronous view functions was added.
|
||||
|
||||
.. function:: never_cache(view_func)
|
||||
|
||||
This decorator adds an ``Expires`` header to the current date/time.
|
||||
|
@ -127,6 +131,10 @@ client-side caching.
|
|||
|
||||
Each header is only added if it isn't already set.
|
||||
|
||||
.. versionchanged:: 5.0
|
||||
|
||||
Support for wrapping asynchronous view functions was added.
|
||||
|
||||
.. module:: django.views.decorators.common
|
||||
|
||||
Common
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from unittest import mock
|
||||
|
||||
from asgiref.sync import iscoroutinefunction
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.test import SimpleTestCase
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -16,6 +18,20 @@ class HttpRequestProxy:
|
|||
|
||||
|
||||
class CacheControlDecoratorTest(SimpleTestCase):
|
||||
def test_wrapped_sync_function_is_not_coroutine_function(self):
|
||||
def sync_view(request):
|
||||
return HttpResponse()
|
||||
|
||||
wrapped_view = cache_control()(sync_view)
|
||||
self.assertIs(iscoroutinefunction(wrapped_view), False)
|
||||
|
||||
def test_wrapped_async_function_is_coroutine_function(self):
|
||||
async def async_view(request):
|
||||
return HttpResponse()
|
||||
|
||||
wrapped_view = cache_control()(async_view)
|
||||
self.assertIs(iscoroutinefunction(wrapped_view), True)
|
||||
|
||||
def test_cache_control_decorator_http_request(self):
|
||||
class MyClass:
|
||||
@cache_control(a="b")
|
||||
|
@ -32,6 +48,22 @@ class CacheControlDecoratorTest(SimpleTestCase):
|
|||
with self.assertRaisesMessage(TypeError, msg):
|
||||
MyClass().a_view(HttpRequestProxy(request))
|
||||
|
||||
async def test_cache_control_decorator_http_request_async_view(self):
|
||||
class MyClass:
|
||||
@cache_control(a="b")
|
||||
async def async_view(self, request):
|
||||
return HttpResponse()
|
||||
|
||||
msg = (
|
||||
"cache_control didn't receive an HttpRequest. If you are decorating a "
|
||||
"classmethod, be sure to use @method_decorator."
|
||||
)
|
||||
request = HttpRequest()
|
||||
with self.assertRaisesMessage(TypeError, msg):
|
||||
await MyClass().async_view(request)
|
||||
with self.assertRaisesMessage(TypeError, msg):
|
||||
await MyClass().async_view(HttpRequestProxy(request))
|
||||
|
||||
def test_cache_control_decorator_http_request_proxy(self):
|
||||
class MyClass:
|
||||
@method_decorator(cache_control(a="b"))
|
||||
|
@ -50,6 +82,14 @@ class CacheControlDecoratorTest(SimpleTestCase):
|
|||
response = a_view(HttpRequest())
|
||||
self.assertEqual(response.get("Cache-Control"), "")
|
||||
|
||||
async def test_cache_control_empty_decorator_async_view(self):
|
||||
@cache_control()
|
||||
async def async_view(request):
|
||||
return HttpResponse()
|
||||
|
||||
response = await async_view(HttpRequest())
|
||||
self.assertEqual(response.get("Cache-Control"), "")
|
||||
|
||||
def test_cache_control_full_decorator(self):
|
||||
@cache_control(max_age=123, private=True, public=True, custom=456)
|
||||
def a_view(request):
|
||||
|
@ -61,6 +101,17 @@ class CacheControlDecoratorTest(SimpleTestCase):
|
|||
set(cache_control_items), {"max-age=123", "private", "public", "custom=456"}
|
||||
)
|
||||
|
||||
async def test_cache_control_full_decorator_async_view(self):
|
||||
@cache_control(max_age=123, private=True, public=True, custom=456)
|
||||
async def async_view(request):
|
||||
return HttpResponse()
|
||||
|
||||
response = await async_view(HttpRequest())
|
||||
cache_control_items = response.get("Cache-Control").split(", ")
|
||||
self.assertEqual(
|
||||
set(cache_control_items), {"max-age=123", "private", "public", "custom=456"}
|
||||
)
|
||||
|
||||
|
||||
class CachePageDecoratorTest(SimpleTestCase):
|
||||
def test_cache_page(self):
|
||||
|
@ -74,6 +125,20 @@ class CachePageDecoratorTest(SimpleTestCase):
|
|||
|
||||
|
||||
class NeverCacheDecoratorTest(SimpleTestCase):
|
||||
def test_wrapped_sync_function_is_not_coroutine_function(self):
|
||||
def sync_view(request):
|
||||
return HttpResponse()
|
||||
|
||||
wrapped_view = never_cache(sync_view)
|
||||
self.assertIs(iscoroutinefunction(wrapped_view), False)
|
||||
|
||||
def test_wrapped_async_function_is_coroutine_function(self):
|
||||
async def async_view(request):
|
||||
return HttpResponse()
|
||||
|
||||
wrapped_view = never_cache(async_view)
|
||||
self.assertIs(iscoroutinefunction(wrapped_view), True)
|
||||
|
||||
@mock.patch("time.time")
|
||||
def test_never_cache_decorator_headers(self, mocked_time):
|
||||
@never_cache
|
||||
|
@ -91,6 +156,20 @@ class NeverCacheDecoratorTest(SimpleTestCase):
|
|||
"max-age=0, no-cache, no-store, must-revalidate, private",
|
||||
)
|
||||
|
||||
@mock.patch("time.time")
|
||||
async def test_never_cache_decorator_headers_async_view(self, mocked_time):
|
||||
@never_cache
|
||||
async def async_view(request):
|
||||
return HttpResponse()
|
||||
|
||||
mocked_time.return_value = 1167616461.0
|
||||
response = await async_view(HttpRequest())
|
||||
self.assertEqual(response.headers["Expires"], "Mon, 01 Jan 2007 01:54:21 GMT")
|
||||
self.assertEqual(
|
||||
response.headers["Cache-Control"],
|
||||
"max-age=0, no-cache, no-store, must-revalidate, private",
|
||||
)
|
||||
|
||||
def test_never_cache_decorator_expires_not_overridden(self):
|
||||
@never_cache
|
||||
def a_view(request):
|
||||
|
@ -99,6 +178,14 @@ class NeverCacheDecoratorTest(SimpleTestCase):
|
|||
response = a_view(HttpRequest())
|
||||
self.assertEqual(response.headers["Expires"], "tomorrow")
|
||||
|
||||
async def test_never_cache_decorator_expires_not_overridden_async_view(self):
|
||||
@never_cache
|
||||
async def async_view(request):
|
||||
return HttpResponse(headers={"Expires": "tomorrow"})
|
||||
|
||||
response = await async_view(HttpRequest())
|
||||
self.assertEqual(response.headers["Expires"], "tomorrow")
|
||||
|
||||
def test_never_cache_decorator_http_request(self):
|
||||
class MyClass:
|
||||
@never_cache
|
||||
|
@ -115,6 +202,22 @@ class NeverCacheDecoratorTest(SimpleTestCase):
|
|||
with self.assertRaisesMessage(TypeError, msg):
|
||||
MyClass().a_view(HttpRequestProxy(request))
|
||||
|
||||
async def test_never_cache_decorator_http_request_async_view(self):
|
||||
class MyClass:
|
||||
@never_cache
|
||||
async def async_view(self, request):
|
||||
return HttpResponse()
|
||||
|
||||
request = HttpRequest()
|
||||
msg = (
|
||||
"never_cache didn't receive an HttpRequest. If you are decorating "
|
||||
"a classmethod, be sure to use @method_decorator."
|
||||
)
|
||||
with self.assertRaisesMessage(TypeError, msg):
|
||||
await MyClass().async_view(request)
|
||||
with self.assertRaisesMessage(TypeError, msg):
|
||||
await MyClass().async_view(HttpRequestProxy(request))
|
||||
|
||||
def test_never_cache_decorator_http_request_proxy(self):
|
||||
class MyClass:
|
||||
@method_decorator(never_cache)
|
||||
|
|
Loading…
Reference in New Issue