resolved RESOLVED PASS · 7 tool calls · 89 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 -nE \"is_coroutine_callable|is_async_gen_callable|is_gen_callable\" fastapi/dependencies/models.py"
} | {"status": "ok", "stdout": "106: def is_gen_callable(self) -> bool:\n130: def is_async_gen_callable(self) -> bool:\n154: def is_coroutine_callable(self) -> boolfull{"status": "ok", "stdout": "106: def is_gen_callable(self) -> bool:\n130: def is_async_gen_callable(self) -> bool:\n154: def is_coroutine_callable(self) -> bool:\n186: if self.is_gen_callable or self.is_async_gen_callable:\n", "stderr": "", "exit_code": 0} |
| 3 | run_command | {
"command": "sed -n '100,180p' 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 |
| 4 | run_command | {
"command": "cat <<EOF > /tmp/repro.py\nimport inspect\nfrom fastapi import FastAPI, Depends\nfrom fastapi.dependencies.models import Dependant\nfrom typing import Annotated\n\nclass MyDep:\n async def __call__(self):\n return \"hello\"\n\napp = FastAPI()\n\n@app.get(\"/\")\nasync def root(dep: Annotated[str, Depends(MyDep)]):\n return dep\n\n# We want to check if MyDep (the class) is considered a coroutine callable\n# In the current buggy version, it might be.\n\n# To simulate how FastAPI does it:\n# We need to create a Dependant object.\n# Dependant(call=MyDep, ...)\nfrom fastapi.dependencies.models import Dependant\n\ndep_model = Dependant(call=MyDep)\nprint(f\"is_gen_callable: {dep_model.is_gen_callable}\")\nprint(f\"is_async_gen_callable: {dep_model.is_async_gen_callable}\")\nprint(f\"is_coroutine_callable: {dep_model.is_coroutine_callable}\")\n\n# Expected: all should be False because MyDep is a class. \n# The instance of MyDep is a coroutine, but the class itself is not.\nEOF\npython3 /tmp/repro.py"
} | {"status": "ok", "stdout": "is_gen_callable: False\nis_async_gen_callable: False\nis_coroutine_callable: True\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "is_gen_callable: False\nis_async_gen_callable: False\nis_coroutine_callable: True\n", "stderr": "", "exit_code": 0} |
| 5 | run_command | {
"command": "cat <<EOF > /tmp/repro_gen.py\nimport inspect\nfrom fastapi.dependencies.models import Dependant\n\nclass MyGenDep:\n def __call__(self):\n yield \"hello\"\n\nclass MyAsyncGenDep:\n async def __call__(self):\n yield \"hello\"\n\ndep_gen = Dependant(call=MyGenDep)\ndep_async_gen = Dependant(call=MyAsyncGenDep)\n\nprint(f\"MyGenDep is_gen_callable: {dep_gen.is_gen_callable}\")\nprint(f\"MyAsyncGenDep is_async_gen_callable: {dep_async_gen.is_async_gen_callable}\")\nEOF\npython3 /tmp/repro_gen.py"
} | {"status": "ok", "stdout": "MyGenDep is_gen_callable: True\nMyAsyncGenDep is_async_gen_callable: True\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "MyGenDep is_gen_callable: True\nMyAsyncGenDep is_async_gen_callable: True\n", "stderr": "", "exit_code": 0} |
| 6 | 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.isgeneratorfunction(\n _impartial(self.call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):\n return True\n if inspect.isclass(self.call):\n return False\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 if inspect.isclass(self.call):\n return False\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(_unwrapped_call(self.call), \"__call__\", None) # noqa: B004\n if dunder_unwrapped_call is None:\n return False # pragma: no cover\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.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 if inspect.isclass(self.call):\n return False\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",
"old_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.isgeneratorfunction(\n _impartial(self.call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):\n return True\n dunder_call = geta | {"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@@ -110,6 +110,8 @@\n _impartial(self.call)\n ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):\n return True\n+ if inspect.isclass(self.call):\n+ return False\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: no cover\n@@ -134,6 +136,8 @@\n _impartial(self.call)\n ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):\n return True\n+ if inspect.isclass(self.call):\n+ return False\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: no cover\n@@ -162,6 +166,8 @@\n _unwrapped_call(self.call)\n ):\n return True\n+ if inspect.isclass(self.call):\n+ return False\n dunder_call = getattr(_impartial(self.call), \"__call__\", None) # noqa: B004\n if dunder_call is None:\n return False # pragma: no cover\n", "is_truncated": false} |
| 7 | run_command | {
"command": "python3 /tmp/repro.py && python3 /tmp/repro_gen.py"
} | {"status": "ok", "stdout": "is_gen_callable: False\nis_async_gen_callable: False\nis_coroutine_callable: False\nMyGenDep is_gen_callable: False\nMyAsyncGenDep ifull{"status": "ok", "stdout": "is_gen_callable: False\nis_async_gen_callable: False\nis_coroutine_callable: False\nMyGenDep is_gen_callable: False\nMyAsyncGenDep is_async_gen_callable: False\n", "stderr": "", "exit_code": 0} |
| 8 | submit_patch | {} | {"status": "ok", "patch_size": 1325, "files_changed": 1}full{"status": "ok", "patch_size": 1325, "files_changed": 1} |
diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py
index af168a17..758afe09 100644
--- a/fastapi/dependencies/models.py
+++ b/fastapi/dependencies/models.py
@@ -110,6 +110,8 @@ class Dependant:
_impartial(self.call)
) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):
return True
+ if inspect.isclass(self.call):
+ return False
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
if dunder_call is None:
return False # pragma: no cover
@@ -134,6 +136,8 @@ class Dependant:
_impartial(self.call)
) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):
return True
+ if inspect.isclass(self.call):
+ return False
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
if dunder_call is None:
return False # pragma: no cover
@@ -162,6 +166,8 @@ class Dependant:
_unwrapped_call(self.call)
):
return True
+ if inspect.isclass(self.call):
+ return False
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
if dunder_call is None:
return False # pragma: no cover
............ [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.71s