Skip to content

Commit

Permalink
Refs #34118 -- Adopted asgiref coroutine detection shims.
Browse files Browse the repository at this point in the history
Thanks to Mariusz Felisiak for review.
  • Loading branch information
carltongibson committed Dec 20, 2022
1 parent a09d39f commit 32d70b2
Show file tree
Hide file tree
Showing 15 changed files with 42 additions and 44 deletions.
12 changes: 6 additions & 6 deletions django/core/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
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
Expand Down Expand Up @@ -119,7 +119,7 @@ def adapt_method_mode(
- 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:
Expand Down Expand Up @@ -191,7 +191,7 @@ def _get_response(self, request):
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)
Expand Down Expand Up @@ -245,7 +245,7 @@ async def _get_response_async(self, request):
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
)
Expand Down Expand Up @@ -278,7 +278,7 @@ async def _get_response_async(self, request):
% (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(
Expand Down Expand Up @@ -346,7 +346,7 @@ def make_view_atomic(self, view):
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."
)
Expand Down
5 changes: 2 additions & 3 deletions django/core/handlers/exception.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down
5 changes: 2 additions & 3 deletions django/test/testcases.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
import difflib
import inspect
import json
Expand Down Expand Up @@ -26,7 +25,7 @@
)
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
Expand Down Expand Up @@ -401,7 +400,7 @@ def _setup_and_call(self, result, debug=False):
)

# Convert async test methods.
if asyncio.iscoroutinefunction(testMethod):
if iscoroutinefunction(testMethod):
setattr(self, self._testMethodName, async_to_sync(testMethod))

if not skipped:
Expand Down
5 changes: 3 additions & 2 deletions django/test/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
import collections
import logging
import os
Expand All @@ -14,6 +13,8 @@
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
Expand Down Expand Up @@ -440,7 +441,7 @@ def setUp(inner_self):
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)
Expand Down
11 changes: 4 additions & 7 deletions django/utils/deprecation.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -120,16 +119,14 @@ def _async_check(self):
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"):
Expand Down
9 changes: 5 additions & 4 deletions django/views/generic/base.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -68,8 +69,8 @@ def view_is_async(cls):
]
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."
Expand Down Expand Up @@ -117,7 +118,7 @@ def view(request, *args, **kwargs):

# 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

Expand Down
2 changes: 1 addition & 1 deletion docs/internals/contributing/writing-code/unit-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ dependencies:

* aiosmtpd_
* argon2-cffi_ 19.1.0+
* asgiref_ 3.5.2+ (required)
* asgiref_ 3.6.0+ (required)
* bcrypt_
* colorama_
* docutils_
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/4.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/topics/async.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions docs/topics/http/middleware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions tests/async/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
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
Expand Down Expand Up @@ -84,7 +84,7 @@ def test_views_are_correctly_marked(self):
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):
Expand Down
7 changes: 3 additions & 4 deletions tests/deprecation/test_middleware_mixin.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -101,11 +100,11 @@ def sync_get_response(request):
# 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):
"""
Expand Down
7 changes: 3 additions & 4 deletions tests/middleware_exceptions/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import asyncio
from asgiref.sync import iscoroutinefunction, markcoroutinefunction

from django.http import Http404, HttpResponse
from django.template import engines
Expand All @@ -15,9 +15,8 @@
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)
Expand Down
2 changes: 1 addition & 1 deletion tests/requirements/py3.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit 32d70b2

Please sign in to comment.