diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index a934659186..8911543d4e 100644 --- a/django/core/handlers/base.py +++ b/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." ) diff --git a/django/core/handlers/exception.py b/django/core/handlers/exception.py index 79577c2d0a..a0b1ba678a 100644 --- a/django/core/handlers/exception.py +++ b/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): diff --git a/django/test/testcases.py b/django/test/testcases.py index c78c2300a7..090a31e7c4 100644 --- a/django/test/testcases.py +++ b/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: diff --git a/django/test/utils.py b/django/test/utils.py index 2b2b92c593..5e5649b0ac 100644 --- a/django/test/utils.py +++ b/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) diff --git a/django/utils/deprecation.py b/django/utils/deprecation.py index caed5b25d4..19379dfe58 100644 --- a/django/utils/deprecation.py +++ b/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"): diff --git a/django/views/generic/base.py b/django/views/generic/base.py index 3a3afb0c73..8f8f9397e8 100644 --- a/django/views/generic/base.py +++ b/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 diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 939abb5631..e9102110eb 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/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_ diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index 7e93df0702..7979e2359b 100644 --- a/docs/releases/4.2.txt +++ b/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 diff --git a/docs/topics/async.txt b/docs/topics/async.txt index a3cc77aeba..39ca864655 100644 --- a/docs/topics/async.txt +++ b/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, diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index 29f379889f..e1a3e95ebc 100644 --- a/docs/topics/http/middleware.txt +++ b/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) diff --git a/setup.cfg b/setup.cfg index cc511c96e6..afef79c2ab 100644 --- a/setup.cfg +++ b/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' diff --git a/tests/async/tests.py b/tests/async/tests.py index 559f21b8b1..6ca5c989b0 100644 --- a/tests/async/tests.py +++ b/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): diff --git a/tests/deprecation/test_middleware_mixin.py b/tests/deprecation/test_middleware_mixin.py index 060c2f5f35..3b6ad6d8ee 100644 --- a/tests/deprecation/test_middleware_mixin.py +++ b/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): """ diff --git a/tests/middleware_exceptions/middleware.py b/tests/middleware_exceptions/middleware.py index 5a0c82afb1..f50aa61327 100644 --- a/tests/middleware_exceptions/middleware.py +++ b/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) diff --git a/tests/requirements/py3.txt b/tests/requirements/py3.txt index 6a88d17a8f..89209ca9e6 100644 --- a/tests/requirements/py3.txt +++ b/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