Skip to content

Commit

Permalink
Added coroutine detection shims for Python 3.12 (#360)
Browse files Browse the repository at this point in the history
  • Loading branch information
carltongibson committed Dec 14, 2022
1 parent 467c154 commit 36f37c9
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 9 deletions.
7 changes: 4 additions & 3 deletions asgiref/compatibility.py
@@ -1,6 +1,7 @@
import asyncio
import inspect

from .sync import iscoroutinefunction


def is_double_callable(application):
"""
Expand All @@ -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):
Expand Down
28 changes: 25 additions & 3 deletions asgiref/sync.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions tests/test_sync.py
Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit 36f37c9

Please sign in to comment.