1
0
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:
Ben Lomax 2022-07-08 09:39:33 +01:00 committed by Mariusz Felisiak
parent a14ddc8cfc
commit 4dfc6ff8a8
5 changed files with 182 additions and 26 deletions

View File

@ -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)

View File

@ -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
~~~~~ ~~~~~

View File

@ -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
----------------- -----------------

View File

@ -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

View File

@ -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)