Fixed #33611 -- Allowed View subclasses to define async method handlers.

This commit is contained in:
Carlton Gibson 2022-04-07 07:05:59 +02:00 committed by GitHub
parent 2ee4caf56b
commit 9ffd4eae2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 168 additions and 4 deletions

View File

@ -1,3 +1,4 @@
import asyncio
import logging
from django.core.exceptions import ImproperlyConfigured
@ -11,6 +12,7 @@ from django.http import (
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.decorators import classonlymethod
from django.utils.functional import classproperty
logger = logging.getLogger("django.request")
@ -57,6 +59,23 @@ class View:
for key, value in kwargs.items():
setattr(self, key, value)
@classproperty
def view_is_async(cls):
handlers = [
getattr(cls, method)
for method in cls.http_method_names
if (method != "options" and hasattr(cls, method))
]
if not handlers:
return False
is_async = asyncio.iscoroutinefunction(handlers[0])
if not all(asyncio.iscoroutinefunction(h) == is_async for h in handlers[1:]):
raise ImproperlyConfigured(
f"{cls.__qualname__} HTTP handlers must either be all sync or all "
"async."
)
return is_async
@classonlymethod
def as_view(cls, **initkwargs):
"""Main entry point for a request-response process."""
@ -96,6 +115,10 @@ class View:
# the dispatch method.
view.__dict__.update(cls.dispatch.__dict__)
# Mark the callback if the view class is async.
if cls.view_is_async:
view._is_coroutine = asyncio.coroutines._is_coroutine
return view
def setup(self, request, *args, **kwargs):
@ -132,6 +155,14 @@ class View:
response = HttpResponse()
response.headers["Allow"] = ", ".join(self._allowed_methods())
response.headers["Content-Length"] = "0"
if self.view_is_async:
async def func():
return response
return func()
else:
return response
def _allowed_methods(self):

View File

@ -77,6 +77,17 @@ MRO is an acronym for Method Resolution Order.
<how-django-processes-a-request>` to the ``args`` and ``kwargs``
attributes, respectively. Then :meth:`dispatch` is called.
If a ``View`` subclass defines asynchronous (``async def``) method
handlers, ``as_view()`` will mark the returned callable as a coroutine
function. An ``ImproperlyConfigured`` exception will be raised if both
asynchronous (``async def``) and synchronous (``def``) handlers are
defined on a single view-class.
.. versionchanged:: 4.1
Compatibility with asynchronous (``async def``) method handlers was
added.
.. method:: setup(request, *args, **kwargs)
Performs key view initialization prior to :meth:`dispatch`.
@ -111,6 +122,14 @@ MRO is an acronym for Method Resolution Order.
response with the ``Allow`` header containing a list of the view's
allowed HTTP method names.
If the other HTTP methods handlers on the class are asynchronous
(``async def``) then the response will be wrapped in a coroutine
function for use with ``await``.
.. versionchanged:: 4.1
Compatibility with classes defining asynchronous (``async def``)
method handlers was added.
``TemplateView``
================

View File

@ -26,6 +26,23 @@ officially support the latest release of each series.
What's new in Django 4.1
========================
Asynchronous handlers for class-based views
-------------------------------------------
View subclasses may now define async HTTP method handlers::
import asyncio
from django.http import HttpResponse
from django.views import View
class AsyncView(View):
async def get(self, request, *args, **kwargs):
# Perform view logic using await.
await asyncio.sleep(1)
return HttpResponse("Hello async world!")
See :ref:`async-class-based-views` for more details.
.. _csrf-cookie-masked-usage:
``CSRF_COOKIE_MASKED`` setting

View File

@ -22,8 +22,9 @@ Async views
Any view can be declared async by making the callable part of it return a
coroutine - commonly, this is done using ``async def``. For a function-based
view, this means declaring the whole view using ``async def``. For a
class-based view, this means making its ``__call__()`` method an ``async def``
(not its ``__init__()`` or ``as_view()``).
class-based view, this means declaring the HTTP method handlers, such as
``get()`` and ``post()`` as ``async def`` (not its ``__init__()``, or
``as_view()``).
.. note::

View File

@ -128,3 +128,33 @@ the response (using the ``book_list.html`` template). But if the client issues
a ``HEAD`` request, the response has an empty body and the ``Last-Modified``
header indicates when the most recent book was published. Based on this
information, the client may or may not download the full object list.
.. _async-class-based-views:
Asynchronous class-based views
==============================
.. versionadded:: 4.1
As well as the synchronous (``def``) method handlers already shown, ``View``
subclasses may define asynchronous (``async def``) method handlers to leverage
asynchronous code using ``await``::
import asyncio
from django.http import HttpResponse
from django.views import View
class AsyncView(View):
async def get(self, request, *args, **kwargs):
# Perform io-blocking view logic using await, sleep for example.
await asyncio.sleep(1)
return HttpResponse("Hello async world!")
Within a single view-class, all user-defined method handlers must be either
synchronous, using ``def``, or all asynchronous, using ``async def``. An
``ImproperlyConfigured`` exception will be raised in ``as_view()`` if ``def``
and ``async def`` declarations are mixed.
Django will automatically detect asynchronous views and run them in an
asynchronous context. You can read more about Django's asynchronous support,
and how to best use async views, in :doc:`/topics/async`.

View File

@ -1,3 +1,4 @@
import asyncio
import os
import sys
from unittest import mock, skipIf
@ -5,9 +6,11 @@ from unittest import mock, skipIf
from asgiref.sync import async_to_sync
from django.core.cache import DEFAULT_CACHE_ALIAS, caches
from django.core.exceptions import SynchronousOnlyOperation
from django.core.exceptions import ImproperlyConfigured, SynchronousOnlyOperation
from django.http import HttpResponse
from django.test import SimpleTestCase
from django.utils.asyncio import async_unsafe
from django.views.generic.base import View
from .models import SimpleModel
@ -72,3 +75,66 @@ class AsyncUnsafeTest(SimpleTestCase):
self.dangerous_method()
except SynchronousOnlyOperation:
self.fail("SynchronousOnlyOperation should not be raised.")
class SyncView(View):
def get(self, request, *args, **kwargs):
return HttpResponse("Hello (sync) world!")
class AsyncView(View):
async def get(self, request, *args, **kwargs):
return HttpResponse("Hello (async) world!")
class ViewTests(SimpleTestCase):
def test_views_are_correctly_marked(self):
tests = [
(SyncView, False),
(AsyncView, True),
]
for view_cls, is_async in tests:
with self.subTest(view_cls=view_cls, is_async=is_async):
self.assertIs(view_cls.view_is_async, is_async)
callback = view_cls.as_view()
self.assertIs(asyncio.iscoroutinefunction(callback), is_async)
def test_mixed_views_raise_error(self):
class MixedView(View):
def get(self, request, *args, **kwargs):
return HttpResponse("Hello (mixed) world!")
async def post(self, request, *args, **kwargs):
return HttpResponse("Hello (mixed) world!")
msg = (
f"{MixedView.__qualname__} HTTP handlers must either be all sync or all "
"async."
)
with self.assertRaisesMessage(ImproperlyConfigured, msg):
MixedView.as_view()
def test_options_handler_responds_correctly(self):
tests = [
(SyncView, False),
(AsyncView, True),
]
for view_cls, is_coroutine in tests:
with self.subTest(view_cls=view_cls, is_coroutine=is_coroutine):
instance = view_cls()
response = instance.options(None)
self.assertIs(
asyncio.iscoroutine(response),
is_coroutine,
)
if is_coroutine:
response = asyncio.run(response)
self.assertIsInstance(response, HttpResponse)
def test_base_view_class_is_sync(self):
"""
View and by extension any subclasses that don't define handlers are
sync.
"""
self.assertIs(View.view_is_async, False)