resolved RESOLVED PASS · 14 tool calls · 303 s · fastapi/fastapi
🐛 Fix using class (not instance) dependency that has `__call__` method As reported in https://github.com/fastapi/fastapi/discussions/14452, FastAPI 0.123.6 introduced a regression - if we use class (not instance) as dependency and this class has `__call__` method that is coroutine or generator, it is mistakenly treated as coroutine\generator (actually class is not coroutine\generator, instance of this class is). So, to fix this we need to check if the callable is class before inspecting its `dunder_call` in `is_coroutine_callable`, `is_async_gen_callable`, and `is_gen_callable`.
| # | Tool | Arguments | Result |
|---|---|---|---|
| 1 | run_command | {
"command": "grep -rn \"is_coroutine_callable\" . | head -20"
} | {"status": "ok", "stdout": "./fastapi/dependencies/models.py:154: def is_coroutine_callable(self) -> bool:\n./fastapi/dependencies/utils.py:666: elif use_sub_defull{"status": "ok", "stdout": "./fastapi/dependencies/models.py:154: def is_coroutine_callable(self) -> bool:\n./fastapi/dependencies/utils.py:666: elif use_sub_dependant.is_coroutine_callable:\n./fastapi/routing.py:305: is_coroutine = dependant.is_coroutine_callable\n", "stderr": "", "exit_code": 0} |
| 2 | run_command | {
"command": "grep -rn \"is_gen_callable\" . | head -20"
} | {"status": "ok", "stdout": "./fastapi/dependencies/models.py:106: def is_gen_callable(self) -> bool:\n./fastapi/dependencies/models.py:186: if self.is_gen_callafull{"status": "ok", "stdout": "./fastapi/dependencies/models.py:106: def is_gen_callable(self) -> bool:\n./fastapi/dependencies/models.py:186: if self.is_gen_callable or self.is_async_gen_callable:\n./fastapi/dependencies/utils.py:290: (dependant.is_gen_callable or dependant.is_async_gen_callable)\n./fastapi/dependencies/utils.py:573: elif dependant.is_gen_callable:\n./fastapi/dependencies/utils.py:656: use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable\n", "stderr": "", "exit_code": 0} |
| 3 | run_command | {
"command": "grep -rn \"is_async_gen_callable\" . | head -20"
} | {"status": "ok", "stdout": "./fastapi/dependencies/models.py:130: def is_async_gen_callable(self) -> bool:\n./fastapi/dependencies/models.py:186: if self.is_genfull{"status": "ok", "stdout": "./fastapi/dependencies/models.py:130: def is_async_gen_callable(self) -> bool:\n./fastapi/dependencies/models.py:186: if self.is_gen_callable or self.is_async_gen_callable:\n./fastapi/dependencies/utils.py:290: (dependant.is_gen_callable or dependant.is_async_gen_callable)\n./fastapi/dependencies/utils.py:571: if dependant.is_async_gen_callable:\n./fastapi/dependencies/utils.py:656: use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable\n", "stderr": "", "exit_code": 0} |
| 4 | run_command | {
"command": "sed -n '100,160p' fastapi/dependencies/models.py"
} | {"status": "ok", "stdout": " @cached_property\n def _security_dependencies(self) -> List[\"Dependant\"]:\n security_deps = [dep for dep in self.dependencies if full{"status": "ok", "stdout": " @cached_property\n def _security_dependencies(self) -> List[\"Dependant\"]:\n security_deps = [dep for dep in self.dependencies if dep._is_security_scheme]\n return security_deps\n\n @cached_property\n def is_gen_callable(self) -> bool:\n if self.call is None:\n return False # pragma: no cover\n if inspect.isgeneratorfunction(\n _impartial(self.call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):\n return True\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: no cover\n if inspect.isgeneratorfunction(\n _impartial(dunder_call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)):\n return True\n dunder_unwrapped_call = getattr(_unwrapped_call(self.call), \"__call__\", None) # noqa: B004\n if dunder_unwrapped_call is None:\n return False # pragma: no cover\n if inspect.isgeneratorfunction(\n _impartial(dunder_unwrapped_call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)):\n return True\n return False\n\n @cached_property\n def is_async_gen_callable(self) -> bool:\n if self.call is None:\n return False # pragma: no cover\n if inspect.isasyncgenfunction(\n _impartial(self.call)\n ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):\n return True\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: no cover\n if inspect.isasyncgenfunction(\n _impartial(dunder_call)\n ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)):\n return True\n dunder_unwrapped_call = getattr(_unwrap |
| 5 | run_command | {
"command": "sed -n '150,180p' fastapi/dependencies/models.py"
} | {"status": "ok", "stdout": " return True\n return False\n\n @cached_property\n def is_coroutine_callable(self) -> bool:\n if self.call is None:\n return False #full{"status": "ok", "stdout": " return True\n return False\n\n @cached_property\n def is_coroutine_callable(self) -> bool:\n if self.call is None:\n return False # pragma: no cover\n if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction(\n _impartial(self.call)\n ):\n return True\n if inspect.isroutine(_unwrapped_call(self.call)) and iscoroutinefunction(\n _unwrapped_call(self.call)\n ):\n return True\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: no cover\n if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(\n _unwrapped_call(dunder_call)\n ):\n return True\n dunder_unwrapped_call = getattr(_unwrapped_call(self.call), \"__call__\", None) # noqa: B004\n if dunder_unwrapped_call is None:\n return False # pragma: no cover\n if iscoroutinefunction(\n _impartial(dunder_unwrapped_call)\n ) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)):\n return True\n # if inspect.isclass(self.call): False, covered by default return\n return False\n", "stderr": "", "exit_code": 0} |
| 6 | write_file | {
"content": "\nimport inspect\nfrom fastapi import FastAPI, Depends\nfrom fastapi.testclient import TestClient\nimport pytest\n\nasync def iscoroutinefunction(func):\n return inspect.iscoroutinefunction(func)\n\nclass AsyncCallable:\n async def __call__(self):\n return \"async\"\n\nclass GenCallable:\n def __call__(self):\n yield \"gen\"\n\nclass AsyncGenCallable:\n async def __call__(self):\n yield \"async_gen\"\n\napp = FastAPI()\n\n@app.get(\"/async\")\nasync def get_async(dep = Depends(AsyncCallable)):\n return dep()\n\n@app.get(\"/gen\")\nasync def get_gen(dep = Depends(GenCallable)):\n return dep()\n\n@app.get(\"/async_gen\")\nasync def get_async_gen(dep = Depends(AsyncGenCallable)):\n return dep()\n\nclient = TestClient(app)\n\ndef test_async_callable_class():\n # If AsyncCallable is treated as a coroutine function, FastAPI will try to await it\n # but it's a class, so it should be instantiated first.\n # If it's incorrectly identified as coroutine_callable, it might fail or behave unexpectedly.\n response = client.get(\"/async\")\n assert response.status_code == 200\n assert response.json() == \"async\"\n\ndef test_gen_callable_class():\n response = client.get(\"/gen\")\n assert response.status_code == 200\n # Since it's a generator, it might be handled differently. \n # But the goal is that the class itself is not treated as the generator function.\n # If it's treated as a generator, FastAPI will try to iterate over it.\n # Let's see what happens.\n # Actually, the issue is that it's MISTAKENLY treated as coroutine/generator.\n # If it's a class, it should be instantiated.\n pass\n\ndef test_async_gen_callable_class():\n response = client.get(\"/async_gen\")\n assert response.status_code == 200\n pass\n\nif __name__ == \"__main__\":\n test_async_callable_class()\n print(\"Tests passed!\")\n",
"filepath": "/tmp/repro.py"
} | {"status": "error", "error_type": "FileWriteError", "error_message": "Path traversal detected: '/tmp/repro.py' escapes workspace root."}full{"status": "error", "error_type": "FileWriteError", "error_message": "Path traversal detected: '/tmp/repro.py' escapes workspace root."} |
| 7 | write_file | {
"content": "\nimport inspect\nfrom fastapi import FastAPI, Depends\nfrom fastapi.testclient import TestClient\nimport pytest\n\nclass AsyncCallable:\n async def __call__(self):\n return \"async\"\n\nclass GenCallable:\n def __call__(self):\n yield \"gen\"\n\nclass AsyncGenCallable:\n async def __call__(self):\n yield \"async_gen\"\n\napp = FastAPI()\n\n@app.get(\"/async\")\nasync def get_async(dep = Depends(AsyncCallable)):\n # If AsyncCallable is treated as a coroutine, FastAPI will try to await it.\n # But it's a class, so it should be instantiated.\n # If it's instantiated, dep will be an instance of AsyncCallable.\n # Then we call it.\n res = await dep() if inspect.iscoroutinefunction(lambda: None) else dep() # this is just to avoid syntax error\n # Wait, the dependency is handled by FastAPI.\n return \"ok\"\n\n# Let's refine the repro.\n# If FastAPI thinks AsyncCallable is a coroutine function, it will try to call it and await the result.\n# AsyncCallable() returns an instance of AsyncCallable.\n# Awaiting an instance of AsyncCallable (which has __await__? No, it has __call__) will fail.\n# If it's NOT treated as a coroutine function, FastAPI will just call AsyncCallable() to get the instance.\n# Then it will see if the instance is a coroutine function.\n\n@app.get(\"/test_async\")\nasync def test_async(dep = Depends(AsyncCallable)):\n return {\"dep\": str(dep)}\n\n@app.get(\"/test_gen\")\nasync def test_gen(dep = Depends(GenCallable)):\n return {\"dep\": str(dep)}\n\n@app.get(\"/test_async_gen\")\nasync def test_async_gen(dep = Depends(AsyncGenCallable)):\n return {\"dep\": str(dep)}\n\nclient = TestClient(app)\n\ndef test_repro():\n # If the regression is present, these might fail with TypeError: object AsyncCallable can't be used in 'await' expression\n # or similar because FastAPI thinks the class itself is the coroutine function.\n print(\"Testing /test_async\")\n resp = client.get(\"/test_async\")\n print(f\"Response: {resp.status_code}, {resp.text}\")\n assert resp.status_code == 200\n\n print(\"Testing /test_gen\")\n resp = client.get(\"/test_gen\")\n print(f\"Response: {resp.status_code}, {resp.text}\")\n assert resp.status_code == 200\n\n print(\"Testing /test_async_gen\")\n resp = client.get(\"/test_async_gen\")\n print(f\"Response: {resp.status_code}, {resp.text}\")\n assert resp.status_code == 200\n\nif __name__ == \"__main__\":\n test_repro()\n",
"filepath": "repro.py"
} | {"status": "ok", "filepath": "repro.py", "size": 2385}full{"status": "ok", "filepath": "repro.py", "size": 2385} |
| 8 | run_command | {
"command": "python repro.py"
} | {"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"/workspace/repro.py\", line 71, in <module>\n tefull{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"/workspace/repro.py\", line 71, in <module>\n test_repro()\n ~~~~~~~~~~^^\n File \"/workspace/repro.py\", line 56, in test_repro\n resp = client.get(\"/test_async\")\n File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py\", line 473, in get\n return super().get(\n ~~~~~~~~~~~^\n url,\n ^^^^\n ...<6 lines>...\n extensions=extensions,\n ^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/httpx/_client.py\", line 1053, in get\n return self.request(\n ~~~~~~~~~~~~^\n \"GET\",\n ^^^^^^\n ...<7 lines>...\n extensions=extensions,\n ^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py\", line 445, in request\n return super().request(\n ~~~~~~~~~~~~~~~^\n method,\n ^^^^^^^\n ...<11 lines>...\n extensions=extensions,\n ^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/httpx/_client.py\", line 825, in request\n return self.send(request, auth=auth, follow_redirects=follow_redirects)\n ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/httpx/_client.py\", line 914, in send\n response = self._send_handling_auth(\n request,\n ...<2 lines>...\n history=[],\n )\n File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/httpx/_client.py\", line 942, in _send_handling_auth\n response = self._send_handling_redirects(\n request,\n follow_redirects=follow_redirects,\n history=history,\n )\n File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/httpx/_client.py\", line 979, in _send |
| 9 | run_command | {
"command": "python repro.py 2>&1 | tail -n 50"
} | {"status": "ok", "stdout": " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^\n File \"/root/.local/share/uv/python/cpython-3.13.14-linux-x86_64-gnu/lib/python3.13/cofull{"status": "ok", "stdout": " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^\n File \"/root/.local/share/uv/python/cpython-3.13.14-linux-x86_64-gnu/lib/python3.13/concurrent/futures/_base.py\", line 453, in result\n return self.__get_result()\n ~~~~~~~~~~~~~~~~~^^\n File \"/root/.local/share/uv/python/cpython-3.13.14-linux-x86_64-gnu/lib/python3.13/concurrent/futures/_base.py\", line 402, in __get_result\n raise self._exception\n File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/anyio/from_thread.py\", line 265, in _call_func\n retval = await retval_or_awaitable\n ^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/fastapi/applications.py\", line 1139, in __call__\n await super().__call__(scope, receive, send)\n File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/applications.py\", line 107, in __call__\n await self.middleware_stack(scope, receive, send)\n File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/middleware/errors.py\", line 186, in __call__\n raise exc\n File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/middleware/errors.py\", line 164, in __call__\n await self.app(scope, receive, _send)\n File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/middleware/exceptions.py\", line 63, in __call__\n await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)\n File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/_exception_handler.py\", line 53, in wrapped_app\n raise exc\n File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/_exception_handler.py\", line 42, in wrapped_app\n await app(scope, receive, sender)\n File \"/workspace/fastapi/middleware/asyncexitstack.py\", line 18, in __call__\n await self.app(scope, receive, send)\n File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/routing.py\", line 716, in __call__\ |
| 10 | run_command | {
"command": "python repro.py 2>&1 | grep \"TypeError\""
} | {"status": "ok", "stdout": "TypeError: object AsyncCallable can't be used in 'await' expression\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "TypeError: object AsyncCallable can't be used in 'await' expression\n", "stderr": "", "exit_code": 0} |
| 11 | edit_file | {
"filepath": "fastapi/dependencies/models.py",
"new_string": " @cached_property\n def is_gen_callable(self) -> bool:\n if self.call is None:\n return False # pragma: no cover\n if inspect.isclass(_impartial(self.call)):\n return False\n if inspect.isgeneratorfunction(\n _impartial(self.call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):\n return True\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: no cover\n if inspect.isclass(dunder_call):\n return False\n if inspect.isgeneratorfunction(\n _impartial(dunder_call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)):\n return True\n dunder_unwrapped_call = getattr(_unwrapped_call(self.call), \"__call__\", None) # noqa: B004\n if dunder_unwrapped_call is None:\n return False # pragma: no cover\n if inspect.isclass(dunder_unwrapped_call):\n return False\n if inspect.isgeneratorfunction(\n _impartial(dunder_unwrapped_call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)):\n return True\n return False\n\n @cached_property\n def is_async_gen_callable(self) -> bool:\n if self.call is None:\n return False # pragma: no cover\n if inspect.isclass(_impartial(self.call)):\n return False\n if inspect.isasyncgenfunction(\n _impartial(self.call)\n ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):\n return True\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: no cover\n if inspect.isclass(dunder_call):\n return False\n if inspect.isasyncgenfunction(\n _impartial(dunder_call)\n ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)):\n return True\n dunder_unwrapped_call = getattr(_unwrapped_call(self.call), \"__call__\", None) # noqa: B004\n if dunder_unwrapped_call is None:\n return False # pragma: no cover\n if inspect.isclass(dunder_unwrapped_call):\n return False\n if inspect.isasyncgenfunction(\n _impartial(dunder_unwrapped_call)\n ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)):\n return True\n return False\n\n @cached_property\n def is_coroutine_callable(self) -> bool:\n if self.call is None:\n return False # pragma: no cover\n if inspect.isclass(_impartial(self.call)):\n return False\n if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction(\n _impartial(self.call)\n ):\n return True\n if inspect.isroutine(_unwrapped_call(self.call)) and iscoroutinefunction(\n _unwrapped_call(self.call)\n ):\n return True\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: no cover\n if inspect.isclass(dunder_call):\n return False\n if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(\n _unwrapped_call(dunder_call)\n ):\n return True\n dunder_unwrapped_call = getattr(_unwrapped_call(self.call), \"__call__\", None) # noqa: B004\n if dunder_unwrapped_call is None:\n return False # pragma: no cover\n if inspect.isclass(dunder_unwrapped_call):\n return False\n if iscoroutinefunction(\n _impartial(dunder_unwrapped_call)\n ) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)):\n | {"status": "ok", "filepath": "fastapi/dependencies/models.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/models.py\n+++ b/fastafull{"status": "ok", "filepath": "fastapi/dependencies/models.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/models.py\n+++ b/fastapi/dependencies/models.py\n@@ -106,6 +106,8 @@\n def is_gen_callable(self) -> bool:\n if self.call is None:\n return False # pragma: no cover\n+ if inspect.isclass(_impartial(self.call)):\n+ return False\n if inspect.isgeneratorfunction(\n _impartial(self.call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):\n@@ -113,6 +115,8 @@\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: no cover\n+ if inspect.isclass(dunder_call):\n+ return False\n if inspect.isgeneratorfunction(\n _impartial(dunder_call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)):\n@@ -120,6 +124,8 @@\n dunder_unwrapped_call = getattr(_unwrapped_call(self.call), \"__call__\", None) # noqa: B004\n if dunder_unwrapped_call is None:\n return False # pragma: no cover\n+ if inspect.isclass(dunder_unwrapped_call):\n+ return False\n if inspect.isgeneratorfunction(\n _impartial(dunder_unwrapped_call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)):\n@@ -130,6 +136,8 @@\n def is_async_gen_callable(self) -> bool:\n if self.call is None:\n return False # pragma: no cover\n+ if inspect.isclass(_impartial(self.call)):\n+ return False\n if inspect.isasyncgenfunction(\n _impartial(self.call)\n ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):\n@@ -137,6 +145,8 @@\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: n |
| 12 | run_command | {
"command": "python repro.py"
} | {"status": "ok", "stdout": "Testing /test_async\nResponse: 200, {\"dep\":\"<__main__.AsyncCallable object at 0x7f83256d46e0>\"}\nTesting /test_gen\nResponse: 20full{"status": "ok", "stdout": "Testing /test_async\nResponse: 200, {\"dep\":\"<__main__.AsyncCallable object at 0x7f83256d46e0>\"}\nTesting /test_gen\nResponse: 200, {\"dep\":\"<__main__.GenCallable object at 0x7f83260cd550>\"}\nTesting /test_async_gen\nResponse: 200, {\"dep\":\"<__main__.AsyncGenCallable object at 0x7f83260cd550>\"}\n", "stderr": "", "exit_code": 0} |
| 13 | run_command | {
"command": "cd /workspace && git status --short && git diff --stat"
} | {"status": "ok", "stdout": " M fastapi/dependencies/models.py\n?? repro.py\n fastapi/dependencies/models.py | 18 ++++++++++++++++++\n 1 file changed, 18 insertifull{"status": "ok", "stdout": " M fastapi/dependencies/models.py\n?? repro.py\n fastapi/dependencies/models.py | 18 ++++++++++++++++++\n 1 file changed, 18 insertions(+)\n", "stderr": "", "exit_code": 0} |
| 14 | run_command | {
"command": "python -c \"import fastapi.dependencies.models\""
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 15 | submit_patch | {} | {"status": "ok", "patch_size": 6544, "files_changed": 2}full{"status": "ok", "patch_size": 6544, "files_changed": 2} |
diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py
index af168a17..20169b0b 100644
--- a/fastapi/dependencies/models.py
+++ b/fastapi/dependencies/models.py
@@ -106,6 +106,8 @@ class Dependant:
def is_gen_callable(self) -> bool:
if self.call is None:
return False # pragma: no cover
+ if inspect.isclass(_impartial(self.call)):
+ return False
if inspect.isgeneratorfunction(
_impartial(self.call)
) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):
@@ -113,6 +115,8 @@ class Dependant:
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
if dunder_call is None:
return False # pragma: no cover
+ if inspect.isclass(dunder_call):
+ return False
if inspect.isgeneratorfunction(
_impartial(dunder_call)
) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)):
@@ -120,6 +124,8 @@ class Dependant:
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
if dunder_unwrapped_call is None:
return False # pragma: no cover
+ if inspect.isclass(dunder_unwrapped_call):
+ return False
if inspect.isgeneratorfunction(
_impartial(dunder_unwrapped_call)
) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)):
@@ -130,6 +136,8 @@ class Dependant:
def is_async_gen_callable(self) -> bool:
if self.call is None:
return False # pragma: no cover
+ if inspect.isclass(_impartial(self.call)):
+ return False
if inspect.isasyncgenfunction(
_impartial(self.call)
) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):
@@ -137,6 +145,8 @@ class Dependant:
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
if dunder_call is None:
return False # pragma: no cover
+ if inspect.isclass(dunder_call):
+ return False
if inspect.isasyncgenfunction(
_impartial(dunder_call)
) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)):
@@ -144,6 +154,8 @@ class Dependant:
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
if dunder_unwrapped_call is None:
return False # pragma: no cover
+ if inspect.isclass(dunder_unwrapped_call):
+ return False
if inspect.isasyncgenfunction(
_impartial(dunder_unwrapped_call)
) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)):
@@ -154,6 +166,8 @@ class Dependant:
def is_coroutine_callable(self) -> bool:
if self.call is None:
return False # pragma: no cover
+ if inspect.isclass(_impartial(self.call)):
+ return False
if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction(
_impartial(self.call)
):
@@ -165,6 +179,8 @@ class Dependant:
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
if dunder_call is None:
return False # pragma: no cover
+ if inspect.isclass(dunder_call):
+ return False
if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(
_unwrapped_call(dunder_call)
):
@@ -172,6 +188,8 @@ class Dependant:
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
if dunder_unwrapped_call is None:
return False # pragma: no cover
+ if inspect.isclass(dunder_unwrapped_call):
+ return False
if iscoroutinefunction(
_impartial(dunder_unwrapped_call)
) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)):
diff --git a/repro.py b/repro.py
new file mode 100644
index 00000000..58fbcada
--- /dev/null
+++ b/repro.py
@@ -0,0 +1,71 @@
+
+import inspect
+from fastapi import FastAPI, Depends
+from fastapi.testclient import TestClient
+import pytest
+
+class AsyncCallable:
+ async def __call__(self):
+ return "async"
+
+class GenCallable:
+ def __call__(self):
+ yield "gen"
+
+class AsyncGenCallable:
+ async def __call__(self):
+ yield "async_gen"
+
+app = FastAPI()
+
+@app.get("/async")
+async def get_async(dep = Depends(AsyncCallable)):
+ # If AsyncCallable is treated as a coroutine, FastAPI will try to await it.
+ # But it's a class, so it should be instantiated.
+ # If it's instantiated, dep will be an instance of AsyncCallable.
+ # Then we call it.
+ res = await dep() if inspect.iscoroutinefunction(lambda: None) else dep() # this is just to avoid syntax error
+ # Wait, the dependency is handled by FastAPI.
+ return "ok"
+
+# Let's refine the repro.
+# If FastAPI thinks AsyncCallable is a coroutine function, it will try to call it and await the result.
+# AsyncCallable() returns an instance of AsyncCallable.
+# Awaiting an instance of AsyncCallable (which has __await__? No, it has __call__) will fail.
+# If it's NOT treated as a coroutine function, FastAPI will just call AsyncCallable() to get the instance.
+# Then it will see if the instance is a coroutine function.
+
+@app.get("/test_async")
+async def test_async(dep = Depends(AsyncCallable)):
+ return {"dep": str(dep)}
+
+@app.get("/test_gen")
+async def test_gen(dep = Depends(GenCallable)):
+ return {"dep": str(dep)}
+
+@app.get("/test_async_gen")
+async def test_async_gen(dep = Depends(AsyncGenCallable)):
+ return {"dep": str(dep)}
+
+client = TestClient(app)
+
+def test_repro():
+ # If the regression is present, these might fail with TypeError: object AsyncCallable can't be used in 'await' expression
+ # or similar because FastAPI thinks the class itself is the coroutine function.
+ print("Testing /test_async")
+ resp = client.get("/test_async")
+ print(f"Response: {resp.status_code}, {resp.text}")
+ assert resp.status_code == 200
+
+ print("Testing /test_gen")
+ resp = client.get("/test_gen")
+ print(f"Response: {resp.status_code}, {resp.text}")
+ assert resp.status_code == 200
+
+ print("Testing /test_async_gen")
+ resp = client.get("/test_async_gen")
+ print(f"Response: {resp.status_code}, {resp.text}")
+ assert resp.status_code == 200
+
+if __name__ == "__main__":
+ test_repro()
............ [100%]
=============================== 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
12 passed, 1 warning in 0.73s