Repository: django/asgiref
Pull request #360 (2022-12-13 10:34:42)

Added coroutine detection shims for Python 3.12
Refs: https://github.com/python/cpython/pull/99247

This would be needed for Django 4.2, which would support PY312 on release of that.

Adds `markcoroutinefunction()` and `iscoroutinefunction` shims, and makes use of those internally. 

carltongibson: Lint failure is a pre-commit fetching from GitLab error. 
carltongibson: Ideally we could get this into an asgiref version for the 4.2a1 release, due Jan 16th. //cc @felixxm 
felixxm: > Ideally we could get this into an asgiref version for the 4.2a1 release, due Jan 16th. //cc @felixxm

:crossed_fingers: 
carltongibson: I really don't understand the CI
failure here. I'll have another look tomorrow 

Patch:
diff --git a/asgiref/compatibility.py b/asgiref/compatibility.py
index eccaee0..3a2a63e 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 6a9bd1d..6f9c7cc 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 @@ class SyncToAsync:
         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


Test patch:
diff --git a/tests/test_sync.py b/tests/test_sync.py
index 022a174..c46e9be 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -10,7 +10,12 @@ from unittest import TestCase
 
 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):