Updated docstring and added support for async function based views

This commit is contained in:
enrico 2022-09-01 08:23:05 +08:00
parent ae84d97d9a
commit 351ea7d61f
3 changed files with 65 additions and 5 deletions

View File

@ -6,6 +6,7 @@ There are also various decorators for setting the API policies on function
based views, as well as the `@action` decorator, which is used to annotate based views, as well as the `@action` decorator, which is used to annotate
methods on viewsets that should be included by routers. methods on viewsets that should be included by routers.
""" """
import asyncio
import types import types
from django.forms.utils import pretty_name from django.forms.utils import pretty_name
@ -46,11 +47,19 @@ def api_view(http_method_names=None):
allowed_methods = set(http_method_names) | {'options'} allowed_methods = set(http_method_names) | {'options'}
WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods] WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods]
def handler(self, *args, **kwargs): def sync_handler(self, *args, **kwargs):
return func(*args, **kwargs) return func(*args, **kwargs)
async def async_handler(self, *args, **kwargs):
return await func(*args, **kwargs)
view_is_async = asyncio.iscoroutinefunction(func)
for method in http_method_names: for method in http_method_names:
setattr(WrappedAPIView, method.lower(), handler) if view_is_async:
setattr(WrappedAPIView, method.lower(), async_handler)
else:
setattr(WrappedAPIView, method.lower(), sync_handler)
WrappedAPIView.__name__ = func.__name__ WrappedAPIView.__name__ = func.__name__
WrappedAPIView.__module__ = func.__module__ WrappedAPIView.__module__ = func.__module__

View File

@ -484,7 +484,7 @@ class APIView(View):
# be overridden. # be overridden.
def sync_dispatch(self, request, *args, **kwargs): def sync_dispatch(self, request, *args, **kwargs):
""" """
`.dispatch()` is pretty much the same as Django's regular dispatch, `.sync_dispatch()` is pretty much the same as Django's regular dispatch,
but with extra hooks for startup, finalize, and exception handling. but with extra hooks for startup, finalize, and exception handling.
""" """
self.args = args self.args = args
@ -513,8 +513,9 @@ class APIView(View):
async def async_dispatch(self, request, *args, **kwargs): async def async_dispatch(self, request, *args, **kwargs):
""" """
`.dispatch()` is pretty much the same as Django's regular dispatch, `.async_dispatch()` is pretty much the same as Django's regular dispatch,
but with extra hooks for startup, finalize, and exception handling. except for awaiting the handler function and with extra hooks for startup,
finalize, and exception handling.
""" """
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
@ -541,6 +542,10 @@ class APIView(View):
return self.response return self.response
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""
Dispatch checks if the view is async or not and uses the respective
async or sync dispatch method.
"""
if hasattr(self, 'view_is_async') and self.view_is_async: if hasattr(self, 'view_is_async') and self.view_is_async:
return self.async_dispatch(request, *args, **kwargs) return self.async_dispatch(request, *args, **kwargs)
else: else:

View File

@ -45,6 +45,18 @@ def basic_view(request):
return Response({'method': 'PATCH', 'data': request.data}) return Response({'method': 'PATCH', 'data': request.data})
@api_view(['GET', 'POST', 'PUT', 'PATCH'])
async def basic_async_view(request):
if request.method == 'GET':
return Response({'method': 'GET'})
elif request.method == 'POST':
return Response({'method': 'POST', 'data': request.data})
elif request.method == 'PUT':
return Response({'method': 'PUT', 'data': request.data})
elif request.method == 'PATCH':
return Response({'method': 'PATCH', 'data': request.data})
class ErrorView(APIView): class ErrorView(APIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
raise Exception raise Exception
@ -173,6 +185,40 @@ class ClassBasedAsyncViewIntegrationTests(TestCase):
assert sanitise_json_error(response.data) == expected assert sanitise_json_error(response.data) == expected
@pytest.mark.skipif(
django.VERSION < (4, 1),
reason="Async view support requires Django 4.1 or higher",
)
class FunctionBasedAsyncViewIntegrationTests(TestCase):
def setUp(self):
self.view = basic_async_view
def test_get_succeeds(self):
request = factory.get('/')
response = async_to_sync(self.view)(request)
assert response.status_code == status.HTTP_200_OK
assert response.data == {'method': 'GET'}
def test_post_succeeds(self):
request = factory.post('/', {'test': 'foo'})
response = async_to_sync(self.view)(request)
expected = {
'method': 'POST',
'data': {'test': ['foo']}
}
assert response.status_code == status.HTTP_200_OK
assert response.data == expected
def test_400_parse_error(self):
request = factory.post('/', 'f00bar', content_type='application/json')
response = async_to_sync(self.view)(request)
expected = {
'detail': JSON_ERROR
}
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert sanitise_json_error(response.data) == expected
class TestCustomExceptionHandler(TestCase): class TestCustomExceptionHandler(TestCase):
def setUp(self): def setUp(self):
self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER