Repository: agronholm/anyio

Issue:
Handle partial asyncio future callbacks
As discussed on gitter,

```
    def find_root_task() -> asyncio.Task:
        try:
            root_task = _root_task.get()
        except LookupError:
            for task in all_tasks():
                if task._callbacks:
                    for cb in _get_task_callbacks(task):
>                       if cb is _run_until_complete_cb or cb.__module__ == 'uvloop.loop':
E                       AttributeError: 'functools.partial' object has no attribute '__module__'
```

Full trace: https://pastebin.com/hhqMvhpT, It happens roughly 10% of the time I run my tests.

Triggered by: https://github.com/vinissimus/async-asgi-testclient/blob/f3243ea332b0a91f3990676aa850fdf68676ae93/async_asgi_testclient/utils.py#L75

```python
def create_monitored_task(coro, send):
    future = asyncio.ensure_future(coro)
    future.add_done_callback(partial(_callback, send))
    return future
```

Anyio needs to handle the case where future callbacks are partial objects, and probably any kind of callable.

Discussion:

Patch:
diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py
index 02053e4..7a7eaaf 100644
--- a/src/anyio/_backends/_asyncio.py
+++ b/src/anyio/_backends/_asyncio.py
@@ -141,7 +141,8 @@ def find_root_task() -> asyncio.Task:
         for task in all_tasks():
             if task._callbacks:
                 for cb in _get_task_callbacks(task):
-                    if cb is _run_until_complete_cb or cb.__module__ == 'uvloop.loop':
+                    if (cb is _run_until_complete_cb
+                            or getattr(cb, '__module__', None) == 'uvloop.loop'):
                         _root_task.set(task)
                         return task
 


Test patch:
diff --git a/.github/workflows/codeqa-test.yml b/.github/workflows/codeqa-test.yml
index 21f0b69..5d7df2f 100644
--- a/.github/workflows/codeqa-test.yml
+++ b/.github/workflows/codeqa-test.yml
@@ -25,7 +25,7 @@ jobs:
     - name: Check types with Mypy
       run: mypy src
     - name: Run isort
-      run: isort -c src tests
+      run: isort -c --diff src tests
 
   test:
     needs: [lint]
diff --git a/tests/test_to_thread.py b/tests/test_to_thread.py
index e39c052..a0e43f1 100644
--- a/tests/test_to_thread.py
+++ b/tests/test_to_thread.py
@@ -2,6 +2,7 @@ import asyncio
 import sys
 import threading
 import time
+from functools import partial
 
 import pytest
 
@@ -148,3 +149,21 @@ def test_asyncio_no_root_task(asyncio_event_loop):
         if t.name == 'AnyIO worker thread':
             t.join(2)
             assert not t.is_alive()
+
+
+def test_asyncio_future_callback_partial(asyncio_event_loop):
+    """
+    Regression test for #272.
+
+    Ensures that futures with partial callbacks are handled correctly when the root task
+    cannot be determined.
+    """
+    def func(future):
+        pass
+
+    async def sleep_sync():
+        return await to_thread.run_sync(time.sleep, 0)
+
+    task = asyncio_event_loop.create_task(sleep_sync())
+    task.add_done_callback(partial(func))
+    asyncio_event_loop.run_until_complete(task)