diff --git a/asgiref/compatibility.py b/asgiref/compatibility.py index eccaee0d..3a2a63e6 100644 --- a/asgiref/compatibility.py +++ b/asgiref/compatibility.py @@ -1,6 +1,7 @@ -import asyncio import inspect +from .sync import iscoroutinefunction + def is_double_callable(application): """ @@ -18,10 +19,10 @@ def is_double_callable(application): if hasattr(application, "__call__"): # We only check to see if its __call__ is a coroutine function - # if it's not, it still might be a coroutine function itself. - if asyncio.iscoroutinefunction(application.__call__): + if iscoroutinefunction(application.__call__): return False # Non-classes we just check directly - return not asyncio.iscoroutinefunction(application) + return not iscoroutinefunction(application) def double_to_single_callable(application): diff --git a/asgiref/sync.py b/asgiref/sync.py index 6a9bd1d6..6f9c7cc5 100644 --- a/asgiref/sync.py +++ b/asgiref/sync.py @@ -26,19 +26,41 @@ def _restore_context(context): cvar.set(context.get(cvar)) +# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for +# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker. +# The latter is replaced with the inspect.markcoroutinefunction decorator. +# Until 3.12 is the minimum supported Python version, provide a shim. +# Django 4.0 only supports 3.8+, so don't concern with the _or_partial backport. + +# Type hint: should be generic: whatever T it takes it returns. (Same id) +def markcoroutinefunction(func: Any) -> Any: + if hasattr(inspect, "markcoroutinefunction"): + return inspect.markcoroutinefunction(func) + else: + func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore + return func + + +def iscoroutinefunction(func: Any) -> bool: + if hasattr(inspect, "markcoroutinefunction"): + return inspect.iscoroutinefunction(func) + else: + return asyncio.iscoroutinefunction(func) + + def _iscoroutinefunction_or_partial(func: Any) -> bool: # Python < 3.8 does not correctly determine partially wrapped # coroutine functions are coroutine functions, hence the need for # this to exist. Code taken from CPython. if sys.version_info >= (3, 8): - return asyncio.iscoroutinefunction(func) + return iscoroutinefunction(func) else: while inspect.ismethod(func): func = func.__func__ while isinstance(func, functools.partial): func = func.func - return asyncio.iscoroutinefunction(func) + return iscoroutinefunction(func) class ThreadSensitiveContext: @@ -356,7 +378,7 @@ def __init__( self.func = func functools.update_wrapper(self, func) self._thread_sensitive = thread_sensitive - self._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore + markcoroutinefunction(self) if thread_sensitive and executor is not None: raise TypeError("executor must not be set when thread_sensitive is True") self._executor = executor diff --git a/tests/test_sync.py b/tests/test_sync.py index 022a1743..c46e9be8 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -10,7 +10,12 @@ import pytest -from asgiref.sync import ThreadSensitiveContext, async_to_sync, sync_to_async +from asgiref.sync import ( + ThreadSensitiveContext, + async_to_sync, + iscoroutinefunction, + sync_to_async, +) from asgiref.timeout import timeout @@ -645,8 +650,8 @@ def test_sync_to_async_detected_as_coroutinefunction(): def sync_func(): return - assert not asyncio.iscoroutinefunction(sync_to_async) - assert asyncio.iscoroutinefunction(sync_to_async(sync_func)) + assert not iscoroutinefunction(sync_to_async) + assert iscoroutinefunction(sync_to_async(sync_func)) async def async_process(queue):