Compare commits

...
8 Commits
Author SHA1 Message Date
Sebastián Ramírezandgithub-actions[bot] 051dfeddcc 🔖 Release version 0.140.2 (#16066)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 14:14:11 +00:00
github-actions[bot] e9980492f2 📝 Update release notes
[skip ci]
2026-07-27 14:07:09 +00:00
Sebastián RamírezandMarcelo Trylesinski 8069eadf5e ️ Stop retaining flat dependency trees (#16065)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2026-07-27 16:06:31 +02:00
github-actions[bot] 2b4349d6c2 📝 Update release notes
[skip ci]
2026-07-27 12:40:07 +00:00
Sebastián Ramírez 2f34b90742 👷 Add new memory benchmark (#16064) 2026-07-27 12:39:36 +00:00
Sebastián Ramírezandgithub-actions[bot] 3e310c90fa 🔖 Release version 0.140.1 (#16063)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 14:06:27 +02:00
github-actions[bot] 43eafc6a28 📝 Update release notes
[skip ci]
2026-07-27 11:18:58 +00:00
Sebastián Ramírez 65ef53ae6a ♻️ Update the lru_cache limit for dependencies to account for large apps (#16062) 2026-07-27 11:18:26 +00:00
6 changed files with 112 additions and 14 deletions

No files matched your search

+16
View File
@@ -7,6 +7,22 @@ hide:
## Latest Changes
## 0.140.2 (2026-07-27)
### Refactors
* ⚡️ Stop retaining flat dependency trees. PR [#16065](https://github.com/fastapi/fastapi/pull/16065) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 👷 Add new memory benchmark. PR [#16064](https://github.com/fastapi/fastapi/pull/16064) by [@tiangolo](https://github.com/tiangolo).
## 0.140.1 (2026-07-27)
### Refactors
* ♻️ Update the lru_cache limit for dependencies to account for large apps. PR [#16062](https://github.com/fastapi/fastapi/pull/16062) by [@tiangolo](https://github.com/tiangolo).
## 0.140.0 (2026-07-24)
### Refactors
+1 -1
View File
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.140.0"
__version__ = "0.140.2"
from starlette import status as status
+4 -3
View File
@@ -52,6 +52,7 @@ class Dependant:
_UsesScopesCache = dict[int, tuple[Dependant, bool]]
_CALLABLE_CLASSIFICATION_CACHE_SIZE = 4096
class _CallIdentity:
@@ -137,7 +138,7 @@ def _get_security_dependencies(*, dependant: Dependant) -> list[Dependant]:
return [dep for dep in dependant.dependencies if _is_security_scheme(dependant=dep)]
@lru_cache(maxsize=1024)
@lru_cache(maxsize=_CALLABLE_CLASSIFICATION_CACHE_SIZE)
def _is_gen_callable_cached(call_identity: _CallIdentity) -> bool:
call = call_identity.call
if inspect.isgeneratorfunction(_impartial(call)) or inspect.isgeneratorfunction(
@@ -167,7 +168,7 @@ def _is_gen_callable(call: Callable[..., Any] | None) -> bool:
return _is_gen_callable_cached(_CallIdentity(call))
@lru_cache(maxsize=1024)
@lru_cache(maxsize=_CALLABLE_CLASSIFICATION_CACHE_SIZE)
def _is_async_gen_callable_cached(call_identity: _CallIdentity) -> bool:
call = call_identity.call
if inspect.isasyncgenfunction(_impartial(call)) or inspect.isasyncgenfunction(
@@ -197,7 +198,7 @@ def _is_async_gen_callable(call: Callable[..., Any] | None) -> bool:
return _is_async_gen_callable_cached(_CallIdentity(call))
@lru_cache(maxsize=1024)
@lru_cache(maxsize=_CALLABLE_CLASSIFICATION_CACHE_SIZE)
def _is_coroutine_callable_cached(call_identity: _CallIdentity) -> bool:
call = call_identity.call
if inspect.isroutine(_impartial(call)) and iscoroutinefunction(_impartial(call)):
+5 -8
View File
@@ -807,7 +807,7 @@ class APIWebSocketRoute(routing.WebSocketRoute):
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
(
self.dependant,
self._flat_dependant,
_,
self._embed_body_fields,
) = _build_dependant_with_parameterless_dependencies(
path=self.path_format,
@@ -944,7 +944,6 @@ class _APIRouteLike(Protocol):
description: str
response_fields: dict[int | str, ModelField]
dependant: Dependant
_flat_dependant: Dependant
_embed_body_fields: bool
body_field: ModelField | None
is_sse_stream: bool
@@ -1091,7 +1090,7 @@ def _populate_api_route_state(
assert callable(endpoint), "An endpoint must be a callable"
(
route.dependant,
route._flat_dependant,
flat_dependant,
route._embed_body_fields,
) = _build_dependant_with_parameterless_dependencies(
path=route.path_format,
@@ -1099,7 +1098,7 @@ def _populate_api_route_state(
dependencies=route.dependencies,
)
route.body_field = get_body_field(
flat_dependant=route._flat_dependant,
flat_dependant=flat_dependant,
name=route.unique_id,
embed_body_fields=route._embed_body_fields,
)
@@ -1145,7 +1144,6 @@ class APIRoute(routing.Route):
description: str
response_fields: dict[int | str, ModelField]
dependant: Dependant
_flat_dependant: Dependant
_embed_body_fields: bool
body_field: ModelField | None
is_sse_stream: bool
@@ -1410,7 +1408,6 @@ class _EffectiveRouteContext:
description: str = ""
response_fields: dict[int | str, ModelField] = field(default_factory=dict)
dependant: Dependant | None = None
_flat_dependant: Dependant | None = None
_embed_body_fields: bool = False
body_field: ModelField | None = None
is_sse_stream: bool = False
@@ -1486,7 +1483,7 @@ class _EffectiveRouteContext:
)
(
context.dependant,
context._flat_dependant,
_,
context._embed_body_fields,
) = _build_dependant_with_parameterless_dependencies(
path="",
@@ -2080,7 +2077,7 @@ class _FrontendRouteGroup(BaseRoute):
self.dependency_overrides_provider = dependency_overrides_provider
(
self.dependant,
self._flat_dependant,
_,
self._embed_body_fields,
) = _build_dependant_with_parameterless_dependencies(
path="",
@@ -0,0 +1,64 @@
import sys
from collections.abc import Callable
from typing import Annotated, Any
import pytest
from fastapi import Depends, FastAPI
from fastapi.routing import APIRoute
if "--codspeed" not in sys.argv:
pytest.skip(
"Benchmark tests are skipped by default; run with --codspeed.",
allow_module_level=True,
)
LAST_DEPENDENCY_INDEX = 100
ROUTE_COUNT = 20
ROUTE_PATH_PREFIX = "/route-"
def _create_app() -> FastAPI:
app = FastAPI()
dependencies: dict[int, Callable[..., Any]] = {}
def create_dependency(index: int) -> Callable[..., Any]:
if index == LAST_DEPENDENCY_INDEX:
def dependency() -> str:
return str(index)
dependency.__name__ = f"dependency_{index}"
return dependency
next_dependency = dependencies[index + 1]
async def dependency(
sub_dependency: Annotated[str, Depends(next_dependency)],
) -> str:
return f"{index} -> {sub_dependency}"
dependency.__name__ = f"dependency_{index}"
return dependency
for index in reversed(range(LAST_DEPENDENCY_INDEX + 1)):
dependencies[index] = create_dependency(index)
async def endpoint(
value: Annotated[str, Depends(dependencies[0])],
) -> dict[str, str]:
return {"value": value}
for index in range(ROUTE_COUNT):
app.add_api_route(f"{ROUTE_PATH_PREFIX}{index}", endpoint)
return app
def test_route_dependency_graph(benchmark) -> None:
app = benchmark(_create_app)
api_routes = [
route
for route in app.routes
if isinstance(route, APIRoute) and route.path.startswith(ROUTE_PATH_PREFIX)
]
assert len(api_routes) == ROUTE_COUNT
+22 -2
View File
@@ -1,4 +1,4 @@
from collections.abc import AsyncGenerator, Generator
from collections.abc import AsyncGenerator, Callable, Generator
from typing import Any
from fastapi.dependencies.models import (
@@ -93,7 +93,27 @@ def test_callable_classification_is_shared_by_call() -> None:
cache_info = cached_function.cache_info()
assert cache_info.hits == 1
assert cache_info.misses == 1
assert cache_info.maxsize == 1024
assert cache_info.maxsize == 4096
def test_callable_classification_cache_supports_large_apps() -> None:
callables: list[Callable[[], None]] = [lambda: None for _ in range(3000)]
for classifier, cached_classifier in (
(_is_gen_callable, _is_gen_callable_cached),
(_is_async_gen_callable, _is_async_gen_callable_cached),
(_is_coroutine_callable, _is_coroutine_callable_cached),
):
cached_classifier.cache_clear()
for _ in range(2):
assert all(not classifier(call) for call in callables)
cache_info = cached_classifier.cache_info()
assert cache_info.hits == len(callables)
assert cache_info.misses == len(callables)
assert cache_info.maxsize == 4096
cached_classifier.cache_clear()
def test_unhashable_callable_classification() -> None: