โ† eval4_submission

fastapi_14448

failed WRONG_FIX wrong_fix ยท 24 tool calls ยท 138 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 (25)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"is_async_func\" fastapi | head -20"
}
{"status": "ok", "stdout": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "exit_code": 0}
2run_command
{
 "command": "grep -rn \"async\" fastapi/utils.py | head -20"
}
{"status": "ok", "stdout": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "exit_code": 0}
3run_command
{
 "command": "grep -rn \"is_async\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/dependencies/models.py:95: def is_async_gen_callable(self) -> bool:\nfastapi/dependencies/models.py:114: if self.is_gen_call
full
{"status": "ok", "stdout": "fastapi/dependencies/models.py:95:    def is_async_gen_callable(self) -> bool:\nfastapi/dependencies/models.py:114:        if self.is_gen_callable or self.is_async_gen_callable:\nfastapi/dependencies/utils.py:270:                (dependant.is_gen_callable or dependant.is_async_gen_callable)\nfastapi/dependencies/utils.py:553:    elif dependant.is_async_gen_callable:\nfastapi/dependencies/utils.py:636:            use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable\nfastapi/routing.py:65:from starlette._utils import is_async_callable\nfastapi/routing.py:92:        func if is_async_callable(func) else functools.partial(run_in_threadpool, func)  # type:ignore\n", "exit_code": 0}
4read_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}
5read_file
{
 "end_line": 85,
 "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", "start_line": 70, "end_line": 85, "total_lines": 116, "is_truncated": false}
6run_command
{
 "command": "grep -rn \"is_async_callable\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/routing.py:65:from starlette._utils import is_async_callable\nfastapi/routing.py:92: func if is_async_callable(func) else fu
full
{"status": "ok", "stdout": "fastapi/routing.py:65:from starlette._utils import is_async_callable\nfastapi/routing.py:92:        func if is_async_callable(func) else functools.partial(run_in_threadpool, func)  # type:ignore\n", "exit_code": 0}
7read_file
{
 "end_line": 100,
 "filepath": "fastapi/routing.py",
 "start_line": 60
}
{"status": "ok", "filepath": "fastapi/routing.py", "content": " is_body_allowed_for_status_code,\n)\nfrom pydantic import BaseModel\nfrom starlette import routi
full
{"status": "ok", "filepath": "fastapi/routing.py", "content": "    is_body_allowed_for_status_code,\n)\nfrom pydantic import BaseModel\nfrom starlette import routing\nfrom starlette._exception_handler import wrap_app_handling_exceptions\nfrom starlette._utils import is_async_callable\nfrom starlette.concurrency import run_in_threadpool\nfrom starlette.exceptions import HTTPException\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse, Response\nfrom starlette.routing import (\n    BaseRoute,\n    Match,\n    compile_path,\n    get_name,\n)\nfrom starlette.routing import Mount as Mount  # noqa\nfrom starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send\nfrom starlette.websockets import WebSocket\nfrom typing_extensions import Annotated, deprecated\n\n\n# Copy of starlette.routing.request_response modified to include the\n# dependencies' AsyncExitStack\ndef request_response(\n    func: Callable[[Request], Union[Awaitable[Response], Response]],\n) -> ASGIApp:\n    \"\"\"\n    Takes a function or coroutine `func(request) -> response`,\n    and returns an ASGI application.\n    \"\"\"\n    f: Callable[[Request], Awaitable[Response]] = (\n        func if is_async_callable(func) else functools.partial(run_in_threadpool, func)  # type:ignore\n    )\n\n    async def app(scope: Scope, receive: Receive, send: Send) -> None:\n        request = Request(scope, receive, send)\n\n        async def app(scope: Scope, receive: Receive, send: Send) -> None:\n            # Starts customization\n            response_awaited = False\n", "start_line": 60, "end_line": 100, "total_lines": 4517, "is_truncated": false}
8run_command
{
 "command": "grep -rn \"is_async_callable\" . | head -20"
}
{"status": "ok", "stdout": "./fastapi/routing.py:65:from starlette._utils import is_async_callable\n./fastapi/routing.py:92: func if is_async_callable(func) els
full
{"status": "ok", "stdout": "./fastapi/routing.py:65:from starlette._utils import is_async_callable\n./fastapi/routing.py:92:        func if is_async_callable(func) else functools.partial(run_in_threadpool, func)  # type:ignore\n", "exit_code": 0}
9run_command
{
 "command": "grep -rn \"is_coroutine_callable\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/dependencies/models.py:102: def is_coroutine_callable(self) -> bool:\nfastapi/dependencies/utils.py:646: elif use_sub_depend
full
{"status": "ok", "stdout": "fastapi/dependencies/models.py:102:    def is_coroutine_callable(self) -> bool:\nfastapi/dependencies/utils.py:646:        elif use_sub_dependant.is_coroutine_callable:\nfastapi/routing.py:305:    is_coroutine = dependant.is_coroutine_callable\n", "exit_code": 0}
10read_file
{
 "end_line": 320,
 "filepath": "fastapi/routing.py",
 "start_line": 280
}
{"status": "ok", "filepath": "fastapi/routing.py", "content": " # facilitate profiling endpoints, since inner functions are harder to profile.\n assert dependan
full
{"status": "ok", "filepath": "fastapi/routing.py", "content": "    # facilitate profiling endpoints, since inner functions are harder to profile.\n    assert dependant.call is not None, \"dependant.call must be a function\"\n\n    if is_coroutine:\n        return await dependant.call(**values)\n    else:\n        return await run_in_threadpool(dependant.call, **values)\n\n\ndef get_request_handler(\n    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        file_stack = request.scope.get(\"fastapi_middleware_astack\")\n        assert isinstance(file_stack, AsyncExitStack), (\n            \"fastapi_middleware_astack not found in request scope\"\n        )\n\n", "start_line": 280, "end_line": 320, "total_lines": 4517, "is_truncated": false}
11read_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}
12write_file
{
 "content": "\nimport inspect\nfrom functools import partial\nfrom typing import Any\nfrom fastapi.dependencies.models import Dependant\nfrom fastapi.routing import get_request_handler\nfrom fastapi import FastAPI, Depends\nfrom starlette.testclient import TestClient\nimport asyncio\n\nasync def async_func():\n    return \"async\"\n\ndef sync_func():\n    return \"sync\"\n\ndef wrapper(func):\n    def wrapped(*args, **kwargs):\n        return func(*args, **kwargs)\n    wrapped.__wrapped__ = func\n    return wrapped\n\n# Simulate functools.wraps\ndef wraps(func):\n    def wrapped(*args, **kwargs):\n        return func(*args, **kwargs)\n    wrapped.__wrapped__ = func\n    return wrapped\n\ndef test_combinations():\n    app = FastAPI()\n    \n    # Case 1: async func wrapped by sync wrapper\n    @wraps\n    async def dep_async_wrapped_sync():\n        return \"async_wrapped_sync\"\n    \n    # Case 2: sync func wrapped by async wrapper\n    async def async_wrapper(func):\n        async def wrapped(*args, **kwargs):\n            return func(*args, **kwargs)\n        wrapped.__wrapped__ = func\n        return wrapped\n    \n    # This is tricky because the wrapper itself is a function that returns a function\n    # Let's just use a manual async wrapper\n    async def dep_sync_wrapped_async():\n        return sync_func()\n\n    @app.get(\"/async_wrapped_sync\")\n    async def route1(d = Depends(dep_async_wrapped_sync)):\n        return d\n\n    @app.get(\"/sync_wrapped_async\")\n    async def route2(d = Depends(dep_sync_wrapped_async)):\n        return d\n\n    client = TestClient(app)\n    \n    # Test Case 1\n    resp1 = client.get(\"/async_wrapped_sync\")\n    assert resp1.status_code == 200\n    assert resp1.json() == \"async_wrapped_sync\"\n    \n    # Test Case 2\n    resp2 = client.get(\"/sync_wrapped_async\")\n    assert resp2.status_code == 200\n    assert resp2.json() == \"sync\"\n\n    print(\"All tests passed!\")\n\nif __name__ == \"__main__\":\n    test_combinations()\n",
 "filepath": "/tmp/test_wraps_partial.py"
}
{"status": "ok", "filepath": "/tmp/test_wraps_partial.py", "size": 1903}
full
{"status": "ok", "filepath": "/tmp/test_wraps_partial.py", "size": 1903}
13-14 ร—2run_command
{
 "command": "python3 /tmp/test_wraps_partial.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/fastapi_14448/a/tmp/test_wraps_partial.py': [Errno 2] No such file or directory\n", "exit_code": 2}}
15run_command
{
 "command": "ls -R /tmp"
}
{"status": "ok", "stdout": "/tmp/swe_work/eval4_submission/fastapi_14448/a/tmp:\n", "exit_code": 0}
full
{"status": "ok", "stdout": "/tmp/swe_work/eval4_submission/fastapi_14448/a/tmp:\n", "exit_code": 0}
16run_command
{
 "command": "python3 -c \"import inspect; from functools import partial; import asyncio; from fastapi.dependencies.models import Dependant; from fastapi.routing import get_request_handler; from fastapi import FastAPI, Depends; from starlette.testclient import TestClient; async def async_func(): return 'async'; def sync_func(): return 'sync'; def wraps(func): def wrapped(*args, **kwargs): return func(*args, **kwargs); wrapped.__wrapped__ = func; return wrapped; app = FastAPI(); @wraps async def dep_async_wrapped_sync(): return 'async_wrapped_sync'; async def dep_sync_wrapped_async(): return sync_func(); @app.get('/async_wrapped_sync') async def route1(d = Depends(dep_async_wrapped_sync)): return d; @app.get('/sync_wrapped_async') async def route2(d = Depends(dep_sync_wrapped_async)): return d; client = TestClient(app); resp1 = client.get('/async_wrapped_sync'); assert resp1.status_code == 200; assert resp1.json() == 'async_wrapped_sync'; resp2 = client.get('/sync_wrapped_async'); assert resp2.status_code == 200; assert resp2.json() == 'sync'; print('All tests passed!')\""
}
{"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 inspect; from functools import partial; import asyncio; from fastapi.dependencies.models import Dependant; from fastapi.routing import get_request_handler; from fastapi import FastAPI, Depends; from starlette.testclient import TestClient; async def async_func(): return 'async'; def sync_func(): return 'sync'; def wraps(func): def wrapped(*args, **kwargs): return func(*args, **kwargs); wrapped.__wrapped__ = func; return wrapped; app = FastAPI(); @wraps async def dep_async_wrapped_sync(): return 'async_wrapped_sync'; async def dep_sync_wrapped_async(): return sync_func(); @app.get('/async_wrapped_sync') async def route1(d = Depends(dep_async_wrapped_sync)): return d; @app.get('/sync_wrapped_async') async def route2(d = Depends(dep_sync_wrapped_async)): return d; client = TestClient(app); resp1 = client.get('/async_wrapped_sync'); assert resp1.status_code == 200; assert resp1.json() == 'async_wrapped_sync'; resp2 = client.get('/sync_wrapped_async'); assert resp2.status_code == 200; assert resp2.json() == 'sync'; print('All tests passed!')\n                                                                                                                                                                                                                                                          ^^^^^\nSyntaxError: invalid syntax\n", "exit_code": 1}}
17run_command
{
 "command": "python3 -c \"\nimport inspect\nfrom functools import partial\nimport asyncio\nfrom fastapi import FastAPI, Depends\nfrom starlette.testclient import TestClient\n\ndef wraps(func):\n    def wrapped(*args, **kwargs):\n        return func(*args, **kwargs)\n    wrapped.__wrapped__ = func\n    return wrapped\n\napp = FastAPI()\n\n@wraps\nasync def dep_async_wrapped_sync():\n    return 'async_wrapped_sync'\n\nasync def dep_sync_wrapped_async():\n    return 'sync_wrapped_async'\n\n@app.get('/async_wrapped_sync')\nasync def route1(d = Depends(dep_async_wrapped_sync)):\n    return d\n\n@app.get('/sync_wrapped_async')\nasync def route2(d = Depends(dep_sync_wrapped_async)):\n    return d\n\nclient = TestClient(app)\nresp1 = client.get('/async_wrapped_sync')\nprint(f'Case 1: {resp1.status_code}, {resp1.json()}')\nresp2 = client.get('/sync_wrapped_async')\nprint(f'Case 2: {resp2.status_code}, {resp2.json()}')\n\""
}
{"status": "ok", "stdout": "Case 1: 200, async_wrapped_sync\nCase 2: 200, sync_wrapped_async\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Case 1: 200, async_wrapped_sync\nCase 2: 200, sync_wrapped_async\n", "exit_code": 0}
18run_command
{
 "command": "python3 -c \"\nimport inspect\nfrom functools import partial\nfrom fastapi import FastAPI, Depends\nfrom starlette.testclient import TestClient\n\ndef wraps(func):\n    def wrapped(*args, **kwargs):\n        return func(*args, **kwargs)\n    wrapped.__wrapped__ = func\n    return wrapped\n\napp = FastAPI()\n\n# Case: async function wrapped by sync wrapper\n@wraps\nasync def dep_async_wrapped_sync():\n    return 'async_wrapped_sync'\n\n# Case: sync function wrapped by sync wrapper\n@wraps\ndef dep_sync_wrapped_sync():\n    return 'sync_wrapped_sync'\n\n# Case: partial of async function\nasync def async_func(a, b):\n    return f'async_{a}_{b}'\n\ndep_partial_async = partial(async_func, 1)\n\n# Case: partial of sync function\ndef sync_func(a, b):\n    return f'sync_{a}_{b}'\n\ndep_partial_sync = partial(sync_func, 1)\n\n@app.get('/async_wrapped_sync')\nasync def route1(d = Depends(dep_async_wrapped_sync)):\n    return d\n\n@app.get('/sync_wrapped_sync')\nasync def route2(d = Depends(dep_sync_wrapped_sync)):\n    return d\n\n@app.get('/partial_async')\nasync def route3(d = Depends(dep_partial_async)): # This might fail if not handled\n    return d\n\n@app.get('/partial_sync')\nasync def route4(d = Depends(dep_partial_sync)):\n    return d\n\nclient = TestClient(app)\nprint(f'Async Wrapped Sync: {client.get(\\\"/async_wrapped_sync\\\").json()}')\nprint(f'Sync Wrapped Sync: {client.get(\\\"/sync_wrapped_sync\\\").json()}')\ntry:\n    print(f'Partial Async: {client.get(\\\"/partial_async\\\").json()}')\nexcept Exception as e:\n    print(f'Partial Async Error: {e}')\nprint(f'Partial Sync: {client.get(\\\"/partial_sync\\\").json()}')\n\""
}
{"status": "ok", "stdout": "Async Wrapped Sync: async_wrapped_sync\nSync Wrapped Sync: sync_wrapped_sync\nPartial Async: {'detail': [{'type': 'missing', 'loc':
full
{"status": "ok", "stdout": "Async Wrapped Sync: async_wrapped_sync\nSync Wrapped Sync: sync_wrapped_sync\nPartial Async: {'detail': [{'type': 'missing', 'loc': ['query', 'b'], 'msg': 'Field required', 'input': None}]}\nPartial Sync: {'detail': [{'type': 'missing', 'loc': ['query', 'b'], 'msg': 'Field required', 'input': None}]}\n", "exit_code": 0}
19run_command
{
 "command": "python3 -c \"\nimport inspect\nfrom functools import partial\nfrom fastapi import FastAPI, Depends\nfrom starlette.testclient import TestClient\n\ndef wraps(func):\n    def wrapped(*args, **kwargs):\n        return func(*args, **kwargs)\n    wrapped.__wrapped__ = func\n    return wrapped\n\napp = FastAPI()\n\n# Case: async function wrapped by sync wrapper\n@wraps\nasync def dep_async_wrapped_sync():\n    return 'async_wrapped_sync'\n\n# Case: partial of async function\nasync def async_func(a):\n    return f'async_{a}'\n\ndep_partial_async = partial(async_func, 1)\n\n@app.get('/async_wrapped_sync')\nasync def route1(d = Depends(dep_async_wrapped_sync)):\n    return d\n\n@app.get('/partial_async')\nasync def route3(d = Depends(dep_partial_async)):\n    return d\n\nclient = TestClient(app)\nprint(f'Async Wrapped Sync: {client.get(\\\"/async_wrapped_sync\\\").json()}')\nprint(f'Partial Async: {client.get(\\\"/partial_async\\\").json()}')\n\""
}
{"status": "ok", "stdout": "Async Wrapped Sync: async_wrapped_sync\nPartial Async: async_1\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Async Wrapped Sync: async_wrapped_sync\nPartial Async: async_1\n", "exit_code": 0}
20run_command
{
 "command": "python3 -c \"\nimport inspect\nfrom functools import partial\nfrom fastapi import FastAPI, Depends\nfrom starlette.testclient import TestClient\nimport asyncio\n\ndef wraps(func):\n    def wrapped(*args, **kwargs):\n        return func(*args, **kwargs)\n    wrapped.__wrapped__ = func\n    return wrapped\n\napp = FastAPI()\n\n# Case: async function wrapped by sync wrapper, then partial\nasync def async_func(a):\n    return f'async_{a}'\n\nwrapped_async = wraps(async_func)\npartial_wrapped_async = partial(wrapped_async, 1)\n\n@app.get('/partial_wrapped_async')\nasync def route1(d = Depends(partial_wrapped_async)):\n    return d\n\nclient = TestClient(app)\nprint(f'Partial Wrapped Async: {client.get(\\\"/partial_wrapped_async\\\").json()}')\n\""
}
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": "Traceback (most recen
full
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": "Traceback (most recent call last):\n  File \"/workspace/fastapi/encoders.py\", line 337, in jsonable_encoder\n    data = dict(obj)\nTypeError: 'coroutine' object is not iterable\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n  File \"/workspace/fastapi/encoders.py\", line 342, in jsonable_encoder\n    data = vars(obj)\nTypeError: vars() argument must have __dict__ attribute\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n  File \"<string>\", line 28, in <module>\n    print(f'Partial Wrapped Async: {client.get(\"/partial_wrapped_async\").json()}')\n                                    ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^\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           ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
21read_file
{
 "end_line": 85,
 "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", "start_line": 70, "end_line": 85, "total_lines": 116, "is_truncated": false}
22edit_file
{
 "filepath": "fastapi/dependencies/models.py",
 "new_string": "    @cached_property\n    def _unwrapped_call(self) -> Any:\n        if self.call is None:\n            return self.call  # pragma: no cover\n        \n        current = self.call\n        while True:\n            unwrapped = inspect.unwrap(current)\n            if isinstance(unwrapped, partial):\n                current = unwrapped.func\n                continue\n            current = unwrapped\n            break\n        return current",
 "old_string": "    @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"
}
{"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@@ -79,10 +79,16 @@\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+        current = self.call\n+        while True:\n+            unwrapped = inspect.unwrap(current)\n+            if isinstance(unwrapped, partial):\n+                current = unwrapped.func\n+                continue\n+            current = unwrapped\n+            break\n+        return current\n \n     @cached_property\n     def is_gen_callable(self) -> bool:\n", "is_truncated": false}
23run_command
{
 "command": "python3 -c \"\nimport inspect\nfrom functools import partial\nfrom fastapi import FastAPI, Depends\nfrom starlette.testclient import TestClient\n\ndef wraps(func):\n    def wrapped(*args, **kwargs):\n        return func(*args, **kwargs)\n    wrapped.__wrapped__ = func\n    return wrapped\n\napp = FastAPI()\n\n# Case: async function wrapped by sync wrapper, then partial\nasync def async_func(a):\n    return f'async_{a}'\n\nwrapped_async = wraps(async_func)\npartial_wrapped_async = partial(wrapped_async, 1)\n\n@app.get('/partial_wrapped_async')\nasync def route1(d = Depends(partial_wrapped_async)):\n    return d\n\nclient = TestClient(app)\nprint(f'Partial Wrapped Async: {client.get(\\\"/partial_wrapped_async\\\").json()}')\n\""
}
{"status": "ok", "stdout": "Partial Wrapped Async: async_1\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Partial Wrapped Async: async_1\n", "exit_code": 0}
24run_command
{
 "command": "python3 -c \"\nimport inspect\nfrom functools import partial\nfrom fastapi import FastAPI, Depends\nfrom starlette.testclient import TestClient\n\ndef wraps(func):\n    def wrapped(*args, **kwargs):\n        return func(*args, **kwargs)\n    wrapped.__wrapped__ = func\n    return wrapped\n\napp = FastAPI()\n\n# Case: partial of async function, then wrapped\nasync def async_func(a):\n    return f'async_{a}'\n\npartial_async = partial(async_func, 1)\nwrapped_partial_async = wraps(partial_async)\n\n@app.get('/wrapped_partial_async')\nasync def route1(d = Depends(wrapped_partial_async)):\n    return d\n\nclient = TestClient(app)\nprint(f'Wrapped Partial Async: {client.get(\\\"/wrapped_partial_async\\\").json()}')\n\""
}
{"status": "ok", "stdout": "Wrapped Partial Async: async_1\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Wrapped Partial Async: async_1\n", "exit_code": 0}
25submit_patch
[]
{"status": "ok", "patch_size": 3014, "files_changed": 2}
full
{"status": "ok", "patch_size": 3014, "files_changed": 2}

Patch

diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py
index 2a4d9a01..ad0e5300 100644
--- a/fastapi/dependencies/models.py
+++ b/fastapi/dependencies/models.py
@@ -79,10 +79,16 @@ class Dependant:
     def _unwrapped_call(self) -> Any:
         if self.call is None:
             return self.call  # pragma: no cover
-        unwrapped = inspect.unwrap(self.call)
-        if isinstance(unwrapped, partial):
-            unwrapped = unwrapped.func
-        return unwrapped
+        
+        current = self.call
+        while True:
+            unwrapped = inspect.unwrap(current)
+            if isinstance(unwrapped, partial):
+                current = unwrapped.func
+                continue
+            current = unwrapped
+            break
+        return current
 
     @cached_property
     def is_gen_callable(self) -> bool:
diff --git a/tmp/test_wraps_partial.py b/tmp/test_wraps_partial.py
new file mode 100644
index 00000000..21ebec99
--- /dev/null
+++ b/tmp/test_wraps_partial.py
@@ -0,0 +1,73 @@
+
+import inspect
+from functools import partial
+from typing import Any
+from fastapi.dependencies.models import Dependant
+from fastapi.routing import get_request_handler
+from fastapi import FastAPI, Depends
+from starlette.testclient import TestClient
+import asyncio
+
+async def async_func():
+    return "async"
+
+def sync_func():
+    return "sync"
+
+def wrapper(func):
+    def wrapped(*args, **kwargs):
+        return func(*args, **kwargs)
+    wrapped.__wrapped__ = func
+    return wrapped
+
+# Simulate functools.wraps
+def wraps(func):
+    def wrapped(*args, **kwargs):
+        return func(*args, **kwargs)
+    wrapped.__wrapped__ = func
+    return wrapped
+
+def test_combinations():
+    app = FastAPI()
+    
+    # Case 1: async func wrapped by sync wrapper
+    @wraps
+    async def dep_async_wrapped_sync():
+        return "async_wrapped_sync"
+    
+    # Case 2: sync func wrapped by async wrapper
+    async def async_wrapper(func):
+        async def wrapped(*args, **kwargs):
+            return func(*args, **kwargs)
+        wrapped.__wrapped__ = func
+        return wrapped
+    
+    # This is tricky because the wrapper itself is a function that returns a function
+    # Let's just use a manual async wrapper
+    async def dep_sync_wrapped_async():
+        return sync_func()
+
+    @app.get("/async_wrapped_sync")
+    async def route1(d = Depends(dep_async_wrapped_sync)):
+        return d
+
+    @app.get("/sync_wrapped_async")
+    async def route2(d = Depends(dep_sync_wrapped_async)):
+        return d
+
+    client = TestClient(app)
+    
+    # Test Case 1
+    resp1 = client.get("/async_wrapped_sync")
+    assert resp1.status_code == 200
+    assert resp1.json() == "async_wrapped_sync"
+    
+    # Test Case 2
+    resp2 = client.get("/sync_wrapped_async")
+    assert resp2.status_code == 200
+    assert resp2.json() == "sync"
+
+    print("All tests passed!")
+
+if __name__ == "__main__":
+    test_combinations()

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.96s
<sys>:0: RuntimeWarning: coroutine 'ClassInstanceAsyncWrappedDep.__call__' was never awaited