mirror of
https://github.com/django/django.git
synced 2025-01-03 06:55:47 +00:00
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 functools import wraps
|
||||||
|
|
||||||
|
from asgiref.sync import iscoroutinefunction
|
||||||
|
|
||||||
from django.middleware.cache import CacheMiddleware
|
from django.middleware.cache import CacheMiddleware
|
||||||
from django.utils.cache import add_never_cache_headers, patch_cache_control
|
from django.utils.cache import add_never_cache_headers, patch_cache_control
|
||||||
from django.utils.decorators import decorator_from_middleware_with_args
|
from django.utils.decorators import decorator_from_middleware_with_args
|
||||||
@ -26,22 +28,34 @@ def cache_page(timeout, *, cache=None, key_prefix=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def cache_control(**kwargs):
|
def _check_request(request, decorator_name):
|
||||||
def _cache_controller(viewfunc):
|
|
||||||
@wraps(viewfunc)
|
|
||||||
def _cache_controlled(request, *args, **kw):
|
|
||||||
# Ensure argument looks like a request.
|
# Ensure argument looks like a request.
|
||||||
if not hasattr(request, "META"):
|
if not hasattr(request, "META"):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"cache_control didn't receive an HttpRequest. If you are "
|
f"{decorator_name} didn't receive an HttpRequest. If you are "
|
||||||
"decorating a classmethod, be sure to use "
|
"decorating a classmethod, be sure to use @method_decorator."
|
||||||
"@method_decorator."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cache_control(**kwargs):
|
||||||
|
def _cache_controller(viewfunc):
|
||||||
|
if iscoroutinefunction(viewfunc):
|
||||||
|
|
||||||
|
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)
|
response = viewfunc(request, *args, **kw)
|
||||||
patch_cache_control(response, **kwargs)
|
patch_cache_control(response, **kwargs)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
return _cache_controlled
|
return wraps(viewfunc)(_view_wrapper)
|
||||||
|
|
||||||
return _cache_controller
|
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.
|
Decorator that adds headers to a response so that it will never be cached.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(view_func)
|
if iscoroutinefunction(view_func):
|
||||||
def _wrapper_view_func(request, *args, **kwargs):
|
|
||||||
# Ensure argument looks like a request.
|
async def _view_wrapper(request, *args, **kwargs):
|
||||||
if not hasattr(request, "META"):
|
_check_request(request, "never_cache")
|
||||||
raise TypeError(
|
response = await view_func(request, *args, **kwargs)
|
||||||
"never_cache didn't receive an HttpRequest. If you are "
|
add_never_cache_headers(response)
|
||||||
"decorating a classmethod, be sure to use @method_decorator."
|
return response
|
||||||
)
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def _view_wrapper(request, *args, **kwargs):
|
||||||
|
_check_request(request, "never_cache")
|
||||||
response = view_func(request, *args, **kwargs)
|
response = view_func(request, *args, **kwargs)
|
||||||
add_never_cache_headers(response)
|
add_never_cache_headers(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
return _wrapper_view_func
|
return wraps(view_func)(_view_wrapper)
|
||||||
|
@ -214,7 +214,9 @@ CSRF
|
|||||||
Decorators
|
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
|
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
|
:ref:`asynchronous safety protection <async-safety>` to protect your data from
|
||||||
corruption.
|
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
|
Queries & the ORM
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
@ -117,6 +117,10 @@ client-side caching.
|
|||||||
:func:`~django.utils.cache.patch_cache_control` for the details of the
|
:func:`~django.utils.cache.patch_cache_control` for the details of the
|
||||||
transformation.
|
transformation.
|
||||||
|
|
||||||
|
.. versionchanged:: 5.0
|
||||||
|
|
||||||
|
Support for wrapping asynchronous view functions was added.
|
||||||
|
|
||||||
.. function:: never_cache(view_func)
|
.. function:: never_cache(view_func)
|
||||||
|
|
||||||
This decorator adds an ``Expires`` header to the current date/time.
|
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.
|
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
|
.. module:: django.views.decorators.common
|
||||||
|
|
||||||
Common
|
Common
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from asgiref.sync import iscoroutinefunction
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
@ -16,6 +18,20 @@ class HttpRequestProxy:
|
|||||||
|
|
||||||
|
|
||||||
class CacheControlDecoratorTest(SimpleTestCase):
|
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):
|
def test_cache_control_decorator_http_request(self):
|
||||||
class MyClass:
|
class MyClass:
|
||||||
@cache_control(a="b")
|
@cache_control(a="b")
|
||||||
@ -32,6 +48,22 @@ class CacheControlDecoratorTest(SimpleTestCase):
|
|||||||
with self.assertRaisesMessage(TypeError, msg):
|
with self.assertRaisesMessage(TypeError, msg):
|
||||||
MyClass().a_view(HttpRequestProxy(request))
|
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):
|
def test_cache_control_decorator_http_request_proxy(self):
|
||||||
class MyClass:
|
class MyClass:
|
||||||
@method_decorator(cache_control(a="b"))
|
@method_decorator(cache_control(a="b"))
|
||||||
@ -50,6 +82,14 @@ class CacheControlDecoratorTest(SimpleTestCase):
|
|||||||
response = a_view(HttpRequest())
|
response = a_view(HttpRequest())
|
||||||
self.assertEqual(response.get("Cache-Control"), "")
|
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):
|
def test_cache_control_full_decorator(self):
|
||||||
@cache_control(max_age=123, private=True, public=True, custom=456)
|
@cache_control(max_age=123, private=True, public=True, custom=456)
|
||||||
def a_view(request):
|
def a_view(request):
|
||||||
@ -61,6 +101,17 @@ class CacheControlDecoratorTest(SimpleTestCase):
|
|||||||
set(cache_control_items), {"max-age=123", "private", "public", "custom=456"}
|
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):
|
class CachePageDecoratorTest(SimpleTestCase):
|
||||||
def test_cache_page(self):
|
def test_cache_page(self):
|
||||||
@ -74,6 +125,20 @@ class CachePageDecoratorTest(SimpleTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class NeverCacheDecoratorTest(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")
|
@mock.patch("time.time")
|
||||||
def test_never_cache_decorator_headers(self, mocked_time):
|
def test_never_cache_decorator_headers(self, mocked_time):
|
||||||
@never_cache
|
@never_cache
|
||||||
@ -91,6 +156,20 @@ class NeverCacheDecoratorTest(SimpleTestCase):
|
|||||||
"max-age=0, no-cache, no-store, must-revalidate, private",
|
"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):
|
def test_never_cache_decorator_expires_not_overridden(self):
|
||||||
@never_cache
|
@never_cache
|
||||||
def a_view(request):
|
def a_view(request):
|
||||||
@ -99,6 +178,14 @@ class NeverCacheDecoratorTest(SimpleTestCase):
|
|||||||
response = a_view(HttpRequest())
|
response = a_view(HttpRequest())
|
||||||
self.assertEqual(response.headers["Expires"], "tomorrow")
|
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):
|
def test_never_cache_decorator_http_request(self):
|
||||||
class MyClass:
|
class MyClass:
|
||||||
@never_cache
|
@never_cache
|
||||||
@ -115,6 +202,22 @@ class NeverCacheDecoratorTest(SimpleTestCase):
|
|||||||
with self.assertRaisesMessage(TypeError, msg):
|
with self.assertRaisesMessage(TypeError, msg):
|
||||||
MyClass().a_view(HttpRequestProxy(request))
|
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):
|
def test_never_cache_decorator_http_request_proxy(self):
|
||||||
class MyClass:
|
class MyClass:
|
||||||
@method_decorator(never_cache)
|
@method_decorator(never_cache)
|
||||||
|
Loading…
Reference in New Issue
Block a user