Refs #31949 -- Made http decorators to work with async functions.

This commit is contained in:
th3nn3ss 2023-04-11 12:40:55 +01:00 committed by Mariusz Felisiak
parent cd4c4c1905
commit 3152f9de47
5 changed files with 180 additions and 19 deletions

View File

@ -4,6 +4,8 @@ Decorators for views based on HTTP headers.
import datetime import datetime
from functools import wraps from functools import wraps
from asgiref.sync import iscoroutinefunction
from django.http import HttpResponseNotAllowed from django.http import HttpResponseNotAllowed
from django.middleware.http import ConditionalGetMiddleware from django.middleware.http import ConditionalGetMiddleware
from django.utils import timezone from django.utils import timezone
@ -28,6 +30,24 @@ def require_http_methods(request_method_list):
""" """
def decorator(func): def decorator(func):
if iscoroutinefunction(func):
@wraps(func)
async def inner(request, *args, **kwargs):
if request.method not in request_method_list:
response = HttpResponseNotAllowed(request_method_list)
log_response(
"Method Not Allowed (%s): %s",
request.method,
request.path,
response=response,
request=request,
)
return response
return await func(request, *args, **kwargs)
else:
@wraps(func) @wraps(func)
def inner(request, *args, **kwargs): def inner(request, *args, **kwargs):
if request.method not in request_method_list: if request.method not in request_method_list:
@ -83,8 +103,7 @@ def condition(etag_func=None, last_modified_func=None):
""" """
def decorator(func): def decorator(func):
@wraps(func) def _pre_process_request(request, *args, **kwargs):
def inner(request, *args, **kwargs):
# Compute values (if any) for the requested resource. # Compute values (if any) for the requested resource.
res_last_modified = None res_last_modified = None
if last_modified_func: if last_modified_func:
@ -100,10 +119,9 @@ def condition(etag_func=None, last_modified_func=None):
etag=res_etag, etag=res_etag,
last_modified=res_last_modified, last_modified=res_last_modified,
) )
return response, res_etag, res_last_modified
if response is None: def _post_process_request(request, response, res_etag, res_last_modified):
response = func(request, *args, **kwargs)
# Set relevant headers on the response if they don't already exist # Set relevant headers on the response if they don't already exist
# and if the request method is safe. # and if the request method is safe.
if request.method in ("GET", "HEAD"): if request.method in ("GET", "HEAD"):
@ -112,6 +130,28 @@ def condition(etag_func=None, last_modified_func=None):
if res_etag: if res_etag:
response.headers.setdefault("ETag", res_etag) response.headers.setdefault("ETag", res_etag)
if iscoroutinefunction(func):
@wraps(func)
async def inner(request, *args, **kwargs):
response, res_etag, res_last_modified = _pre_process_request(
request, *args, **kwargs
)
if response is None:
response = await func(request, *args, **kwargs)
_post_process_request(request, response, res_etag, res_last_modified)
return response
else:
@wraps(func)
def inner(request, *args, **kwargs):
response, res_etag, res_last_modified = _pre_process_request(
request, *args, **kwargs
)
if response is None:
response = func(request, *args, **kwargs)
_post_process_request(request, response, res_etag, res_last_modified)
return response return response
return inner return inner

View File

@ -243,6 +243,13 @@ Decorators
* :func:`~django.views.decorators.common.no_append_slash` * :func:`~django.views.decorators.common.no_append_slash`
* :func:`~django.views.decorators.debug.sensitive_variables` * :func:`~django.views.decorators.debug.sensitive_variables`
* :func:`~django.views.decorators.debug.sensitive_post_parameters` * :func:`~django.views.decorators.debug.sensitive_post_parameters`
* :func:`~django.views.decorators.http.condition`
* :func:`~django.views.decorators.http.etag`
* :func:`~django.views.decorators.http.last_modified`
* :func:`~django.views.decorators.http.require_http_methods`
* :func:`~django.views.decorators.http.require_GET`
* :func:`~django.views.decorators.http.require_POST`
* :func:`~django.views.decorators.http.require_safe`
* ``xframe_options_deny()`` * ``xframe_options_deny()``
* ``xframe_options_sameorigin()`` * ``xframe_options_sameorigin()``
* ``xframe_options_exempt()`` * ``xframe_options_exempt()``

View File

@ -84,6 +84,13 @@ view functions:
* :func:`~django.views.decorators.cache.cache_control` * :func:`~django.views.decorators.cache.cache_control`
* :func:`~django.views.decorators.cache.never_cache` * :func:`~django.views.decorators.cache.never_cache`
* :func:`~django.views.decorators.common.no_append_slash` * :func:`~django.views.decorators.common.no_append_slash`
* :func:`~django.views.decorators.http.condition`
* :func:`~django.views.decorators.http.etag`
* :func:`~django.views.decorators.http.last_modified`
* :func:`~django.views.decorators.http.require_http_methods`
* :func:`~django.views.decorators.http.require_GET`
* :func:`~django.views.decorators.http.require_POST`
* :func:`~django.views.decorators.http.require_safe`
* ``xframe_options_deny()`` * ``xframe_options_deny()``
* ``xframe_options_sameorigin()`` * ``xframe_options_sameorigin()``
* ``xframe_options_exempt()`` * ``xframe_options_exempt()``

View File

@ -33,14 +33,26 @@ a :class:`django.http.HttpResponseNotAllowed` if the conditions are not met.
Note that request methods should be in uppercase. Note that request methods should be in uppercase.
.. versionchanged:: 5.0
Support for wrapping asynchronous view functions was added.
.. function:: require_GET() .. function:: require_GET()
Decorator to require that a view only accepts the GET method. Decorator to require that a view only accepts the GET method.
.. versionchanged:: 5.0
Support for wrapping asynchronous view functions was added.
.. function:: require_POST() .. function:: require_POST()
Decorator to require that a view only accepts the POST method. Decorator to require that a view only accepts the POST method.
.. versionchanged:: 5.0
Support for wrapping asynchronous view functions was added.
.. function:: require_safe() .. function:: require_safe()
Decorator to require that a view only accepts the GET and HEAD methods. Decorator to require that a view only accepts the GET and HEAD methods.
@ -55,6 +67,10 @@ a :class:`django.http.HttpResponseNotAllowed` if the conditions are not met.
such as link checkers, rely on HEAD requests, you might prefer such as link checkers, rely on HEAD requests, you might prefer
using ``require_safe`` instead of ``require_GET``. using ``require_safe`` instead of ``require_GET``.
.. versionchanged:: 5.0
Support for wrapping asynchronous view functions was added.
Conditional view processing Conditional view processing
=========================== ===========================
@ -71,6 +87,10 @@ control caching behavior on particular views.
headers; see headers; see
:doc:`conditional view processing </topics/conditional-view-processing>`. :doc:`conditional view processing </topics/conditional-view-processing>`.
.. versionchanged:: 5.0
Support for wrapping asynchronous view functions was added.
.. module:: django.views.decorators.gzip .. module:: django.views.decorators.gzip
GZip compression GZip compression

View File

@ -1,11 +1,27 @@
import datetime import datetime
from asgiref.sync import iscoroutinefunction
from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.views.decorators.http import condition, require_http_methods, require_safe from django.views.decorators.http import condition, require_http_methods, require_safe
class RequireHttpMethodsTest(SimpleTestCase): class RequireHttpMethodsTest(SimpleTestCase):
def test_wrapped_sync_function_is_not_coroutine_function(self):
def sync_view(request):
return HttpResponse()
wrapped_view = require_http_methods(["GET"])(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 = require_http_methods(["GET"])(async_view)
self.assertIs(iscoroutinefunction(wrapped_view), True)
def test_require_http_methods_methods(self): def test_require_http_methods_methods(self):
@require_http_methods(["GET", "PUT"]) @require_http_methods(["GET", "PUT"])
def my_view(request): def my_view(request):
@ -23,6 +39,23 @@ class RequireHttpMethodsTest(SimpleTestCase):
request.method = "DELETE" request.method = "DELETE"
self.assertIsInstance(my_view(request), HttpResponseNotAllowed) self.assertIsInstance(my_view(request), HttpResponseNotAllowed)
async def test_require_http_methods_methods_async_view(self):
@require_http_methods(["GET", "PUT"])
async def my_view(request):
return HttpResponse("OK")
request = HttpRequest()
request.method = "GET"
self.assertIsInstance(await my_view(request), HttpResponse)
request.method = "PUT"
self.assertIsInstance(await my_view(request), HttpResponse)
request.method = "HEAD"
self.assertIsInstance(await my_view(request), HttpResponseNotAllowed)
request.method = "POST"
self.assertIsInstance(await my_view(request), HttpResponseNotAllowed)
request.method = "DELETE"
self.assertIsInstance(await my_view(request), HttpResponseNotAllowed)
class RequireSafeDecoratorTest(SimpleTestCase): class RequireSafeDecoratorTest(SimpleTestCase):
def test_require_safe_accepts_only_safe_methods(self): def test_require_safe_accepts_only_safe_methods(self):
@ -42,6 +75,23 @@ class RequireSafeDecoratorTest(SimpleTestCase):
request.method = "DELETE" request.method = "DELETE"
self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed) self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed)
async def test_require_safe_accepts_only_safe_methods_async_view(self):
@require_safe
async def async_view(request):
return HttpResponse("OK")
request = HttpRequest()
request.method = "GET"
self.assertIsInstance(await async_view(request), HttpResponse)
request.method = "HEAD"
self.assertIsInstance(await async_view(request), HttpResponse)
request.method = "POST"
self.assertIsInstance(await async_view(request), HttpResponseNotAllowed)
request.method = "PUT"
self.assertIsInstance(await async_view(request), HttpResponseNotAllowed)
request.method = "DELETE"
self.assertIsInstance(await async_view(request), HttpResponseNotAllowed)
class ConditionDecoratorTest(SimpleTestCase): class ConditionDecoratorTest(SimpleTestCase):
def etag_func(request, *args, **kwargs): def etag_func(request, *args, **kwargs):
@ -50,6 +100,24 @@ class ConditionDecoratorTest(SimpleTestCase):
def latest_entry(request, *args, **kwargs): def latest_entry(request, *args, **kwargs):
return datetime.datetime(2023, 1, 2, 23, 21, 47) return datetime.datetime(2023, 1, 2, 23, 21, 47)
def test_wrapped_sync_function_is_not_coroutine_function(self):
def sync_view(request):
return HttpResponse()
wrapped_view = condition(
etag_func=self.etag_func, last_modified_func=self.latest_entry
)(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 = condition(
etag_func=self.etag_func, last_modified_func=self.latest_entry
)(async_view)
self.assertIs(iscoroutinefunction(wrapped_view), True)
def test_condition_decorator(self): def test_condition_decorator(self):
@condition( @condition(
etag_func=self.etag_func, etag_func=self.etag_func,
@ -68,3 +136,22 @@ class ConditionDecoratorTest(SimpleTestCase):
response.headers["Last-Modified"], response.headers["Last-Modified"],
"Mon, 02 Jan 2023 23:21:47 GMT", "Mon, 02 Jan 2023 23:21:47 GMT",
) )
async def test_condition_decorator_async_view(self):
@condition(
etag_func=self.etag_func,
last_modified_func=self.latest_entry,
)
async def async_view(request):
return HttpResponse()
request = HttpRequest()
request.method = "GET"
response = await async_view(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["ETag"], '"b4246ffc4f62314ca13147c9d4f76974"')
self.assertEqual(
response.headers["Last-Modified"],
"Mon, 02 Jan 2023 23:21:47 GMT",
)