← pod_r2_v1f

fastapi_14458

resolved RESOLVED PASS · 17 tool calls · 260 s · fastapi/fastapi

Task input

🐛 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 calls (18)

#ToolArgumentsResult
1run_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_de
full
{"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}
2run_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}
3run_command
{
 "command": "grep -n \"is_async_gen_callable\" fastapi/dependencies/models.py"
}
{"status": "ok", "stdout": "130: def is_async_gen_callable(self) -> bool:\n186: if self.is_gen_callable or self.is_async_gen_callable:\n", "stderr": "", "exit_c
full
{"status": "ok", "stdout": "130:    def is_async_gen_callable(self) -> bool:\n186:        if self.is_gen_callable or self.is_async_gen_callable:\n", "stderr": "", "exit_code": 0}
4run_command
{
 "command": "grep -n \"is_gen_callable\" fastapi/dependencies/models.py"
}
{"status": "ok", "stdout": "106: def is_gen_callable(self) -> bool:\n186: if self.is_gen_callable or self.is_async_gen_callable:\n", "stderr": "", "exit_code":
full
{"status": "ok", "stdout": "106:    def is_gen_callable(self) -> bool:\n186:        if self.is_gen_callable or self.is_async_gen_callable:\n", "stderr": "", "exit_code": 0}
5run_command
{
 "command": "sed -n '100,135p' 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", "stderr": "", "exit_code": 0}
6run_command
{
 "command": "sed -n '130,150p' fastapi/dependencies/models.py"
}
{"status": "ok", "stdout": " def is_async_gen_callable(self) -> bool:\n if self.call is None:\n return False # pragma: no cover\n if inspect.isasyncgenfunction(
full
{"status": "ok", "stdout": "    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(_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", "stderr": "", "exit_code": 0}
7run_command
{
 "command": "cat > /tmp/repro.py <<'PY'\nimport inspect\nfrom fastapi.dependencies.models import Dependant\nfrom fastapi.dependencies.utils import _impartial, _unwrapped_call\nfrom asyncio import iscoroutinefunction\n\nclass CoroCall:\n    def __call__(self):\n        async def inner():\n            pass\n        return inner\n    \n    # To make it a coroutine function, it should be defined as async def\n    # But the issue says \"if we use class (not instance) as dependency and this class has __call__ method that is coroutine or generator\"\n    # In Python, a class's __call__ is a method. \n    # If we define:\n    # class MyDep:\n    #     async def __call__(self):\n    #         return 1\n    # Then MyDep.__call__ is a function (specifically a coroutine function).\n\nclass MyDep:\n    async def __call__(self):\n        return 1\n\ndef test_is_coroutine_callable():\n    dep = Dependant(call=MyDep)\n    print(f\"Is coroutine callable: {dep.is_coroutine_callable}\")\n\nclass MyGenDep:\n    def __call__(self):\n        yield 1\n\ndef test_is_gen_callable():\n    dep = Dependant(call=MyGenDep)\n    print(f\"Is gen callable: {dep.is_gen_callable}\")\n\nclass MyAsyncGenDep:\n    async def __call__(self):\n        yield 1\n\ndef test_is_async_gen_callable():\n    dep = Dependant(call=MyAsyncGenDep)\n    print(f\"Is async gen callable: {dep.is_async_gen_callable}\")\n\nif __name__ == \"__main__\":\n    test_is_coroutine_callable()\n    test_is_gen_callable()\n    test_is_async_gen_callable()\nPY\npython3 /tmp/repro.py\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"/tmp/repro.py\", line 3, in <module>\n from fast
full
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n  File \"/tmp/repro.py\", line 3, in <module>\n    from fastapi.dependencies.utils import _impartial, _unwrapped_call\nImportError: cannot import name '_impartial' from 'fastapi.dependencies.utils' (/workspace/fastapi/dependencies/utils.py)\n", "details": {"stdout": "", "stderr": "Traceback (most recent call last):\n  File \"/tmp/repro.py\", line 3, in <module>\n    from fastapi.dependencies.utils import _impartial, _unwrapped_call\nImportError: cannot import name '_impartial' from 'fastapi.dependencies.utils' (/workspace/fastapi/dependencies/utils.py)\n", "exit_code": 1}}
8run_command
{
 "command": "grep -rn \"_impartial\" fastapi/dependencies/models.py"
}
{"status": "ok", "stdout": "fastapi/dependencies/models.py:21: unwrapped = inspect.unwrap(_impartial(call))\nfastapi/dependencies/models.py:25:def _impartial(fu
full
{"status": "ok", "stdout": "fastapi/dependencies/models.py:21:    unwrapped = inspect.unwrap(_impartial(call))\nfastapi/dependencies/models.py:25:def _impartial(func: Callable[..., Any]) -> Callable[..., Any]:\nfastapi/dependencies/models.py:110:            _impartial(self.call)\nfastapi/dependencies/models.py:113:        dunder_call = getattr(_impartial(self.call), \"__call__\", None)  # noqa: B004\nfastapi/dependencies/models.py:117:            _impartial(dunder_call)\nfastapi/dependencies/models.py:124:            _impartial(dunder_unwrapped_call)\nfastapi/dependencies/models.py:134:            _impartial(self.call)\nfastapi/dependencies/models.py:137:        dunder_call = getattr(_impartial(self.call), \"__call__\", None)  # noqa: B004\nfastapi/dependencies/models.py:141:            _impartial(dunder_call)\nfastapi/dependencies/models.py:148:            _impartial(dunder_unwrapped_call)\nfastapi/dependencies/models.py:157:        if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction(\nfastapi/dependencies/models.py:158:            _impartial(self.call)\nfastapi/dependencies/models.py:165:        dunder_call = getattr(_impartial(self.call), \"__call__\", None)  # noqa: B004\nfastapi/dependencies/models.py:168:        if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(\nfastapi/dependencies/models.py:176:            _impartial(dunder_unwrapped_call)\n", "stderr": "", "exit_code": 0}
9run_command
{
 "command": "cat > /tmp/repro.py <<'PY'\nfrom fastapi.dependencies.models import Dependant\n\nclass MyDep:\n    async def __call__(self):\n        return 1\n\ndef test_is_coroutine_callable():\n    dep = Dependant(call=MyDep)\n    print(f\"Is coroutine callable: {dep.is_coroutine_callable}\")\n\nclass MyGenDep:\n    def __call__(self):\n        yield 1\n\ndef test_is_gen_callable():\n    dep = Dependant(call=MyGenDep)\n    print(f\"Is gen callable: {dep.is_gen_callable}\")\n\nclass MyAsyncGenDep:\n    async def __call__(self):\n        yield 1\n\ndef test_is_async_gen_callable():\n    dep = Dependant(call=MyAsyncGenDep)\n    print(f\"Is async gen callable: {dep.is_async_gen_callable}\")\n\nif __name__ == \"__main__\":\n    test_is_coroutine_callable()\n    test_is_gen_callable()\n    test_is_async_gen_callable()\nPY\npython3 /tmp/repro.py\n"
}
{"status": "ok", "stdout": "Is coroutine callable: True\nIs gen callable: True\nIs async gen callable: True\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Is coroutine callable: True\nIs gen callable: True\nIs async gen callable: True\n", "stderr": "", "exit_code": 0}
10run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.dependencies.models import Dependant\nimport inspect\n\nclass MyDep:\n    async def __call__(self):\n        return 1\n\ndep = Dependant(call=MyDep)\nprint(f\"Is coroutine callable: {dep.is_coroutine_callable}\")\nprint(f\"Is class: {inspect.isclass(MyDep)}\")\nPY\n"
}
{"status": "ok", "stdout": "Is coroutine callable: True\nIs class: True\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Is coroutine callable: True\nIs class: True\n", "stderr": "", "exit_code": 0}
11run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/dependencies/models.py\"); s = p.read_text()\n\n# Fix is_gen_callable\nold_gen = \"\"\"        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\"\"\"\nnew_gen = \"\"\"        dunder_call = getattr(_impartial(self.call), \"__call__\", None)  # noqa: B004\n        if dunder_call is None or inspect.isclass(self.call):\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\n# Fix is_async_gen_callable\nold_async_gen = \"\"\"        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\"\"\"\nnew_async_gen = \"\"\"        dunder_call = getattr(_impartial(self.call), \"__call__\", None)  # noqa: B004\n        if dunder_call is None or inspect.isclass(self.call):\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\n# Fix is_coroutine_callable\nold_coro = \"\"\"        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\"\"\"\nnew_coro = \"\"\"        dunder_call = getattr(_impartial(self.call), \"__call__\", None)  # noqa: B004\n        if dunder_call is None or inspect.isclass(self.call):\n            return False  # pragma: no cover\n        if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(\n            _unwrapped_call(dunder_call)\n        ):\n            return True\"\"\"\n\ns = s.replace(old_gen, new_gen)\ns = s.replace(old_async_gen, new_async_gen)\ns = s.replace(old_coro, new_coro)\np.write_text(s)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
12run_command
{
 "command": "python3 /tmp/repro.py"
}
{"status": "ok", "stdout": "Is coroutine callable: False\nIs gen callable: False\nIs async gen callable: False\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Is coroutine callable: False\nIs gen callable: False\nIs async gen callable: False\n", "stderr": "", "exit_code": 0}
13run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.dependencies.models import Dependant\nimport inspect\nfrom asyncio import iscoroutinefunction\n\nclass MyDep:\n    async def __call__(self):\n        return 1\n\n# Instance of MyDep should still be recognized as coroutine callable\ndep_inst = Dependant(call=MyDep())\nprint(f\"Instance is coroutine callable: {dep_inst.is_coroutine_callable}\")\n\n# Class MyDep should NOT be recognized as coroutine callable\ndep_cls = Dependant(call=MyDep)\nprint(f\"Class is coroutine callable: {dep_cls.is_coroutine_callable}\")\nPY\n"
}
{"status": "ok", "stdout": "Instance is coroutine callable: True\nClass is coroutine callable: False\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Instance is coroutine callable: True\nClass is coroutine callable: False\n", "stderr": "", "exit_code": 0}
14run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.dependencies.models import Dependant\nimport inspect\n\nclass MyGenDep:\n    def __call__(self):\n        yield 1\n\n# Instance\ndep_inst = Dependant(call=MyGenDep())\nprint(f\"Instance is gen callable: {dep_inst.is_gen_callable}\")\n\n# Class\ndep_cls = Dependant(call=MyGenDep)\nprint(f\"Class is gen callable: {dep_cls.is_gen_callable}\")\nPY\n"
}
{"status": "ok", "stdout": "Instance is gen callable: True\nClass is gen callable: False\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Instance is gen callable: True\nClass is gen callable: False\n", "stderr": "", "exit_code": 0}
15run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.dependencies.models import Dependant\nimport inspect\n\nclass MyAsyncGenDep:\n    async def __call__(self):\n        yield 1\n\n# Instance\ndep_inst = Dependant(call=MyAsyncGenDep())\nprint(f\"Instance is async gen callable: {dep_inst.is_async_gen_callable}\")\n\n# Class\ndep_cls = Dependant(call=MyAsyncGenDep)\nprint(f\"Class is async gen callable: {dep_cls.is_async_gen_callable}\")\nPY\n"
}
{"status": "ok", "stdout": "Instance is async gen callable: True\nClass is async gen callable: False\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Instance is async gen callable: True\nClass is async gen callable: False\n", "stderr": "", "exit_code": 0}
16run_command
{
 "command": "git diff"
}
{"status": "ok", "stdout": "diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py\nindex af168a17..cd0a5391 100644\n--- a/fastapi/depende
full
{"status": "ok", "stdout": "diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py\nindex af168a17..cd0a5391 100644\n--- a/fastapi/dependencies/models.py\n+++ b/fastapi/dependencies/models.py\n@@ -111,7 +111,7 @@ class Dependant:\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+        if dunder_call is None or inspect.isclass(self.call):\n             return False  # pragma: no cover\n         if inspect.isgeneratorfunction(\n             _impartial(dunder_call)\n@@ -135,7 +135,7 @@ class Dependant:\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+        if dunder_call is None or inspect.isclass(self.call):\n             return False  # pragma: no cover\n         if inspect.isasyncgenfunction(\n             _impartial(dunder_call)\n@@ -163,7 +163,7 @@ class Dependant:\n         ):\n             return True\n         dunder_call = getattr(_impartial(self.call), \"__call__\", None)  # noqa: B004\n-        if dunder_call is None:\n+        if dunder_call is None or inspect.isclass(self.call):\n             return False  # pragma: no cover\n         if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(\n             _unwrapped_call(dunder_call)\n", "stderr": "", "exit_code": 0}
17run_command
{
 "command": "submit_patch"
}
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bas
full
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bash: submit_patch: command not found\n", "exit_code": 127}}
18submit_patch
{}
{"status": "ok", "patch_size": 1484, "files_changed": 1}
full
{"status": "ok", "patch_size": 1484, "files_changed": 1}

Patch

diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py
index af168a17..cd0a5391 100644
--- a/fastapi/dependencies/models.py
+++ b/fastapi/dependencies/models.py
@@ -111,7 +111,7 @@ class Dependant:
         ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):
             return True
         dunder_call = getattr(_impartial(self.call), "__call__", None)  # noqa: B004
-        if dunder_call is None:
+        if dunder_call is None or inspect.isclass(self.call):
             return False  # pragma: no cover
         if inspect.isgeneratorfunction(
             _impartial(dunder_call)
@@ -135,7 +135,7 @@ class Dependant:
         ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):
             return True
         dunder_call = getattr(_impartial(self.call), "__call__", None)  # noqa: B004
-        if dunder_call is None:
+        if dunder_call is None or inspect.isclass(self.call):
             return False  # pragma: no cover
         if inspect.isasyncgenfunction(
             _impartial(dunder_call)
@@ -163,7 +163,7 @@ class Dependant:
         ):
             return True
         dunder_call = getattr(_impartial(self.call), "__call__", None)  # noqa: B004
-        if dunder_call is None:
+        if dunder_call is None or inspect.isclass(self.call):
             return False  # pragma: no cover
         if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(
             _unwrapped_call(dunder_call)

Test output

show
............                                                             [100%]
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
  /Users/jp/repos/kaggle-gemini-coding-agent-post-training/.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.45s