Repository: mirumee/ariadne
Pull request #1244 (2025-04-08 02:54:49)

check for async callable object and function for relay resolver
This PR ensures that the relay `resolve_wrapper` can handle async functions and objects with an async `__call__` method.

I ran into this issue trying to use the ariadne-auth `authz.require_permission` function alongside the connection class.
`require_permission` converts the decorated resolver into an instance of `PermissionChecker` which defines an `async __call__` and is not picked up by `inspect.iscoroutinefunction`.

The helper function was inspired by [starlette](https://github.com/encode/starlette/blob/cbddcaf59267d5a6e7bf10736cdd5af6048fe274/starlette/_utils.py#L35-L39).

For now, I only added the helper to the connection object, that said, many of the existing usages of `inspect.iscoroutinefunction` will want to move to this helper.


Patch:
diff --git a/ariadne/contrib/relay/objects.py b/ariadne/contrib/relay/objects.py
index 6788305..cd2795b 100644
--- a/ariadne/contrib/relay/objects.py
+++ b/ariadne/contrib/relay/objects.py
@@ -1,3 +1,4 @@
+from collections.abc import Awaitable
 from inspect import iscoroutinefunction
 from typing import Optional, cast
 
@@ -6,6 +7,7 @@ from graphql.pyutils import is_awaitable
 from graphql.type import GraphQLSchema
 
 from ariadne import InterfaceType, ObjectType
+from ariadne.contrib.relay import RelayConnection
 from ariadne.contrib.relay.arguments import (
     ConnectionArguments,
     ConnectionArgumentsTypeUnion,
@@ -16,7 +18,7 @@ from ariadne.contrib.relay.types import (
 )
 from ariadne.contrib.relay.utils import decode_global_id
 from ariadne.types import Resolver
-from ariadne.utils import type_get_extension, type_set_extension
+from ariadne.utils import is_async_callable, type_get_extension, type_set_extension
 
 
 class RelayObjectType(ObjectType):
@@ -33,14 +35,16 @@ class RelayObjectType(ObjectType):
     def resolve_wrapper(self, resolver: ConnectionResolver):
         def wrapper(obj, info, *args, **kwargs):
             connection_arguments = self.connection_arguments_class(**kwargs)
-            if iscoroutinefunction(resolver):
+            if is_async_callable(resolver):
 
                 async def async_my_extension():
-                    relay_connection = await resolver(
+                    relay_connection = resolver(
                         obj, info, connection_arguments, *args, **kwargs
                     )
                     if is_awaitable(relay_connection):
-                        relay_connection = await relay_connection
+                        relay_connection = await cast(
+                            Awaitable[RelayConnection], relay_connection
+                        )
                     return {
                         "totalCount": relay_connection.total,
                         "edges": relay_connection.get_edges(),
diff --git a/ariadne/utils.py b/ariadne/utils.py
index 9034b75..e2cc5d7 100644
--- a/ariadne/utils.py
+++ b/ariadne/utils.py
@@ -1,4 +1,5 @@
 import asyncio
+import inspect
 from collections.abc import Mapping
 from functools import wraps
 from typing import Any, Callable, Optional, Union, cast
@@ -264,3 +265,9 @@ def type_get_extension(
     object_type: GraphQLNamedType, extension_name: str, fallback: Any = None
 ) -> Any:
     return getattr(object_type, "extensions", {}).get(extension_name, fallback)
+
+
+def is_async_callable(obj: Any) -> bool:
+    return inspect.iscoroutinefunction(obj) or (
+        callable(obj) and inspect.iscoroutinefunction(obj.__call__)
+    )


Test patch:
diff --git a/tests/relay/test_objects.py b/tests/relay/test_objects.py
index 9d0d1e7..215ca8e 100644
--- a/tests/relay/test_objects.py
+++ b/tests/relay/test_objects.py
@@ -271,3 +271,28 @@ def test_relay_node_query_faction(
 
     assert result.errors is None
     assert result.data == {"node": {"bid": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
+
+
+@pytest.mark.asyncio
+async def test_relay_object_resolve_wrapper_awaitable(friends_connection):
+    class Resolver:
+        async def __call__(self, *_, **__):
+            return friends_connection
+
+    object_type = RelayObjectType("User")
+    wrapped_resolver = object_type.resolve_wrapper(Resolver())
+
+    result = await wrapped_resolver(None, None, first=10)
+    assert result == {
+        "totalCount": 2,
+        "edges": [
+            {"node": {"id": "VXNlcjox", "name": "Alice"}, "cursor": "VXNlcjox"},
+            {"node": {"id": "VXNlcjoy", "name": "Bob"}, "cursor": "VXNlcjoy"},
+        ],
+        "pageInfo": {
+            "hasNextPage": False,
+            "hasPreviousPage": False,
+            "startCursor": "VXNlcjox",
+            "endCursor": "VXNlcjoy",
+        },
+    }