Refs #34118 -- Adopted asgiref coroutine detection shims.

Thanks to Mariusz Felisiak for review.
This commit is contained in:
Carlton Gibson 2022-12-20 11:10:48 +01:00 committed by GitHub
parent a09d39f286
commit 32d70b2f55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 42 additions and 44 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -312,7 +312,7 @@ If your middleware has both ``sync_capable = True`` and
``async_capable = True``, then Django will pass it the request without ``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 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 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 The ``django.utils.decorators`` module contains
:func:`~django.utils.decorators.sync_only_middleware`, :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:: 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 from django.utils.decorators import sync_and_async_middleware
@sync_and_async_middleware @sync_and_async_middleware
def simple_middleware(get_response): def simple_middleware(get_response):
# One-time configuration and initialization goes here. # One-time configuration and initialization goes here.
if asyncio.iscoroutinefunction(get_response): if iscoroutinefunction(get_response):
async def middleware(request): async def middleware(request):
# Do something here! # Do something here!
response = await get_response(request) response = await get_response(request)

View File

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

View File

@ -2,7 +2,7 @@ import asyncio
import os import os
from unittest import mock 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.cache import DEFAULT_CACHE_ALIAS, caches
from django.core.exceptions import ImproperlyConfigured, SynchronousOnlyOperation 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): with self.subTest(view_cls=view_cls, is_async=is_async):
self.assertIs(view_cls.view_is_async, is_async) self.assertIs(view_cls.view_is_async, is_async)
callback = view_cls.as_view() 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): def test_mixed_views_raise_error(self):
class MixedView(View): class MixedView(View):

View File

@ -1,7 +1,6 @@
import asyncio
import threading 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.admindocs.middleware import XViewMiddleware
from django.contrib.auth.middleware import ( from django.contrib.auth.middleware import (
@ -101,11 +100,11 @@ class MiddlewareMixinTests(SimpleTestCase):
# Middleware appears as coroutine if get_function is # Middleware appears as coroutine if get_function is
# a coroutine. # a coroutine.
middleware_instance = middleware(async_get_response) 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 # Middleware doesn't appear as coroutine if get_function is not
# a coroutine. # a coroutine.
middleware_instance = middleware(sync_get_response) 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): def test_sync_to_async_uses_base_thread_and_connection(self):
""" """

View File

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

View File

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