← pod_r2_v1f

fastapi_14372

resolved RESOLVED UNSUBMITTED PASS · 18 tool calls · 314 s · fastapi/fastapi

Task input

♻️ Make the result of `Depends()` and `Security()` hashable, as a workaround for other tools interacting with these internal parts

♻️ Make the result of `Depends()` and `Security()` hashable, as a workaround for other tools interacting with these internal parts

---

This comes from https://github.com/fastapi/fastapi/pull/14320

As a note, FastAPI doesn't really have official support for extending its internals in ways that are not documented in the docs.

So this is not a long-term commitment to keep some specific behavior, but a quick workaround to make things easier for current tools that interact with FastAPI internals in some way.

---

At some point in the future, I'll refactor the internals of FastAPI to make them more explicitly private, and then also figure out the right ways to expose any points of connection with external components that currently need to interact with the internals in some way, and formalize that better.

---

Before that, I need to handle a few issues, bugs, features, refactors, deprecations, etc. And then I'll be able to come back to this idea of figuring out how and what to expose. :nerd_face: 

Tool calls (18)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"def Depends\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/param_functions.py:2221:def Depends( # noqa: N802\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "fastapi/param_functions.py:2221:def Depends(  # noqa: N802\n", "stderr": "", "exit_code": 0}
2run_command
{
 "command": "grep -rn \"def Security\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/param_functions.py:2301:def Security( # noqa: N802\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "fastapi/param_functions.py:2301:def Security(  # noqa: N802\n", "stderr": "", "exit_code": 0}
3run_command
{
 "command": "sed -n '2220,2310p' fastapi/param_functions.py"
}
{"status": "ok", "stdout": "\ndef Depends( # noqa: N802\n dependency: Annotated[\n Optional[Callable[..., Any]],\n Doc(\n \"\"\"\n A \"dependable\" callable (li
full
{"status": "ok", "stdout": "\ndef Depends(  # noqa: N802\n    dependency: Annotated[\n        Optional[Callable[..., Any]],\n        Doc(\n            \"\"\"\n            A \"dependable\" callable (like a function).\n\n            Don't call it directly, FastAPI will call it for you, just pass the object\n            directly.\n            \"\"\"\n        ),\n    ] = None,\n    *,\n    use_cache: Annotated[\n        bool,\n        Doc(\n            \"\"\"\n            By default, after a dependency is called the first time in a request, if\n            the dependency is declared again for the rest of the request (for example\n            if the dependency is needed by several dependencies), the value will be\n            re-used for the rest of the request.\n\n            Set `use_cache` to `False` to disable this behavior and ensure the\n            dependency is called again (if declared more than once) in the same request.\n            \"\"\"\n        ),\n    ] = True,\n    scope: Annotated[\n        Union[Literal[\"function\", \"request\"], None],\n        Doc(\n            \"\"\"\n            Mainly for dependencies with `yield`, define when the dependency function\n            should start (the code before `yield`) and when it should end (the code\n            after `yield`).\n\n            * `\"function\"`: start the dependency before the *path operation function*\n                that handles the request, end the dependency after the *path operation\n                function* ends, but **before** the response is sent back to the client.\n                So, the dependency function will be executed **around** the *path operation\n                **function***.\n            * `\"request\"`: start the dependency before the *path operation function*\n                that handles the request (similar to when using `\"function\"`), but end\n                **after** the response is sent back to the client. So, the dependency\n                function will be execu
4run_command
{
 "command": "grep -rn \"class Depends\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/params.py:766:class Depends:\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "fastapi/params.py:766:class Depends:\n", "stderr": "", "exit_code": 0}
5run_command
{
 "command": "grep -rn \"class Security\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/params.py:773:class Security(Depends):\nfastapi/security/oauth2.py:614:class SecurityScopes:\nfastapi/security/base.py:4:cla
full
{"status": "ok", "stdout": "fastapi/params.py:773:class Security(Depends):\nfastapi/security/oauth2.py:614:class SecurityScopes:\nfastapi/security/base.py:4:class SecurityBase:\nfastapi/dependencies/models.py:19:class SecurityRequirement:\nfastapi/openapi/models.py:337:class SecuritySchemeType(Enum):\nfastapi/openapi/models.py:344:class SecurityBase(BaseModelWithConfig):\n", "stderr": "", "exit_code": 0}
6run_command
{
 "command": "sed -n '760,780p' fastapi/params.py"
}
{"status": "ok", "stdout": " json_schema_extra=json_schema_extra,\n **extra,\n )\n\n\n@dataclass\nclass Depends:\n dependency: Optional[Callable[..., Any]] = No
full
{"status": "ok", "stdout": "            json_schema_extra=json_schema_extra,\n            **extra,\n        )\n\n\n@dataclass\nclass Depends:\n    dependency: Optional[Callable[..., Any]] = None\n    use_cache: bool = True\n    scope: Union[Literal[\"function\", \"request\"], None] = None\n\n\n@dataclass\nclass Security(Depends):\n    scopes: Optional[Sequence[str]] = None\n", "stderr": "", "exit_code": 0}
7run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.params import Depends, Security\nfrom typing import Callable\n\ndef mock_dep():\n    pass\n\nd1 = Depends(mock_dep)\nd2 = Depends(mock_dep)\ns1 = Security(mock_dep)\ns2 = Security(mock_dep)\n\nprint(f\"Depends hashable: {hash(d1) == hash(d2)}\")\nprint(f\"Security hashable: {hash(s1) == hash(s2)}\")\ntry:\n    set([d1, d2])\n    print(\"Depends can be in set\")\nexcept Exception as e:\n    print(f\"Depends set error: {e}\")\n\ntry:\n    set([s1, s2])\n    print(\"Security can be in set\")\nexcept Exception as e:\n    print(f\"Security set error: {e}\")\nPY\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"<stdin>\", line 12, in <module>\nTypeError: unha
full
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n  File \"<stdin>\", line 12, in <module>\nTypeError: unhashable type: 'Depends'\n", "details": {"stdout": "", "stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 12, in <module>\nTypeError: unhashable type: 'Depends'\n", "exit_code": 1}}
8run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/params.py\")\ns = p.read_text()\nold = \"\"\"@dataclass\nclass Depends:\n    dependency: Optional[Callable[..., Any]] = None\n    use_cache: bool = True\n    scope: Union[Literal[\"function\", \"request\"], None] = None\"\"\"\nnew = \"\"\"@dataclass(frozen=True)\nclass Depends:\n    dependency: Optional[Callable[..., Any]] = None\n    use_cache: bool = True\n    scope: Union[Literal[\"function\", \"request\"], None] = None\"\"\"\nassert s.count(old) == 1, (\"old text found this many times:\", s.count(old))\nt = s.replace(old, new)\np.write_text(t)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
9run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.params import Depends, Security\nfrom typing import Callable\n\ndef mock_dep():\n    pass\n\nd1 = Depends(mock_dep)\nd2 = Depends(mock_dep)\ns1 = Security(mock_dep)\ns2 = Security(mock_dep)\n\nprint(f\"Depends hashable: {hash(d1) == hash(d2)}\")\nprint(f\"Security hashable: {hash(s1) == hash(s2)}\")\ntry:\n    set([d1, d2])\n    print(\"Depends can be in set\")\nexcept Exception as e:\n    print(f\"Depends set error: {e}\")\n\ntry:\n    set([s1, s2])\n    print(\"Security can be in set\")\nexcept Exception as e:\n    print(f\"Security set error: {e}\")\nPY\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"<stdin>\", line 1, in <module>\n File \"/private
full
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\n  File \"/private/workspace/fastapi/__init__.py\", line 7, in <module>\n    from .applications import FastAPI as FastAPI\n  File \"/private/workspace/fastapi/applications.py\", line 17, in <module>\n    from fastapi import routing\n  File \"/private/workspace/fastapi/routing.py\", line 28, in <module>\n    from fastapi import params, temp_pydantic_v1_params\n  File \"/private/workspace/fastapi/params.py\", line 772, in <module>\n    @dataclass\n     ^^^^^^^^^\n  File \"/Users/jp/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/dataclasses.py\", line 1354, in dataclass\n    return wrap(cls)\n  File \"/Users/jp/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/dataclasses.py\", line 1344, in wrap\n    return _process_class(cls, init, repr, eq, order, unsafe_hash,\n                          frozen, match_args, kw_only, slots,\n                          weakref_slot)\n  File \"/Users/jp/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/dataclasses.py\", line 1038, in _process_class\n    raise TypeError('cannot inherit non-frozen dataclass from a '\n                    'frozen one')\nTypeError: cannot inherit non-frozen dataclass from a frozen one\n", "details": {"stdout": "", "stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\n  File \"/private/workspace/fastapi/__init__.py\", line 7, in <module>\n    from .applications import FastAPI as FastAPI\n  File \"/private/workspace/fastapi/applications.py\", line 17, in <module>\n    from fastapi import routing\n  File \"/private/workspace/fastapi/routing.py\", line 28, in <module>\n    from fastapi import params, temp_pydantic_v1_params\n  File \"/private/workspace/fastapi/params.py\", line 772, in <module>\n    @dataclass\n     ^^^^^^^^^\n  File \"/Users/jp/.local/share/uv/python
10run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/params.py\")\ns = p.read_text()\nold = \"\"\"@dataclass\nclass Security(Depends):\n    scopes: Optional[Sequence[str]] = None\"\"\"\nnew = \"\"\"@dataclass(frozen=True)\nclass Security(Depends):\n    scopes: Optional[Sequence[str]] = None\"\"\"\nassert s.count(old) == 1, (\"old text found this many times:\", s.count(old))\nt = s.replace(old, new)\np.write_text(t)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
11-12 ×2run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.params import Depends, Security\nfrom typing import Callable\n\ndef mock_dep():\n    pass\n\nd1 = Depends(mock_dep)\nd2 = Depends(mock_dep)\ns1 = Security(mock_dep)\ns2 = Security(mock_dep)\n\nprint(f\"Depends hashable: {hash(d1) == hash(d2)}\")\nprint(f\"Security hashable: {hash(s1) == hash(s2)}\")\ntry:\n    set([d1, d2])\n    print(\"Depends can be in set\")\nexcept Exception as e:\n    print(f\"Depends set error: {e}\")\n\ntry:\n    set([s1, s2])\n    print(\"Security can be in set\")\nexcept Exception as e:\n    print(f\"Security set error: {e}\")\nPY\n"
}
{"status": "ok", "stdout": "Depends hashable: True\nSecurity hashable: True\nDepends can be in set\nSecurity can be in set\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Depends hashable: True\nSecurity hashable: True\nDepends can be in set\nSecurity can be in set\n", "stderr": "", "exit_code": 0}
13run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.params import Depends, Security\nfrom typing import Callable\n\ndef mock_dep():\n    pass\n\nd1 = Depends(mock_dep)\nd2 = Depends(mock_dep)\nprint(f\"d1: {d1}\")\nprint(f\"d2: {d2}\")\ntry:\n    d1.use_cache = False\n    print(\"Successfully modified d1.use_cache\")\nexcept Exception as e:\n    print(f\"Error modifying d1.use_cache: {e}\")\nPY\n"
}
{"status": "ok", "stdout": "d1: Depends(dependency=<function mock_dep at 0x10241b7e0>, use_cache=True, scope=None)\nd2: Depends(dependency=<function mock_dep at
full
{"status": "ok", "stdout": "d1: Depends(dependency=<function mock_dep at 0x10241b7e0>, use_cache=True, scope=None)\nd2: Depends(dependency=<function mock_dep at 0x10241b7e0>, use_cache=True, scope=None)\nError modifying d1.use_cache: cannot assign to field 'use_cache'\n", "stderr": "", "exit_code": 0}
14run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/params.py\")\ns = p.read_text()\nold = \"\"\"@dataclass(frozen=True)\nclass Security(Depends):\n    scopes: Optional[Sequence[str]] = None\"\"\"\nnew = \"\"\"@dataclass\nclass Security(Depends):\n    scopes: Optional[Sequence[str]] = None\"\"\"\nassert s.count(old) == 1, (\"old text found this many times:\", s.count(old))\nt = s.replace(old, new)\np.write_text(t)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
15run_command
{
 "command": "python3 - <<'PY'\nfrom fastapi.params import Depends, Security\nfrom typing import Callable\n\ndef mock_dep():\n    pass\n\nd1 = Depends(mock_dep)\nd2 = Depends(mock_dep)\ns1 = Security(mock_dep)\ns2 = Security(mock_dep)\n\nprint(f\"Depends hashable: {hash(d1) == hash(d2)}\")\nprint(f\"Security hashable: {hash(s1) == hash(s2)}\")\ntry:\n    set([d1, d2])\n    print(\"Depends can be in set\")\nexcept Exception as e:\n    print(f\"Depends set error: {e}\")\n\ntry:\n    set([s1, s2])\n    print(\"Security can be in set\")\nexcept Exception as e:\n    print(f\"Security set error: {e}\")\nPY\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"<stdin>\", line 1, in <module>\n File \"/private
full
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\n  File \"/private/workspace/fastapi/__init__.py\", line 7, in <module>\n    from .applications import FastAPI as FastAPI\n  File \"/private/workspace/fastapi/applications.py\", line 17, in <module>\n    from fastapi import routing\n  File \"/private/workspace/fastapi/routing.py\", line 28, in <module>\n    from fastapi import params, temp_pydantic_v1_params\n  File \"/private/workspace/fastapi/params.py\", line 772, in <module>\n    @dataclass\n     ^^^^^^^^^\n  File \"/Users/jp/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/dataclasses.py\", line 1354, in dataclass\n    return wrap(cls)\n  File \"/Users/jp/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/dataclasses.py\", line 1344, in wrap\n    return _process_class(cls, init, repr, eq, order, unsafe_hash,\n                          frozen, match_args, kw_only, slots,\n                          weakref_slot)\n  File \"/Users/jp/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/dataclasses.py\", line 1038, in _process_class\n    raise TypeError('cannot inherit non-frozen dataclass from a '\n                    'frozen one')\nTypeError: cannot inherit non-frozen dataclass from a frozen one\n", "details": {"stdout": "", "stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\n  File \"/private/workspace/fastapi/__init__.py\", line 7, in <module>\n    from .applications import FastAPI as FastAPI\n  File \"/private/workspace/fastapi/applications.py\", line 17, in <module>\n    from fastapi import routing\n  File \"/private/workspace/fastapi/routing.py\", line 28, in <module>\n    from fastapi import params, temp_pydantic_v1_params\n  File \"/private/workspace/fastapi/params.py\", line 772, in <module>\n    @dataclass\n     ^^^^^^^^^\n  File \"/Users/jp/.local/share/uv/python
16run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/params.py\")\ns = p.read_text()\nold = \"\"\"@dataclass\nclass Depends:\n    dependency: Optional[Callable[..., Any]] = None\n    use_cache: bool = True\n    scope: Union[Literal[\"function\", \"request\"], None] = None\"\"\"\nnew = \"\"\"@dataclass(frozen=True)\nclass Depends:\n    dependency: Optional[Callable[..., Any]] = None\n    use_cache: bool = True\n    scope: Union[Literal[\"function\", \"request\"], None] = None\"\"\"\nassert s.count(old) == 1, (\"old text found this many times:\", s.count(old))\nt = s.replace(old, new)\np.write_text(t)\nPY\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n File \"<stdin>\", line 14, in <module>\nAssertionError:
full
{"status": "error", "error_type": "CommandError", "error_message": "Traceback (most recent call last):\n  File \"<stdin>\", line 14, in <module>\nAssertionError: ('old text found this many times:', 0)\n", "details": {"stdout": "", "stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 14, in <module>\nAssertionError: ('old text found this many times:', 0)\n", "exit_code": 1}}
17run_command
{
 "command": "sed -n '760,780p' fastapi/params.py"
}
{"status": "ok", "stdout": " json_schema_extra=json_schema_extra,\n **extra,\n )\n\n\n@dataclass(frozen=True)\nclass Depends:\n dependency: Optional[Callable[..
full
{"status": "ok", "stdout": "            json_schema_extra=json_schema_extra,\n            **extra,\n        )\n\n\n@dataclass(frozen=True)\nclass Depends:\n    dependency: Optional[Callable[..., Any]] = None\n    use_cache: bool = True\n    scope: Union[Literal[\"function\", \"request\"], None] = None\n\n\n@dataclass\nclass Security(Depends):\n    scopes: Optional[Sequence[str]] = None\n", "stderr": "", "exit_code": 0}
18run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/params.py\")\ns = p.read_text()\nold = \"\"\"@dataclass\nclass Security(Depends):\n    scopes: Optional[Sequence[str]] = None\"\"\"\nnew = \"\"\"@dataclass(frozen=True)\nclass Security(Depends):\n    scopes: Optional[Sequence[str]] = None\"\"\"\nassert s.count(old) == 1, (\"old text found this many times:\", s.count(old))\nt = s.replace(old, new)\np.write_text(t)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}

Patch

diff --git a/fastapi/params.py b/fastapi/params.py
index 6a58d580..6d07df35 100644
--- a/fastapi/params.py
+++ b/fastapi/params.py
@@ -762,13 +762,13 @@ class File(Form):  # type: ignore[misc]
         )
 
 
-@dataclass
+@dataclass(frozen=True)
 class Depends:
     dependency: Optional[Callable[..., Any]] = None
     use_cache: bool = True
     scope: Union[Literal["function", "request"], None] = None
 
 
-@dataclass
+@dataclass(frozen=True)
 class Security(Depends):
     scopes: Optional[Sequence[str]] = None

Test output

show
.                                                                        [100%]
1 passed in 0.30s