mirror of https://github.com/django/django.git
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
|
import logging
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
@ -11,6 +12,7 @@ from django.http import (
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.decorators import classonlymethod
|
from django.utils.decorators import classonlymethod
|
||||||
|
from django.utils.functional import classproperty
|
||||||
|
|
||||||
logger = logging.getLogger("django.request")
|
logger = logging.getLogger("django.request")
|
||||||
|
|
||||||
|
@ -57,6 +59,23 @@ class View:
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
setattr(self, key, value)
|
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
|
@classonlymethod
|
||||||
def as_view(cls, **initkwargs):
|
def as_view(cls, **initkwargs):
|
||||||
"""Main entry point for a request-response process."""
|
"""Main entry point for a request-response process."""
|
||||||
|
@ -96,6 +115,10 @@ class View:
|
||||||
# the dispatch method.
|
# the dispatch method.
|
||||||
view.__dict__.update(cls.dispatch.__dict__)
|
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
|
return view
|
||||||
|
|
||||||
def setup(self, request, *args, **kwargs):
|
def setup(self, request, *args, **kwargs):
|
||||||
|
@ -132,7 +155,15 @@ class View:
|
||||||
response = HttpResponse()
|
response = HttpResponse()
|
||||||
response.headers["Allow"] = ", ".join(self._allowed_methods())
|
response.headers["Allow"] = ", ".join(self._allowed_methods())
|
||||||
response.headers["Content-Length"] = "0"
|
response.headers["Content-Length"] = "0"
|
||||||
return response
|
|
||||||
|
if self.view_is_async:
|
||||||
|
|
||||||
|
async def func():
|
||||||
|
return response
|
||||||
|
|
||||||
|
return func()
|
||||||
|
else:
|
||||||
|
return response
|
||||||
|
|
||||||
def _allowed_methods(self):
|
def _allowed_methods(self):
|
||||||
return [m.upper() for m in self.http_method_names if hasattr(self, m)]
|
return [m.upper() for m in self.http_method_names if hasattr(self, m)]
|
||||||
|
|
|
@ -77,6 +77,17 @@ MRO is an acronym for Method Resolution Order.
|
||||||
<how-django-processes-a-request>` to the ``args`` and ``kwargs``
|
<how-django-processes-a-request>` to the ``args`` and ``kwargs``
|
||||||
attributes, respectively. Then :meth:`dispatch` is called.
|
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)
|
.. method:: setup(request, *args, **kwargs)
|
||||||
|
|
||||||
Performs key view initialization prior to :meth:`dispatch`.
|
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
|
response with the ``Allow`` header containing a list of the view's
|
||||||
allowed HTTP method names.
|
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``
|
``TemplateView``
|
||||||
================
|
================
|
||||||
|
|
|
@ -26,6 +26,23 @@ officially support the latest release of each series.
|
||||||
What's new in Django 4.1
|
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-usage:
|
||||||
|
|
||||||
``CSRF_COOKIE_MASKED`` setting
|
``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
|
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
|
coroutine - commonly, this is done using ``async def``. For a function-based
|
||||||
view, this means declaring the whole view using ``async def``. For a
|
view, this means declaring the whole view using ``async def``. For a
|
||||||
class-based view, this means making its ``__call__()`` method an ``async def``
|
class-based view, this means declaring the HTTP method handlers, such as
|
||||||
(not its ``__init__()`` or ``as_view()``).
|
``get()`` and ``post()`` as ``async def`` (not its ``__init__()``, or
|
||||||
|
``as_view()``).
|
||||||
|
|
||||||
.. note::
|
.. 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``
|
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
|
header indicates when the most recent book was published. Based on this
|
||||||
information, the client may or may not download the full object list.
|
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 os
|
||||||
import sys
|
import sys
|
||||||
from unittest import mock, skipIf
|
from unittest import mock, skipIf
|
||||||
|
@ -5,9 +6,11 @@ from unittest import mock, skipIf
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
|
|
||||||
from django.core.cache import DEFAULT_CACHE_ALIAS, caches
|
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.test import SimpleTestCase
|
||||||
from django.utils.asyncio import async_unsafe
|
from django.utils.asyncio import async_unsafe
|
||||||
|
from django.views.generic.base import View
|
||||||
|
|
||||||
from .models import SimpleModel
|
from .models import SimpleModel
|
||||||
|
|
||||||
|
@ -72,3 +75,66 @@ class AsyncUnsafeTest(SimpleTestCase):
|
||||||
self.dangerous_method()
|
self.dangerous_method()
|
||||||
except SynchronousOnlyOperation:
|
except SynchronousOnlyOperation:
|
||||||
self.fail("SynchronousOnlyOperation should not be raised.")
|
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