Fixed #33611 -- Allowed View subclasses to define async method handlers.
This commit is contained in:
parent
2ee4caf56b
commit
9ffd4eae2c
|
@ -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):
|
||||
|
|
|
@ -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``
|
||||
================
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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::
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue