Repository: django/asgiref

Issue:
Prevent accidental mixup of sync_to_async / async_to_sync
Putting `sync_to_async` onto an `async def` silently breaks it. Instead, it would be good if that raised an error at decorator application time - and likewise for the opposite case.

Discussion:

Patch:
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 3c617e8..e4ed96f 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,3 +1,9 @@
+Pending
+-------
+
+* async_to_sync and sync_to_async now check their arguments are functions of
+  the correct type.
+
 3.3.1 (2020-11-09)
 ------------------
 
diff --git a/asgiref/sync.py b/asgiref/sync.py
index 869fb48..a8d91ea 100644
--- a/asgiref/sync.py
+++ b/asgiref/sync.py
@@ -101,6 +101,8 @@ class AsyncToSync:
     executors = Local()
 
     def __init__(self, awaitable, force_new_loop=False):
+        if not callable(awaitable) or not asyncio.iscoroutinefunction(awaitable):
+            raise TypeError("async_to_sync can only be applied to async functions.")
         self.awaitable = awaitable
         try:
             self.__self__ = self.awaitable.__self__
@@ -325,6 +327,8 @@ class SyncToAsync:
     )
 
     def __init__(self, func, thread_sensitive=True):
+        if not callable(func) or asyncio.iscoroutinefunction(func):
+            raise TypeError("sync_to_async can only be applied to sync functions.")
         self.func = func
         functools.update_wrapper(self, func)
         self._thread_sensitive = thread_sensitive


Test patch:
diff --git a/tests/test_sync.py b/tests/test_sync.py
index 5e9c171..5f9d9ec 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -45,6 +45,34 @@ async def test_sync_to_async():
         loop.set_default_executor(old_executor)
 
 
+def test_sync_to_async_fail_non_function():
+    """
+    async_to_sync raises a TypeError when called with a non-function.
+    """
+    with pytest.raises(TypeError) as excinfo:
+        sync_to_async(1)
+
+    assert excinfo.value.args == (
+        "sync_to_async can only be applied to sync functions.",
+    )
+
+
+@pytest.mark.asyncio
+async def test_sync_to_async_fail_async():
+    """
+    sync_to_async raises a TypeError when applied to a sync function.
+    """
+    with pytest.raises(TypeError) as excinfo:
+
+        @sync_to_async
+        async def test_function():
+            pass
+
+    assert excinfo.value.args == (
+        "sync_to_async can only be applied to sync functions.",
+    )
+
+
 @pytest.mark.asyncio
 async def test_sync_to_async_decorator():
     """
@@ -152,6 +180,33 @@ async def test_async_to_sync_to_async():
     assert result["thread"] == threading.current_thread()
 
 
+def test_async_to_sync_fail_non_function():
+    """
+    async_to_sync raises a TypeError when applied to a non-function.
+    """
+    with pytest.raises(TypeError) as excinfo:
+        async_to_sync(1)
+
+    assert excinfo.value.args == (
+        "async_to_sync can only be applied to async functions.",
+    )
+
+
+def test_async_to_sync_fail_sync():
+    """
+    async_to_sync raises a TypeError when applied to a sync function.
+    """
+    with pytest.raises(TypeError) as excinfo:
+
+        @async_to_sync
+        def test_function(self):
+            pass
+
+    assert excinfo.value.args == (
+        "async_to_sync can only be applied to async functions.",
+    )
+
+
 def test_async_to_sync():
     """
     Tests we can call async_to_sync outside of an outer event loop.