resolved RESOLVED PASS · 6 tool calls · 13 s · fastapi/fastapi
🐛 Fix `Depends(func, scope='function')` for top level (parameterless) dependencies **Summary** This PR enables proper support for `scope="function"` on `APIRoute` dependencies by passing the dependency’s scope through to `get_dependant`. Previously, parameter-less dependencies created via `get_parameterless_sub_dependant` ignored `depends.scope`, which led to unexpected behavior for function-scoped dependencies. **Motivation** Users expect `scope="function"` to work as defined, even when registered at the route level. Because `depends.scope` wasn’t forwarded, function scope behaved like the default(request). This change aligns behavior with user expectations and documentation around dependency scopes. (Related: #14296)
| # | Tool | Arguments | Result |
|---|---|---|---|
| 1 | run_command | {
"command": "grep -rn \"get_parameterless_sub_dependant\" ."
} | {"status": "ok", "stdout": "./fastapi/dependencies/utils.py:124:def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant:\n./fastfull{"status": "ok", "stdout": "./fastapi/dependencies/utils.py:124:def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant:\n./fastapi/routing.py:44: get_parameterless_sub_dependant,\n./fastapi/routing.py:489: get_parameterless_sub_dependant(depends=depends, path=self.path_format),\n./fastapi/routing.py:642: get_parameterless_sub_dependant(depends=depends, path=self.path_format),\n", "exit_code": 0} |
| 2 | read_file | {
"filepath": "fastapi/dependencies/utils.py",
"start_line": 120,
"end_line": 140
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " logger.error(multipart_not_installed_error)\n raise RuntimeError(multipart_not_instalfull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " logger.error(multipart_not_installed_error)\n raise RuntimeError(multipart_not_installed_error) from None\n\n\ndef get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant:\n assert callable(depends.dependency), (\n \"A parameter-less dependency must have a callable dependency\"\n )\n use_security_scopes: List[str] = []\n if isinstance(depends, params.Security) and depends.scopes:\n use_security_scopes.extend(depends.scopes)\n return get_dependant(\n path=path, call=depends.dependency, security_scopes=use_security_scopes\n )\n\n\ndef get_flat_dependant(\n dependant: Dependant,\n *,\n skip_repeats: bool = False,\n visited: Optional[List[DependencyCacheKey]] = None,\n", "start_line": 120, "end_line": 140, "total_lines": 1020, "is_truncated": false} |
| 3 | read_file | {
"end_line": 120,
"filepath": "fastapi/dependencies/utils.py",
"start_line": 100
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n # Import an attribute that can be mocked/deleted in testing\n assert __version__ > full{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n # Import an attribute that can be mocked/deleted in testing\n assert __version__ > \"0.0.12\"\n except (ImportError, AssertionError):\n try:\n # __version__ is available in both multiparts, and can be mocked\n from multipart import __version__ # type: ignore[no-redef,import-untyped]\n\n assert __version__\n try:\n # parse_options_header is only available in the right multipart\n from multipart.multipart import ( # type: ignore[import-untyped]\n parse_options_header,\n )\n\n assert parse_options_header\n except ImportError:\n logger.error(multipart_incorrect_install_error)\n raise RuntimeError(multipart_incorrect_install_error) from None\n except ImportError:\n logger.error(multipart_not_installed_error)\n", "start_line": 100, "end_line": 120, "total_lines": 1020, "is_truncated": false} |
| 4 | run_command | {
"command": "grep -rn \"def get_dependant\" fastapi/dependencies/utils.py"
} | {"status": "ok", "stdout": "226:def get_dependant(\n", "exit_code": 0}full{"status": "ok", "stdout": "226:def get_dependant(\n", "exit_code": 0} |
| 5 | read_file | {
"end_line": 260,
"filepath": "fastapi/dependencies/utils.py",
"start_line": 226
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "def get_dependant(\n *,\n path: str,\n call: Callable[..., Any],\n name: Optional[str]full{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "def get_dependant(\n *,\n path: str,\n call: Callable[..., Any],\n name: Optional[str] = None,\n security_scopes: Optional[List[str]] = None,\n use_cache: bool = True,\n scope: Union[Literal[\"function\", \"request\"], None] = None,\n) -> Dependant:\n dependant = Dependant(\n call=call,\n name=name,\n path=path,\n security_scopes=security_scopes,\n use_cache=use_cache,\n scope=scope,\n )\n path_param_names = get_path_param_names(path)\n endpoint_signature = get_typed_signature(call)\n signature_params = endpoint_signature.parameters\n if isinstance(call, SecurityBase):\n use_scopes: List[str] = []\n if isinstance(call, (OAuth2, OpenIdConnect)):\n use_scopes = security_scopes or use_scopes\n security_requirement = SecurityRequirement(\n security_scheme=call, scopes=use_scopes\n )\n dependant.security_requirements.append(security_requirement)\n for param_name, param in signature_params.items():\n is_path_param = param_name in path_param_names\n param_details = analyze_param(\n param_name=param_name,\n annotation=param.annotation,\n value=param.default,\n is_path_param=is_path_param,\n", "start_line": 226, "end_line": 260, "total_lines": 1020, "is_truncated": false} |
| 6 | edit_file | {
"filepath": "fastapi/dependencies/utils.py",
"new_string": " return get_dependant(\n path=path, call=depends.dependency, security_scopes=use_security_scopes, scope=depends.scope\n )",
"old_string": " return get_dependant(\n path=path, call=depends.dependency, security_scopes=use_security_scopes\n )"
} | {"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapifull{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapi/dependencies/utils.py\n@@ -129,7 +129,7 @@\n if isinstance(depends, params.Security) and depends.scopes:\n use_security_scopes.extend(depends.scopes)\n return get_dependant(\n- path=path, call=depends.dependency, security_scopes=use_security_scopes\n+ path=path, call=depends.dependency, security_scopes=use_security_scopes, scope=depends.scope\n )\n \n \n", "is_truncated": false} |
| 7 | submit_patch | [] | {"status": "ok", "patch_size": 618, "files_changed": 1}full{"status": "ok", "patch_size": 618, "files_changed": 1} |
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index c5c6b69b..f71e84ee 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -129,7 +129,7 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De
if isinstance(depends, params.Security) and depends.scopes:
use_security_scopes.extend(depends.scopes)
return get_dependant(
- path=path, call=depends.dependency, security_scopes=use_security_scopes
+ path=path, call=depends.dependency, security_scopes=use_security_scopes, scope=depends.scope
)
........... [100%]
=============================== warnings summary ===============================
../../../../../../kaggle/tmp/envs/overlays/starlette-0.49.3-py3-none-any/starlette/testclient.py:51
/kaggle/tmp/envs/overlays/starlette-0.49.3-py3-none-any/starlette/testclient.py:51: 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
11 passed, 1 warning in 0.74s