← pod_r5_v1h

fastapi_14458

resolved RESOLVED PASS · 14 tool calls · 139 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 (15)

#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": "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_gen
full
{"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}
3run_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_calla
full
{"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}
4run_command
{
 "command": "sed -n '100,200p' 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
5run_command
{
 "command": "python3 -c \"import inspect; print(inspect.isclass)\""
}
{"status": "ok", "stdout": "<function isclass at 0x103e9cea0>\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "<function isclass at 0x103e9cea0>\n", "stderr": "", "exit_code": 0}
6run_command
{
 "command": "cat > /tmp/reproduce_issue.py <<'PY'\nimport inspect\nfrom fastapi import FastAPI, Depends\nfrom fastapi.dependencies.models import Dependant\nfrom fastapi.routing import APIRoute\n\n# Mocking the necessary parts of Dependant\nclass MockDependant:\n    def __init__(self, call):\n        self.call = call\n\n    @property\n    def is_gen_callable(self):\n        if self.call is None: return False\n        if inspect.isgeneratorfunction(self.call): return True\n        dunder_call = getattr(self.call, \"__call__\", None)\n        if dunder_call and inspect.isgeneratorfunction(dunder_call):\n            return True\n        return False\n\n    @property\n    def is_async_gen_callable(self):\n        if self.call is None: return False\n        if inspect.isasyncgenfunction(self.call): return True\n        dunder_call = getattr(self.call, \"__call__\", None)\n        if dunder_call and inspect.isasyncgenfunction(dunder_call):\n            return True\n        return False\n\n    @property\n    def is_coroutine_callable(self):\n        if self.call is None: return False\n        if inspect.iscoroutinefunction(self.call): return True\n        dunder_call = getattr(self.call, \"__call__\", None)\n        if dunder_call and inspect.iscoroutinefunction(dunder_call):\n            return True\n        return False\n\nclass MyDep:\n    def __call__(self):\n        yield 1\n\ndef test_issue():\n    # When MyDep is used as a class dependency\n    dep = MockDependant(MyDep)\n    print(f\"is_gen_callable: {dep.is_gen_callable}\")\n    print(f\"is_async_gen_callable: {dep.is_async_gen_callable}\")\n    print(f\"is_coroutine_callable: {dep.is_coroutine_callable}\")\n\nif __name__ == \"__main__\":\n    test_issue()\nPY\npython3 /tmp/reproduce_issue.py\n"
}
{"status": "ok", "stdout": "is_gen_callable: True\nis_async_gen_callable: False\nis_coroutine_callable: False\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "is_gen_callable: True\nis_async_gen_callable: False\nis_coroutine_callable: False\n", "stderr": "", "exit_code": 0}
7run_command
{
 "command": "cat > /tmp/reproduce_issue.py <<'PY'\nimport inspect\nimport asyncio\nfrom fastapi.dependencies.models import Dependant\n\nclass MyDep:\n    def __call__(self):\n        yield 1\n\nclass MyAsyncDep:\n    async def __call__(self, x):\n        return x\n\nclass MyAsyncGenDep:\n    async def __call__(self, x):\n        yield x\n\n# We need to create a real Dependant object to test this, \n# but it's easier to just mock it or use the real Dependant class \n# if we can initialize it.\n# However, Dependant is a Pydantic model, let's see how to instantiate it.\n\nfrom fastapi.dependencies.models import Dependant\n# Dependant is likely a Pydantic model. Let's try to instantiate it.\n# We can't easily instantiate Dependant because it requires many arguments.\n# Let's just use a simple class that implements the properties as in the real fastapi.\n# Instead of actually running it, let's just check if we can reproduce the logic.\n\ndef check_logic(call):\n    # simplified version of is_gen_callable logic\n    if call is None: return False\n    # simulate inspect.isgeneratorfunction(_impartial(self.call))\n    if inspect.isgeneratorfunction(call): return True\n    dunder_call = getattr(call, \"__call__\", None)\n    if dunder_call is None: return False\n    if inspect.isgeneratorfunction(dunder_call): return True\n    return False\n\ndef check_coroutine_logic(call):\n    if call is None: return False\n    if inspect.iscoroutinefunction(call): return True\n    dunder_call = getattr(call, \"__call__\", None)\n    if dunder_call is None: return False\n    if inspect.iscoroutinefunction(dunder_call): return True\n    return False\n\nclass GenClass:\n    def __call__(self):\n        yield 1\n\nclass CoroClass:\n    async def __call__(self):\n        return 1\n\nprint(f\"GenClass is_gen: {check_logic(GenClass)}\")\nprint(f\"CoroClass is_coro: {check_coroutine_logic(CoroClass)}\")\nPY\npython3 /tmp/reproduce_issue.py\n"
}
{"status": "ok", "stdout": "GenClass is_gen: True\nCoroClass is_coro: True\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "GenClass is_gen: True\nCoroClass is_coro: True\n", "stderr": "", "exit_code": 0}
8run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\nimport inspect\n\np = Path(\"fastapi/dependencies/models.py\")\ns = 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)\n\np.write_text(s)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
9run_command
{
 "command": "cat > /tmp/verify_fix.py <<'PY'\nimport inspect\nimport asyncio\nfrom fastapi.dependencies.models import Dependant\n\nclass MyDep:\n    def __call__(self):\n        yield 1\n\nclass MyAsyncDep:\n    async def __call__(self, x):\n        return x\n\nclass MyAsyncGenDep:\n    async def __call__(self, x):\n        yield x\n\n# Mocking the Dependant for verification because it's a Pydantic model \n# and we don't want to set up a whole FastAPI app.\n# We can't easily instantiate Dependant, but we can mock it.\n# Since we modified the methods on the Dependant class, \n# let's use a dummy object that inherits from Dependant or \n# just mock the attributes.\n\n# However, the modified methods use self.call, and they use helper functions \n# _impartial and _unwrapped_call.\n# Let's look at those functions.\nPY\nrun_command: grep -n \"_impartial\" fastapi/dependencies/models.py\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: line 28: run_command:: command not found\n", "details": {"stdout": "", "stderr":
full
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: line 28: run_command:: command not found\n", "details": {"stdout": "", "stderr": "/bin/bash: line 28: run_command:: command not found\n", "exit_code": 127}}
10run_command
{
 "command": "grep -n \"_impartial\" fastapi/dependencies/models.py"
}
{"status": "ok", "stdout": "21: unwrapped = inspect.unwrap(_impartial(call))\n25:def _impartial(func: Callable[..., Any]) -> Callable[..., Any]:\n110: _impartia
full
{"status": "ok", "stdout": "21:    unwrapped = inspect.unwrap(_impartial(call))\n25:def _impartial(func: Callable[..., Any]) -> Callable[..., Any]:\n110:            _impartial(self.call)\n113:        dunder_call = getattr(_impartial(self.call), \"__call__\", None)  # noqa: B004\n117:            _impartial(dunder_call)\n124:            _impartial(dunder_unwrapped_call)\n134:            _impartial(self.call)\n137:        dunder_call = getattr(_impartial(self.call), \"__call__\", None)  # noqa: B004\n141:            _impartial(dunder_call)\n148:            _impartial(dunder_unwrapped_call)\n157:        if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction(\n158:            _impartial(self.call)\n165:        dunder_call = getattr(_impartial(self.call), \"__call__\", None)  # noqa: B004\n168:        if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(\n176:            _impartial(dunder_unwrapped_call)\n", "stderr": "", "exit_code": 0}
11run_command
{
 "command": "sed -n '20,40p' fastapi/dependencies/models.py"
}
{"status": "ok", "stdout": " return call # pragma: no cover\n unwrapped = inspect.unwrap(_impartial(call))\n return unwrapped\n\n\ndef _impartial(func: Callable
full
{"status": "ok", "stdout": "        return call  # pragma: no cover\n    unwrapped = inspect.unwrap(_impartial(call))\n    return unwrapped\n\n\ndef _impartial(func: Callable[..., Any]) -> Callable[..., Any]:\n    while isinstance(func, partial):\n        func = func.func\n    return func\n\n\n@dataclass\nclass Dependant:\n    path_params: List[ModelField] = field(default_factory=list)\n    query_params: List[ModelField] = field(default_factory=list)\n    header_params: List[ModelField] = field(default_factory=list)\n    cookie_params: List[ModelField] = field(default_factory=list)\n    body_params: List[ModelField] = field(default_factory=list)\n    dependencies: List[\"Dependant\"] = field(default_factory=list)\n    name: Optional[str] = None\n    call: Optional[Callable[..., Any]] = None\n", "stderr": "", "exit_code": 0}
12run_command
{
 "command": "cat > /tmp/verify_fix.py <<'PY'\nimport inspect\nfrom functools import partial\nfrom fastapi.dependencies.models import Dependant\n\ndef _impartial(func):\n    while isinstance(func, partial):\n        func = func.func\n    return func\n\ndef _unwrapped_call(call):\n    return inspect.unwrap(_impartial(call))\n\nclass MyDep:\n    def __call__(self):\n        yield 1\n\nclass MyAsyncDep:\n    async def __call__(self, x):\n        return x\n\nclass MyAsyncGenDep:\n    async def __call__(self, x):\n        yield x\n\n# We need to override the methods of Dependant because we are using \n# the logic from fastapi/dependencies/models.py but Dependant is a dataclass.\n# The patched methods in models.py are using @cached_property.\n\nfrom fastapi.dependencies.models import Dependant\n\n# Create a dummy Dependant instance. \n# Dependant is a dataclass, so we can just instantiate it.\ndep_gen = Dependant(call=MyDep)\ndep_async = Dependant(call=MyAsyncDep)\ndep_async_gen = Dependant(call=MyAsyncGenDep)\n\n# Dependant methods are @cached_property, so they are handled by the class.\n# But we need to make sure they are correctly imported.\n# Wait, the methods I edited are in the Dependant class.\n\nprint(f\"GenClass is_gen: {dep_gen.is_gen_callable}\")\nprint(f\"AsyncDep is_coro: {dep_async.is_coroutine_callable}\")\nprint(f\"AsyncGenDep is_async_gen: {dep_async_gen.is_async_gen_callable}\")\n\n# Now test with instances\ndep_gen_inst = Dependant(call=MyDep())\ndep_async_inst = Dependant(call=MyAsyncDep())\ndep_async_gen_inst = Dependant(call=MyAsyncGenDep())\n\nprint(f\"GenInst is_gen: {dep_gen_inst.is_gen_callable}\")\nprint(f\"AsyncDepInst is_coro: {dep_async_inst.is_coroutine_callable}\")\nprint(f\"AsyncGenDepInst is_async_gen: {dep_async_gen_inst.is_async_gen_callable}\")\nPY\npython3 /tmp/verify_fix.py\n"
}
{"status": "ok", "stdout": "GenClass is_gen: False\nAsyncDep is_coro: False\nAsyncGenDep is_async_gen: False\nGenInst is_gen: True\nAsyncDepInst is_coro: True\n
full
{"status": "ok", "stdout": "GenClass is_gen: False\nAsyncDep is_coro: False\nAsyncGenDep is_async_gen: False\nGenInst is_gen: True\nAsyncDepInst is_coro: True\nAsyncGenDepInst is_async_gen: True\n", "stderr": "", "exit_code": 0}
13run_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}
14run_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}}
15submit_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.51s