Browse Source

Refs #34118 -- Adopted asgiref coroutine detection shims.

Thanks to Mariusz Felisiak for review.
Carlton Gibson 2 years ago
parent
commit
32d70b2f55

+ 6 - 6
django/core/handlers/base.py

@@ -2,7 +2,7 @@ import asyncio
 import logging
 import types
 
-from asgiref.sync import async_to_sync, sync_to_async
+from asgiref.sync import async_to_sync, iscoroutinefunction, sync_to_async
 
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured, MiddlewareNotUsed
@@ -119,7 +119,7 @@ class BaseHandler:
           - Asynchronous methods are left alone
         """
         if method_is_async is None:
-            method_is_async = asyncio.iscoroutinefunction(method)
+            method_is_async = iscoroutinefunction(method)
         if debug and not name:
             name = name or "method %s()" % method.__qualname__
         if is_async:
@@ -191,7 +191,7 @@ class BaseHandler:
         if response is None:
             wrapped_callback = self.make_view_atomic(callback)
             # If it is an asynchronous view, run it in a subthread.
-            if asyncio.iscoroutinefunction(wrapped_callback):
+            if iscoroutinefunction(wrapped_callback):
                 wrapped_callback = async_to_sync(wrapped_callback)
             try:
                 response = wrapped_callback(request, *callback_args, **callback_kwargs)
@@ -245,7 +245,7 @@ class BaseHandler:
         if response is None:
             wrapped_callback = self.make_view_atomic(callback)
             # If it is a synchronous view, run it in a subthread
-            if not asyncio.iscoroutinefunction(wrapped_callback):
+            if not iscoroutinefunction(wrapped_callback):
                 wrapped_callback = sync_to_async(
                     wrapped_callback, thread_sensitive=True
                 )
@@ -278,7 +278,7 @@ class BaseHandler:
                     % (middleware_method.__self__.__class__.__name__,),
                 )
             try:
-                if asyncio.iscoroutinefunction(response.render):
+                if iscoroutinefunction(response.render):
                     response = await response.render()
                 else:
                     response = await sync_to_async(
@@ -346,7 +346,7 @@ class BaseHandler:
         non_atomic_requests = getattr(view, "_non_atomic_requests", set())
         for alias, settings_dict in connections.settings.items():
             if settings_dict["ATOMIC_REQUESTS"] and alias not in non_atomic_requests:
-                if asyncio.iscoroutinefunction(view):
+                if iscoroutinefunction(view):
                     raise RuntimeError(
                         "You cannot use ATOMIC_REQUESTS with async views."
                     )

+ 2 - 3
django/core/handlers/exception.py

@@ -1,9 +1,8 @@
-import asyncio
 import logging
 import sys
 from functools import wraps
 
-from asgiref.sync import sync_to_async
+from asgiref.sync import iscoroutinefunction, sync_to_async
 
 from django.conf import settings
 from django.core import signals
@@ -34,7 +33,7 @@ def convert_exception_to_response(get_response):
     no middleware leaks an exception and that the next middleware in the stack
     can rely on getting a response instead of an exception.
     """
-    if asyncio.iscoroutinefunction(get_response):
+    if iscoroutinefunction(get_response):
 
         @wraps(get_response)
         async def inner(request):

+ 2 - 3
django/test/testcases.py

@@ -1,4 +1,3 @@
-import asyncio
 import difflib
 import inspect
 import json
@@ -26,7 +25,7 @@ from urllib.parse import (
 )
 from urllib.request import url2pathname
 
-from asgiref.sync import async_to_sync
+from asgiref.sync import async_to_sync, iscoroutinefunction
 
 from django.apps import apps
 from django.conf import settings
@@ -401,7 +400,7 @@ class SimpleTestCase(unittest.TestCase):
         )
 
         # Convert async test methods.
-        if asyncio.iscoroutinefunction(testMethod):
+        if iscoroutinefunction(testMethod):
             setattr(self, self._testMethodName, async_to_sync(testMethod))
 
         if not skipped:

+ 3 - 2
django/test/utils.py

@@ -1,4 +1,3 @@
-import asyncio
 import collections
 import logging
 import os
@@ -14,6 +13,8 @@ from types import SimpleNamespace
 from unittest import TestCase, skipIf, skipUnless
 from xml.dom.minidom import Node, parseString
 
+from asgiref.sync import iscoroutinefunction
+
 from django.apps import apps
 from django.apps.registry import Apps
 from django.conf import UserSettingsHolder, settings
@@ -440,7 +441,7 @@ class TestContextDecorator:
         raise TypeError("Can only decorate subclasses of unittest.TestCase")
 
     def decorate_callable(self, func):
-        if asyncio.iscoroutinefunction(func):
+        if iscoroutinefunction(func):
             # If the inner function is an async function, we must execute async
             # as well so that the `with` statement executes at the right time.
             @wraps(func)

+ 4 - 7
django/utils/deprecation.py

@@ -1,8 +1,7 @@
-import asyncio
 import inspect
 import warnings
 
-from asgiref.sync import sync_to_async
+from asgiref.sync import iscoroutinefunction, markcoroutinefunction, sync_to_async
 
 
 class RemovedInDjango50Warning(DeprecationWarning):
@@ -120,16 +119,14 @@ class MiddlewareMixin:
         If get_response is a coroutine function, turns us into async mode so
         a thread is not consumed during a whole request.
         """
-        if asyncio.iscoroutinefunction(self.get_response):
+        if iscoroutinefunction(self.get_response):
             # Mark the class as async-capable, but do the actual switch
             # inside __call__ to avoid swapping out dunder methods
-            self._is_coroutine = asyncio.coroutines._is_coroutine
-        else:
-            self._is_coroutine = None
+            markcoroutinefunction(self)
 
     def __call__(self, request):
         # Exit out to async mode, if needed
-        if self._is_coroutine:
+        if iscoroutinefunction(self):
             return self.__acall__(request)
         response = None
         if hasattr(self, "process_request"):

+ 5 - 4
django/views/generic/base.py

@@ -1,6 +1,7 @@
-import asyncio
 import logging
 
+from asgiref.sync import iscoroutinefunction, markcoroutinefunction
+
 from django.core.exceptions import ImproperlyConfigured
 from django.http import (
     HttpResponse,
@@ -68,8 +69,8 @@ class View:
         ]
         if not handlers:
             return False
-        is_async = asyncio.iscoroutinefunction(handlers[0])
-        if not all(asyncio.iscoroutinefunction(h) == is_async for h in handlers[1:]):
+        is_async = iscoroutinefunction(handlers[0])
+        if not all(iscoroutinefunction(h) == is_async for h in handlers[1:]):
             raise ImproperlyConfigured(
                 f"{cls.__qualname__} HTTP handlers must either be all sync or all "
                 "async."
@@ -117,7 +118,7 @@ class View:
 
         # Mark the callback if the view class is async.
         if cls.view_is_async:
-            view._is_coroutine = asyncio.coroutines._is_coroutine
+            markcoroutinefunction(view)
 
         return view
 

+ 1 - 1
docs/internals/contributing/writing-code/unit-tests.txt

@@ -278,7 +278,7 @@ dependencies:
 
 *  aiosmtpd_
 *  argon2-cffi_ 19.1.0+
-*  asgiref_ 3.5.2+ (required)
+*  asgiref_ 3.6.0+ (required)
 *  bcrypt_
 *  colorama_
 *  docutils_

+ 3 - 0
docs/releases/4.2.txt

@@ -438,6 +438,9 @@ Miscellaneous
   ``DatabaseIntrospection.get_table_description()`` rather than
   ``internal_size`` for ``CharField``.
 
+* The minimum supported version of ``asgiref`` is increased from 3.5.2 to
+  3.6.0.
+
 .. _deprecated-features-4.2:
 
 Features deprecated in 4.2

+ 3 - 3
docs/topics/async.txt

@@ -28,10 +28,10 @@ class-based view, this means declaring the HTTP method handlers, such as
 
 .. note::
 
-    Django uses ``asyncio.iscoroutinefunction`` to test if your view is
+    Django uses ``asgiref.sync.iscoroutinefunction`` to test if your view is
     asynchronous or not. If you implement your own method of returning a
-    coroutine, ensure you set the ``_is_coroutine`` attribute of the view
-    to ``asyncio.coroutines._is_coroutine`` so this function returns ``True``.
+    coroutine, ensure you use ``asgiref.sync.markcoroutinefunction`` so this
+    function returns ``True``.
 
 Under a WSGI server, async views will run in their own, one-off event loop.
 This means you can use async features, like concurrent async HTTP requests,

+ 3 - 3
docs/topics/http/middleware.txt

@@ -312,7 +312,7 @@ If your middleware has both ``sync_capable = True`` and
 ``async_capable = True``, then Django will pass it the request without
 converting it. In this case, you can work out if your middleware will receive
 async requests by checking if the ``get_response`` object you are passed is a
-coroutine function, using ``asyncio.iscoroutinefunction``.
+coroutine function, using ``asgiref.sync.iscoroutinefunction``.
 
 The ``django.utils.decorators`` module contains
 :func:`~django.utils.decorators.sync_only_middleware`,
@@ -331,13 +331,13 @@ at an additional performance penalty.
 
 Here's an example of how to create a middleware function that supports both::
 
-    import asyncio
+    from asgiref.sync import iscoroutinefunction
     from django.utils.decorators import sync_and_async_middleware
 
     @sync_and_async_middleware
     def simple_middleware(get_response):
         # One-time configuration and initialization goes here.
-        if asyncio.iscoroutinefunction(get_response):
+        if iscoroutinefunction(get_response):
             async def middleware(request):
                 # Do something here!
                 response = await get_response(request)

+ 1 - 1
setup.cfg

@@ -39,7 +39,7 @@ packages = find:
 include_package_data = true
 zip_safe = false
 install_requires =
-    asgiref >= 3.5.2
+    asgiref >= 3.6.0
     backports.zoneinfo; python_version<"3.9"
     sqlparse >= 0.2.2
     tzdata; sys_platform == 'win32'

+ 2 - 2
tests/async/tests.py

@@ -2,7 +2,7 @@ import asyncio
 import os
 from unittest import mock
 
-from asgiref.sync import async_to_sync
+from asgiref.sync import async_to_sync, iscoroutinefunction
 
 from django.core.cache import DEFAULT_CACHE_ALIAS, caches
 from django.core.exceptions import ImproperlyConfigured, SynchronousOnlyOperation
@@ -84,7 +84,7 @@ class ViewTests(SimpleTestCase):
             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)
+                self.assertIs(iscoroutinefunction(callback), is_async)
 
     def test_mixed_views_raise_error(self):
         class MixedView(View):

+ 3 - 4
tests/deprecation/test_middleware_mixin.py

@@ -1,7 +1,6 @@
-import asyncio
 import threading
 
-from asgiref.sync import async_to_sync
+from asgiref.sync import async_to_sync, iscoroutinefunction
 
 from django.contrib.admindocs.middleware import XViewMiddleware
 from django.contrib.auth.middleware import (
@@ -101,11 +100,11 @@ class MiddlewareMixinTests(SimpleTestCase):
                 # Middleware appears as coroutine if get_function is
                 # a coroutine.
                 middleware_instance = middleware(async_get_response)
-                self.assertIs(asyncio.iscoroutinefunction(middleware_instance), True)
+                self.assertIs(iscoroutinefunction(middleware_instance), True)
                 # Middleware doesn't appear as coroutine if get_function is not
                 # a coroutine.
                 middleware_instance = middleware(sync_get_response)
-                self.assertIs(asyncio.iscoroutinefunction(middleware_instance), False)
+                self.assertIs(iscoroutinefunction(middleware_instance), False)
 
     def test_sync_to_async_uses_base_thread_and_connection(self):
         """

+ 3 - 4
tests/middleware_exceptions/middleware.py

@@ -1,4 +1,4 @@
-import asyncio
+from asgiref.sync import iscoroutinefunction, markcoroutinefunction
 
 from django.http import Http404, HttpResponse
 from django.template import engines
@@ -15,9 +15,8 @@ log = []
 class BaseMiddleware:
     def __init__(self, get_response):
         self.get_response = get_response
-        if asyncio.iscoroutinefunction(self.get_response):
-            # Mark the class as async-capable.
-            self._is_coroutine = asyncio.coroutines._is_coroutine
+        if iscoroutinefunction(self.get_response):
+            markcoroutinefunction(self)
 
     def __call__(self, request):
         return self.get_response(request)

+ 1 - 1
tests/requirements/py3.txt

@@ -1,5 +1,5 @@
 aiosmtpd
-asgiref >= 3.5.2
+asgiref >= 3.6.0
 argon2-cffi >= 16.1.0
 backports.zoneinfo; python_version < '3.9'
 bcrypt