mirror of
https://github.com/fastapi/fastapi.git
synced 2026-09-10 20:37:10 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ac0f1b541 | ||
|
|
44e4eeaa78 | ||
|
|
7134121a71 | ||
|
|
415d37fa6f | ||
|
|
ca353d7215 | ||
|
|
d012979f9a | ||
|
|
051dfeddcc | ||
|
|
e9980492f2 | ||
|
|
8069eadf5e | ||
|
|
2b4349d6c2 | ||
|
|
2f34b90742 | ||
|
|
3e310c90fa | ||
|
|
43eafc6a28 | ||
|
|
65ef53ae6a |
No files matched your search
@@ -7,6 +7,34 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.140.4 (2026-07-27)
|
||||
|
||||
### Refactors
|
||||
|
||||
* ⚡️ Skip unused dependency repeat bookkeeping. PR [#16069](https://github.com/fastapi/fastapi/pull/16069) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.140.3 (2026-07-27)
|
||||
|
||||
### Refactors
|
||||
|
||||
* ⚡️ Avoid repeated dependency flattening in OpenAPI. PR [#16067](https://github.com/fastapi/fastapi/pull/16067) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 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
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.140.0"
|
||||
__version__ = "0.140.4"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -152,16 +152,20 @@ def get_flat_dependant(
|
||||
parent_oauth_scopes: list[str] | None = None,
|
||||
_uses_scopes_cache: _UsesScopesCache | None = None,
|
||||
) -> Dependant:
|
||||
if visited is None:
|
||||
visited = []
|
||||
if _uses_scopes_cache is None:
|
||||
_uses_scopes_cache = {}
|
||||
visited.append(
|
||||
_get_cache_key(
|
||||
dependant=dependant,
|
||||
uses_scopes_cache=_uses_scopes_cache,
|
||||
)
|
||||
track_visited = (
|
||||
skip_repeats or visited is not None or _uses_scopes_cache is not None
|
||||
)
|
||||
if track_visited:
|
||||
if visited is None:
|
||||
visited = []
|
||||
if _uses_scopes_cache is None:
|
||||
_uses_scopes_cache = {}
|
||||
visited.append(
|
||||
_get_cache_key(
|
||||
dependant=dependant,
|
||||
uses_scopes_cache=_uses_scopes_cache,
|
||||
)
|
||||
)
|
||||
use_parent_oauth_scopes = (parent_oauth_scopes or []) + (
|
||||
_get_oauth_scopes(dependant=dependant)
|
||||
)
|
||||
@@ -187,15 +191,16 @@ def get_flat_dependant(
|
||||
scope=dependant.scope,
|
||||
)
|
||||
for sub_dependant in dependant.dependencies:
|
||||
if (
|
||||
skip_repeats
|
||||
and _get_cache_key(
|
||||
dependant=sub_dependant,
|
||||
uses_scopes_cache=_uses_scopes_cache,
|
||||
)
|
||||
in visited
|
||||
):
|
||||
continue
|
||||
if skip_repeats:
|
||||
assert visited is not None
|
||||
if (
|
||||
_get_cache_key(
|
||||
dependant=sub_dependant,
|
||||
uses_scopes_cache=_uses_scopes_cache,
|
||||
)
|
||||
in visited
|
||||
):
|
||||
continue
|
||||
flat_sub = get_flat_dependant(
|
||||
sub_dependant,
|
||||
skip_repeats=skip_repeats,
|
||||
|
||||
@@ -112,7 +112,7 @@ def get_openapi_security_definitions(
|
||||
|
||||
def _get_openapi_operation_parameters(
|
||||
*,
|
||||
dependant: Dependant,
|
||||
flat_dependant: Dependant,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: dict[
|
||||
tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
|
||||
@@ -120,7 +120,6 @@ def _get_openapi_operation_parameters(
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> list[dict[str, Any]]:
|
||||
parameters = []
|
||||
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
|
||||
path_params = _get_flat_fields_from_params(flat_dependant.path_params)
|
||||
query_params = _get_flat_fields_from_params(flat_dependant.query_params)
|
||||
header_params = _get_flat_fields_from_params(flat_dependant.header_params)
|
||||
@@ -284,12 +283,22 @@ def get_openapi_path(
|
||||
assert current_response_class, "A response class is needed to generate OpenAPI"
|
||||
route_response_media_type: str | None = current_response_class.media_type
|
||||
if route.include_in_schema:
|
||||
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
|
||||
all_route_params = [
|
||||
field
|
||||
for fields in (
|
||||
flat_dependant.path_params,
|
||||
flat_dependant.query_params,
|
||||
flat_dependant.header_params,
|
||||
flat_dependant.cookie_params,
|
||||
)
|
||||
for field in _get_flat_fields_from_params(fields)
|
||||
]
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(
|
||||
route=route, method=method, operation_ids=operation_ids
|
||||
)
|
||||
parameters: list[dict[str, Any]] = []
|
||||
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
|
||||
security_definitions, operation_security = get_openapi_security_definitions(
|
||||
flat_dependant=flat_dependant
|
||||
)
|
||||
@@ -298,7 +307,7 @@ def get_openapi_path(
|
||||
if security_definitions:
|
||||
security_schemes.update(security_definitions)
|
||||
operation_parameters = _get_openapi_operation_parameters(
|
||||
dependant=route.dependant,
|
||||
flat_dependant=flat_dependant,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
@@ -458,7 +467,6 @@ def get_openapi_path(
|
||||
deep_dict_update(openapi_response, process_response)
|
||||
openapi_response["description"] = description
|
||||
http422 = "422"
|
||||
all_route_params = get_flat_params(route.dependant)
|
||||
if (all_route_params or route.body_field) and not any(
|
||||
status in operation["responses"]
|
||||
for status in [http422, "4XX", "default"]
|
||||
|
||||
+5
-8
@@ -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
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user