浏览代码

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

Carlton Gibson 3 年之前
父节点
当前提交
9ffd4eae2c

+ 32 - 1
django/views/generic/base.py

@@ -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)]

+ 19 - 0
docs/ref/class-based-views/base.txt

@@ -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``
 ================
 ================

+ 17 - 0
docs/releases/4.1.txt

@@ -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

+ 3 - 2
docs/topics/async.txt

@@ -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``
-(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::
 .. note::
 
 

+ 30 - 0
docs/topics/class-based-views/index.txt

@@ -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`.

+ 67 - 1
tests/async/tests.py

@@ -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)