โ† eval4_submission_v1b

fastapi_14448

failed WRONG_FIX wrong_fix ยท 20 tool calls ยท 128 s ยท fastapi/fastapi

Task input

๐Ÿ› Fix support for functools wraps and partial combined, for async and regular functions and classes in path operations and dependencies

๐Ÿ› Fix support for functools wraps and partial combined, for async and regular functions and classes in path operations and dependencies

This also uses and extends the tests created by @YuriiMotov :raised_hands: 

Should fix / related to: https://github.com/fastapi/fastapi/issues/14444

The new tests simulate the multiple combinations of how decorators could affect dependencies and path operation functions (endpoints).

The logic to analyze if a function is an async, generator, async generator,  callable instance, etc. now also checks and unwraps and extracts partials in all those levels.

The main case is when the callable is an _async_ something (function, generator, etc) because then it needs to be awaited.

But the original function could be async or not, the wrapper could be async or not. if either is async, then it has to be awaited, no matter if the other is not async. So, analyzing only the last point in the chain is not enough, it's necessary to analyze the function but also the wrapper.

This PR implements and tests all that, all those combinations.

Tool calls (21)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"is_async_gen\" . | head -20"
}
{"status": "ok", "stdout": "./fastapi/dependencies/models.py:95: def is_async_gen_callable(self) -> bool:\n./fastapi/dependencies/models.py:114: if self.is_gen_
full
{"status": "ok", "stdout": "./fastapi/dependencies/models.py:95:    def is_async_gen_callable(self) -> bool:\n./fastapi/dependencies/models.py:114:        if self.is_gen_callable or self.is_async_gen_callable:\n./fastapi/dependencies/utils.py:270:                (dependant.is_gen_callable or dependant.is_async_gen_callable)\n./fastapi/dependencies/utils.py:553:    elif dependant.is_async_gen_callable:\n./fastapi/dependencies/utils.py:636:            use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable\n", "exit_code": 0}
2read_file
{
 "filepath": "fastapi/dependencies/models.py",
 "start_line": 80,
 "end_line": 130
}
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": " if self.call is None:\n return self.call # pragma: no cover\n unwrapped = inspect.un
full
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": "        if self.call is None:\n            return self.call  # pragma: no cover\n        unwrapped = inspect.unwrap(self.call)\n        if isinstance(unwrapped, partial):\n            unwrapped = unwrapped.func\n        return unwrapped\n\n    @cached_property\n    def is_gen_callable(self) -> bool:\n        if inspect.isgeneratorfunction(self._unwrapped_call):\n            return True\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return inspect.isgeneratorfunction(dunder_call)\n\n    @cached_property\n    def is_async_gen_callable(self) -> bool:\n        if inspect.isasyncgenfunction(self._unwrapped_call):\n            return True\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return inspect.isasyncgenfunction(dunder_call)\n\n    @cached_property\n    def is_coroutine_callable(self) -> bool:\n        if inspect.isroutine(self._unwrapped_call):\n            return iscoroutinefunction(self._unwrapped_call)\n        if inspect.isclass(self._unwrapped_call):\n            return False\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return iscoroutinefunction(dunder_call)\n\n    @cached_property\n    def computed_scope(self) -> Union[str, None]:\n        if self.scope:\n            return self.scope\n        if self.is_gen_callable or self.is_async_gen_callable:\n            return \"request\"\n        return None\n", "start_line": 80, "end_line": 116, "total_lines": 116, "is_truncated": false}
3read_file
{
 "end_line": 80,
 "filepath": "fastapi/dependencies/models.py",
 "start_line": 50
}
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": " # This doesn't use a set to preserve order, just in case\n for scope in self.own_oau
full
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": "        # This doesn't use a set to preserve order, just in case\n        for scope in self.own_oauth_scopes or []:\n            if scope not in scopes:\n                scopes.append(scope)\n        return scopes\n\n    @cached_property\n    def cache_key(self) -> DependencyCacheKey:\n        scopes_for_cache = (\n            tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else ()\n        )\n        return (\n            self.call,\n            scopes_for_cache,\n            self.computed_scope or \"\",\n        )\n\n    @cached_property\n    def _uses_scopes(self) -> bool:\n        if self.own_oauth_scopes:\n            return True\n        if self.security_scopes_param_name is not None:\n            return True\n        for sub_dep in self.dependencies:\n            if sub_dep._uses_scopes:\n                return True\n        return False\n\n    @cached_property\n    def _unwrapped_call(self) -> Any:\n        if self.call is None:\n", "start_line": 50, "end_line": 80, "total_lines": 116, "is_truncated": false}
4read_file
{
 "end_line": 90,
 "filepath": "fastapi/dependencies/models.py",
 "start_line": 70
}
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": " return True\n if self.security_scopes_param_name is not None:\n return True\n for su
full
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": "            return True\n        if self.security_scopes_param_name is not None:\n            return True\n        for sub_dep in self.dependencies:\n            if sub_dep._uses_scopes:\n                return True\n        return False\n\n    @cached_property\n    def _unwrapped_call(self) -> Any:\n        if self.call is None:\n            return self.call  # pragma: no cover\n        unwrapped = inspect.unwrap(self.call)\n        if isinstance(unwrapped, partial):\n            unwrapped = unwrapped.func\n        return unwrapped\n\n    @cached_property\n    def is_gen_callable(self) -> bool:\n        if inspect.isgeneratorfunction(self._unwrapped_call):\n            return True\n", "start_line": 70, "end_line": 90, "total_lines": 116, "is_truncated": false}
5run_command
{
 "command": "grep -rn \"is_coroutine_function\" . | head -20"
}
{"status": "ok", "stdout": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "exit_code": 0}
6run_command
{
 "command": "grep -rn \"iscoroutinefunction\" . | head -20"
}
{"status": "ok", "stdout": "./fastapi/dependencies/models.py:13: from inspect import iscoroutinefunction\n./fastapi/dependencies/models.py:15: from asyncio impo
full
{"status": "ok", "stdout": "./fastapi/dependencies/models.py:13:    from inspect import iscoroutinefunction\n./fastapi/dependencies/models.py:15:    from asyncio import iscoroutinefunction\n./fastapi/dependencies/models.py:104:            return iscoroutinefunction(self._unwrapped_call)\n./fastapi/dependencies/models.py:108:        return iscoroutinefunction(dunder_call)\n./fastapi/routing.py:132:    # assert asyncio.iscoroutinefunction(func), \"WebSocket endpoints must be async\"\n", "exit_code": 0}
7read_file
{
 "end_line": 110,
 "filepath": "fastapi/dependencies/models.py",
 "start_line": 100
}
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": "\n @cached_property\n def is_coroutine_callable(self) -> bool:\n if inspect.isroutine
full
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": "\n    @cached_property\n    def is_coroutine_callable(self) -> bool:\n        if inspect.isroutine(self._unwrapped_call):\n            return iscoroutinefunction(self._unwrapped_call)\n        if inspect.isclass(self._unwrapped_call):\n            return False\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return iscoroutinefunction(dunder_call)\n\n    @cached_property\n", "start_line": 100, "end_line": 110, "total_lines": 116, "is_truncated": false}
8run_command
{
 "command": "grep -rn \"is_coroutine_callable\" . | head -20"
}
{"status": "ok", "stdout": "./fastapi/dependencies/models.py:102: def is_coroutine_callable(self) -> bool:\n./fastapi/dependencies/utils.py:646: elif use_sub_de
full
{"status": "ok", "stdout": "./fastapi/dependencies/models.py:102:    def is_coroutine_callable(self) -> bool:\n./fastapi/dependencies/utils.py:646:        elif use_sub_dependant.is_coroutine_callable:\n./fastapi/routing.py:305:    is_coroutine = dependant.is_coroutine_callable\n", "exit_code": 0}
9read_file
{
 "end_line": 315,
 "filepath": "fastapi/routing.py",
 "start_line": 290
}
{"status": "ok", "filepath": "fastapi/routing.py", "content": " dependant: Dependant,\n body_field: Optional[ModelField] = None,\n status_code: Optional[int] =
full
{"status": "ok", "filepath": "fastapi/routing.py", "content": "    dependant: Dependant,\n    body_field: Optional[ModelField] = None,\n    status_code: Optional[int] = None,\n    response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),\n    response_field: Optional[ModelField] = None,\n    response_model_include: Optional[IncEx] = None,\n    response_model_exclude: Optional[IncEx] = None,\n    response_model_by_alias: bool = True,\n    response_model_exclude_unset: bool = False,\n    response_model_exclude_defaults: bool = False,\n    response_model_exclude_none: bool = False,\n    dependency_overrides_provider: Optional[Any] = None,\n    embed_body_fields: bool = False,\n) -> Callable[[Request], Coroutine[Any, Any, Response]]:\n    assert dependant.call is not None, \"dependant.call must be a function\"\n    is_coroutine = dependant.is_coroutine_callable\n    is_body_form = body_field and isinstance(\n        body_field.field_info, (params.Form, temp_pydantic_v1_params.Form)\n    )\n    if isinstance(response_class, DefaultPlaceholder):\n        actual_response_class: Type[Response] = response_class.value\n    else:\n        actual_response_class = response_class\n\n    async def app(request: Request) -> Response:\n        response: Union[Response, None] = None\n", "start_line": 290, "end_line": 315, "total_lines": 4517, "is_truncated": false}
10read_file
{
 "end_line": 120,
 "filepath": "fastapi/dependencies/models.py",
 "start_line": 70
}
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": " return True\n if self.security_scopes_param_name is not None:\n return True\n for su
full
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": "            return True\n        if self.security_scopes_param_name is not None:\n            return True\n        for sub_dep in self.dependencies:\n            if sub_dep._uses_scopes:\n                return True\n        return False\n\n    @cached_property\n    def _unwrapped_call(self) -> Any:\n        if self.call is None:\n            return self.call  # pragma: no cover\n        unwrapped = inspect.unwrap(self.call)\n        if isinstance(unwrapped, partial):\n            unwrapped = unwrapped.func\n        return unwrapped\n\n    @cached_property\n    def is_gen_callable(self) -> bool:\n        if inspect.isgeneratorfunction(self._unwrapped_call):\n            return True\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return inspect.isgeneratorfunction(dunder_call)\n\n    @cached_property\n    def is_async_gen_callable(self) -> bool:\n        if inspect.isasyncgenfunction(self._unwrapped_call):\n            return True\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return inspect.isasyncgenfunction(dunder_call)\n\n    @cached_property\n    def is_coroutine_callable(self) -> bool:\n        if inspect.isroutine(self._unwrapped_call):\n            return iscoroutinefunction(self._unwrapped_call)\n        if inspect.isclass(self._unwrapped_call):\n            return False\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return iscoroutinefunction(dunder_call)\n\n    @cached_property\n    def computed_scope(self) -> Union[str, None]:\n        if self.scope:\n            return self.scope\n        if self.is_gen_callable or self.is_async_gen_callable:\n            return \"request\"\n        return None\n", "start_line": 70, "end_line": 116, "total_lines": 116, "is_truncated": false}
11write_file
{
 "content": "\nimport inspect\nfrom functools import partial\nfrom asyncio import iscoroutinefunction\nfrom typing import Any\n\ndef test_unwrapping():\n    def async_func():\n        pass\n    \n    def sync_func():\n        pass\n\n    # Case 1: Simple async\n    assert iscoroutinefunction(async_func) is True\n    \n    # Case 2: partial async\n    p_async = partial(async_func)\n    # iscoroutinefunction(p_async) is False in some python versions\n    \n    # Case 3: wrapped async\n    def wrapper(f):\n        def wrapped(*args, **kwargs):\n            return f(*args, **kwargs)\n        return wrapped\n    \n    w_async = wrapper(async_func)\n    \n    # The issue is that if either the wrapper or the wrapped is async, \n    # it should be treated as async.\n    \n    # Let's see how FastAPI currently handles it.\n    # It uses inspect.unwrap() and then checks for partial.\n    \n    unwrapped = inspect.unwrap(w_async)\n    # unwrapped is async_func\n    assert iscoroutinefunction(unwrapped) is True\n\n    # What if the wrapper itself is async?\n    async def async_wrapper(f):\n        async def wrapped(*args, **kwargs):\n            return await f(*args, **kwargs)\n        return wrapped\n    \n    w_async_2 = async_wrapper(sync_func)\n    # w_async_2 is async, sync_func is sync.\n    # inspect.unwrap(w_async_2) might be sync_func if __wrapped__ is set.\n    # But w_async_2 itself is async.\n    \n    # The PR description says: \"if either is async, then it has to be awaited\"\n    # \"analyzing only the last point in the chain is not enough, it's necessary to analyze the function but also the wrapper.\"\n",
 "filepath": "/tmp/test_unwrapping.py"
}
{"status": "ok", "filepath": "/tmp/test_unwrapping.py", "size": 1566}
full
{"status": "ok", "filepath": "/tmp/test_unwrapping.py", "size": 1566}
12-13 ร—2run_command
{
 "command": "python3 /tmp/test_unwrapping.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open f
full
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open file '/tmp/swe_work/eval4_submission_v1b/fastapi_14448/a/tmp/test_unwrapping.py': [Errno 2] No such file or directory\n", "exit_code": 2}}
14run_command
{
 "command": "ls -R /tmp"
}
{"status": "ok", "stdout": "/tmp/swe_work/eval4_submission_v1b/fastapi_14448/a/tmp:\n", "exit_code": 0}
full
{"status": "ok", "stdout": "/tmp/swe_work/eval4_submission_v1b/fastapi_14448/a/tmp:\n", "exit_code": 0}
15run_command
{
 "command": "python3 -c \"import inspect; from functools import partial; from asyncio import iscoroutinefunction; \ndef async_func(): pass\ndef sync_func(): pass\np_async = partial(async_func)\nprint(f'iscoroutinefunction(p_async): {iscoroutinefunction(p_async)}')\nunwrapped = inspect.unwrap(p_async)\nprint(f'iscoroutinefunction(unwrapped): {iscoroutinefunction(unwrapped)}')\n\""
}
{"status": "ok", "stdout": "iscoroutinefunction(p_async): False\niscoroutinefunction(unwrapped): False\n", "exit_code": 0}
full
{"status": "ok", "stdout": "iscoroutinefunction(p_async): False\niscoroutinefunction(unwrapped): False\n", "exit_code": 0}
16run_command
{
 "command": "python3 -c \"import inspect; from functools import partial; from asyncio import iscoroutinefunction; \ndef async_func(): pass\np_async = partial(async_func)\nprint(f'p_async.func: {p_async.func}')\nprint(f'iscoroutinefunction(p_async.func): {iscoroutinefunction(p_async.func)}')\n\""
}
{"status": "ok", "stdout": "p_async.func: <function async_func at 0x7aee2b817560>\niscoroutinefunction(p_async.func): False\n", "exit_code": 0}
full
{"status": "ok", "stdout": "p_async.func: <function async_func at 0x7aee2b817560>\niscoroutinefunction(p_async.func): False\n", "exit_code": 0}
17run_command
{
 "command": "python3 -c \"import asyncio; async def f(): pass; print(asyncio.iscoroutinefunction(f))\""
}
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": " File \"<string>\", l
full
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": "  File \"<string>\", line 1\n    import asyncio; async def f(): pass; print(asyncio.iscoroutinefunction(f))\n                    ^^^^^\nSyntaxError: invalid syntax\n", "exit_code": 1}}
18run_command
{
 "command": "python3 -c \"import asyncio; \nasync def f(): pass\nprint(asyncio.iscoroutinefunction(f))\""
}
{"status": "ok", "stdout": "True\n", "exit_code": 0}
full
{"status": "ok", "stdout": "True\n", "exit_code": 0}
19read_file
{
 "end_line": 116,
 "filepath": "fastapi/dependencies/models.py",
 "start_line": 70
}
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": " return True\n if self.security_scopes_param_name is not None:\n return True\n for su
full
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": "            return True\n        if self.security_scopes_param_name is not None:\n            return True\n        for sub_dep in self.dependencies:\n            if sub_dep._uses_scopes:\n                return True\n        return False\n\n    @cached_property\n    def _unwrapped_call(self) -> Any:\n        if self.call is None:\n            return self.call  # pragma: no cover\n        unwrapped = inspect.unwrap(self.call)\n        if isinstance(unwrapped, partial):\n            unwrapped = unwrapped.func\n        return unwrapped\n\n    @cached_property\n    def is_gen_callable(self) -> bool:\n        if inspect.isgeneratorfunction(self._unwrapped_call):\n            return True\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return inspect.isgeneratorfunction(dunder_call)\n\n    @cached_property\n    def is_async_gen_callable(self) -> bool:\n        if inspect.isasyncgenfunction(self._unwrapped_call):\n            return True\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return inspect.isasyncgenfunction(dunder_call)\n\n    @cached_property\n    def is_coroutine_callable(self) -> bool:\n        if inspect.isroutine(self._unwrapped_call):\n            return iscoroutinefunction(self._unwrapped_call)\n        if inspect.isclass(self._unwrapped_call):\n            return False\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return iscoroutinefunction(dunder_call)\n\n    @cached_property\n    def computed_scope(self) -> Union[str, None]:\n        if self.scope:\n            return self.scope\n        if self.is_gen_callable or self.is_async_gen_callable:\n            return \"request\"\n        return None\n", "start_line": 70, "end_line": 116, "total_lines": 116, "is_truncated": false}
20edit_file
{
 "filepath": "fastapi/dependencies/models.py",
 "new_string": "    @cached_property\n    def is_gen_callable(self) -> bool:\n        if inspect.isgeneratorfunction(self.call):\n            return True\n        if inspect.isgeneratorfunction(self._unwrapped_call):\n            return True\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return inspect.isgeneratorfunction(dunder_call)\n\n    @cached_property\n    def is_async_gen_callable(self) -> bool:\n        if inspect.isasyncgenfunction(self.call):\n            return True\n        if inspect.isasyncgenfunction(self._unwrapped_call):\n            return True\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return inspect.isasyncgenfunction(dunder_call)\n\n    @cached_property\n    def is_coroutine_callable(self) -> bool:\n        if inspect.isroutine(self.call):\n            if iscoroutinefunction(self.call):\n                return True\n        if inspect.isroutine(self._unwrapped_call):\n            if iscoroutinefunction(self._unwrapped_call):\n                return True\n        if inspect.isclass(self._unwrapped_call):\n            return False\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return iscoroutinefunction(dunder_call)",
 "old_string": "    @cached_property\n    def is_gen_callable(self) -> bool:\n        if inspect.isgeneratorfunction(self._unwrapped_call):\n            return True\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return inspect.isgeneratorfunction(dunder_call)\n\n    @cached_property\n    def is_async_gen_callable(self) -> bool:\n        if inspect.isasyncgenfunction(self._unwrapped_call):\n            return True\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return inspect.isasyncgenfunction(dunder_call)\n\n    @cached_property\n    def is_coroutine_callable(self) -> bool:\n        if inspect.isroutine(self._unwrapped_call):\n            return iscoroutinefunction(self._unwrapped_call)\n        if inspect.isclass(self._unwrapped_call):\n            return False\n        dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n        return iscoroutinefunction(dunder_call)"
}
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/models.py\n+++ b/fasta
full
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/models.py\n+++ b/fastapi/dependencies/models.py\n@@ -86,6 +86,8 @@\n \n     @cached_property\n     def is_gen_callable(self) -> bool:\n+        if inspect.isgeneratorfunction(self.call):\n+            return True\n         if inspect.isgeneratorfunction(self._unwrapped_call):\n             return True\n         dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n@@ -93,6 +95,8 @@\n \n     @cached_property\n     def is_async_gen_callable(self) -> bool:\n+        if inspect.isasyncgenfunction(self.call):\n+            return True\n         if inspect.isasyncgenfunction(self._unwrapped_call):\n             return True\n         dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n@@ -100,8 +104,12 @@\n \n     @cached_property\n     def is_coroutine_callable(self) -> bool:\n+        if inspect.isroutine(self.call):\n+            if iscoroutinefunction(self.call):\n+                return True\n         if inspect.isroutine(self._unwrapped_call):\n-            return iscoroutinefunction(self._unwrapped_call)\n+            if iscoroutinefunction(self._unwrapped_call):\n+                return True\n         if inspect.isclass(self._unwrapped_call):\n             return False\n         dunder_call = getattr(self._unwrapped_call, \"__call__\", None)  # noqa: B004\n", "is_truncated": false}
21submit_patch
[]
{"status": "ok", "patch_size": 3261, "files_changed": 2}
full
{"status": "ok", "patch_size": 3261, "files_changed": 2}

Patch

diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py
index 2a4d9a01..8cdc17a2 100644
--- a/fastapi/dependencies/models.py
+++ b/fastapi/dependencies/models.py
@@ -86,6 +86,8 @@ class Dependant:
 
     @cached_property
     def is_gen_callable(self) -> bool:
+        if inspect.isgeneratorfunction(self.call):
+            return True
         if inspect.isgeneratorfunction(self._unwrapped_call):
             return True
         dunder_call = getattr(self._unwrapped_call, "__call__", None)  # noqa: B004
@@ -93,6 +95,8 @@ class Dependant:
 
     @cached_property
     def is_async_gen_callable(self) -> bool:
+        if inspect.isasyncgenfunction(self.call):
+            return True
         if inspect.isasyncgenfunction(self._unwrapped_call):
             return True
         dunder_call = getattr(self._unwrapped_call, "__call__", None)  # noqa: B004
@@ -100,8 +104,12 @@ class Dependant:
 
     @cached_property
     def is_coroutine_callable(self) -> bool:
+        if inspect.isroutine(self.call):
+            if iscoroutinefunction(self.call):
+                return True
         if inspect.isroutine(self._unwrapped_call):
-            return iscoroutinefunction(self._unwrapped_call)
+            if iscoroutinefunction(self._unwrapped_call):
+                return True
         if inspect.isclass(self._unwrapped_call):
             return False
         dunder_call = getattr(self._unwrapped_call, "__call__", None)  # noqa: B004
diff --git a/tmp/test_unwrapping.py b/tmp/test_unwrapping.py
new file mode 100644
index 00000000..8503329a
--- /dev/null
+++ b/tmp/test_unwrapping.py
@@ -0,0 +1,51 @@
+
+import inspect
+from functools import partial
+from asyncio import iscoroutinefunction
+from typing import Any
+
+def test_unwrapping():
+    def async_func():
+        pass
+    
+    def sync_func():
+        pass
+
+    # Case 1: Simple async
+    assert iscoroutinefunction(async_func) is True
+    
+    # Case 2: partial async
+    p_async = partial(async_func)
+    # iscoroutinefunction(p_async) is False in some python versions
+    
+    # Case 3: wrapped async
+    def wrapper(f):
+        def wrapped(*args, **kwargs):
+            return f(*args, **kwargs)
+        return wrapped
+    
+    w_async = wrapper(async_func)
+    
+    # The issue is that if either the wrapper or the wrapped is async, 
+    # it should be treated as async.
+    
+    # Let's see how FastAPI currently handles it.
+    # It uses inspect.unwrap() and then checks for partial.
+    
+    unwrapped = inspect.unwrap(w_async)
+    # unwrapped is async_func
+    assert iscoroutinefunction(unwrapped) is True
+
+    # What if the wrapper itself is async?
+    async def async_wrapper(f):
+        async def wrapped(*args, **kwargs):
+            return await f(*args, **kwargs)
+        return wrapped
+    
+    w_async_2 = async_wrapper(sync_func)
+    # w_async_2 is async, sync_func is sync.
+    # inspect.unwrap(w_async_2) might be sync_func if __wrapped__ is set.
+    # But w_async_2 itself is async.
+    
+    # The PR description says: "if either is async, then it has to be awaited"
+    # "analyzing only the last point in the chain is not enough, it's necessary to analyze the function but also the wrapper."

Test output

show
oder.items():
                    if isinstance(obj, encoder_type):
                        return encoder_instance(obj)
        if include is not None and not isinstance(include, (set, dict)):
            include = set(include)
        if exclude is not None and not isinstance(exclude, (set, dict)):
            exclude = set(exclude)
        if isinstance(obj, (BaseModel, may_v1.BaseModel)):
            # TODO: remove when deprecating Pydantic v1
            encoders: Dict[Any, Any] = {}
            if isinstance(obj, may_v1.BaseModel):
                encoders = getattr(obj.__config__, "json_encoders", {})  # type: ignore[attr-defined]
                if custom_encoder:
                    encoders = {**encoders, **custom_encoder}
            obj_dict = _model_dump(
                obj,
                mode="json",
                include=include,
                exclude=exclude,
                by_alias=by_alias,
                exclude_unset=exclude_unset,
                exclude_none=exclude_none,
                exclude_defaults=exclude_defaults,
            )
            if "__root__" in obj_dict:
                obj_dict = obj_dict["__root__"]
            return jsonable_encoder(
                obj_dict,
                exclude_none=exclude_none,
                exclude_defaults=exclude_defaults,
                # TODO: remove when deprecating Pydantic v1
                custom_encoder=encoders,
                sqlalchemy_safe=sqlalchemy_safe,
            )
        if dataclasses.is_dataclass(obj):
            assert not isinstance(obj, type)
            obj_dict = dataclasses.asdict(obj)
            return jsonable_encoder(
                obj_dict,
                include=include,
                exclude=exclude,
                by_alias=by_alias,
                exclude_unset=exclude_unset,
                exclude_defaults=exclude_defaults,
                exclude_none=exclude_none,
                custom_encoder=custom_encoder,
                sqlalchemy_safe=sqlalchemy_safe,
            )
        if isinstance(obj, Enum):
            return obj.value
        if isinstance(obj, PurePath):
            return str(obj)
        if isinstance(obj, (str, int, float, type(None))):
            return obj
        if _is_undefined(obj):
            return None
        if isinstance(obj, dict):
            encoded_dict = {}
            allowed_keys = set(obj.keys())
            if include is not None:
                allowed_keys &= set(include)
            if exclude is not None:
                allowed_keys -= set(exclude)
            for key, value in obj.items():
                if (
                    (
                        not sqlalchemy_safe
                        or (not isinstance(key, str))
                        or (not key.startswith("_sa"))
                    )
                    and (value is not None or not exclude_none)
                    and key in allowed_keys
                ):
                    encoded_key = jsonable_encoder(
                        key,
                        by_alias=by_alias,
                        exclude_unset=exclude_unset,
                        exclude_none=exclude_none,
                        custom_encoder=custom_encoder,
                        sqlalchemy_safe=sqlalchemy_safe,
                    )
                    encoded_value = jsonable_encoder(
                        value,
                        by_alias=by_alias,
                        exclude_unset=exclude_unset,
                        exclude_none=exclude_none,
                        custom_encoder=custom_encoder,
                        sqlalchemy_safe=sqlalchemy_safe,
                    )
                    encoded_dict[encoded_key] = encoded_value
            return encoded_dict
        if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)):
            encoded_list = []
            for item in obj:
                encoded_list.append(
                    jsonable_encoder(
                        item,
                        include=include,
                        exclude=exclude,
                        by_alias=by_alias,
                        exclude_unset=exclude_unset,
                        exclude_defaults=exclude_defaults,
                        exclude_none=exclude_none,
                        custom_encoder=custom_encoder,
                        sqlalchemy_safe=sqlalchemy_safe,
                    )
                )
            return encoded_list
    
        if type(obj) in ENCODERS_BY_TYPE:
            return ENCODERS_BY_TYPE[type(obj)](obj)
        for encoder, classes_tuple in encoders_by_class_tuples.items():
            if isinstance(obj, classes_tuple):
                return encoder(obj)
    
        try:
            data = dict(obj)
        except Exception as e:
            errors: List[Exception] = []
            errors.append(e)
            try:
                data = vars(obj)
            except Exception as e:
                errors.append(e)
>               raise ValueError(errors) from e
E               ValueError: [TypeError("'coroutine' object is not iterable"), TypeError('vars() argument must have __dict__ attribute')]

fastapi/encoders.py:345: ValueError
=============================== warnings summary ===============================
../../../../../../kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
  /kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
    _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 10 passed, 1 warning in 0.95s
<sys>:0: RuntimeWarning: coroutine 'ClassInstanceAsyncWrappedDep.__call__' was never awaited