Repository: django/asgiref

Issue:
`sync.async_to_sync` incorrectly warns "a non-async-marked callable" for async callable class instance
Repro

```python
from asgiref.sync import async_to_sync


class CallableClass:
    async def __call__(self):
        return None
        
async_to_sync(CallableClass())
```

Yields the `UserWarning`

```
UserWarning: async_to_sync was passed a non-async-marked callable
```

Though the code works at runtime.

Discussion:

Patch:
diff --git a/asgiref/sync.py b/asgiref/sync.py
index a70dac1..d02bd4a 100644
--- a/asgiref/sync.py
+++ b/asgiref/sync.py
@@ -107,7 +107,12 @@ class AsyncToSync:
     loop_thread_executors: "Dict[asyncio.AbstractEventLoop, CurrentThreadExecutor]" = {}
 
     def __init__(self, awaitable, force_new_loop=False):
-        if not callable(awaitable) or not _iscoroutinefunction_or_partial(awaitable):
+        if not callable(awaitable) or (
+            not _iscoroutinefunction_or_partial(awaitable)
+            and not _iscoroutinefunction_or_partial(
+                getattr(awaitable, "__call__", awaitable)
+            )
+        ):
             # Python does not have very reliable detection of async functions
             # (lots of false negatives) so this is just a warning.
             warnings.warn(


Test patch:
diff --git a/tests/test_sync.py b/tests/test_sync.py
index 2837423..c5677fc 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -3,6 +3,7 @@ import functools
 import multiprocessing
 import threading
 import time
+import warnings
 from concurrent.futures import ThreadPoolExecutor
 from functools import wraps
 from unittest import TestCase
@@ -365,6 +366,29 @@ def test_async_to_sync_partial():
     assert result["worked"]
 
 
+def test_async_to_sync_on_callable_object():
+    """
+    Tests async_to_sync on a callable class instance
+    """
+
+    result = {}
+
+    class CallableClass:
+        async def __call__(self, value):
+            await asyncio.sleep(0)
+            result["worked"] = True
+            return value
+
+    # Run it (without warnings)
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")
+        sync_function = async_to_sync(CallableClass())
+        out = sync_function(42)
+
+    assert out == 42
+    assert result["worked"] is True
+
+
 def test_async_to_sync_method_self_attribute():
     """
     Tests async_to_sync on a method copies __self__.