diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 71dc7bb74..72be2c6bd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,15 @@ hide: ## Latest Changes +### Features + +* ✨ Show a clear error on attempt to include router into itself. PR [#14258](https://github.com/fastapi/fastapi/pull/14258) by [@JavierSanchezCastro](https://github.com/JavierSanchezCastro). +* ✨ Replace `dict` by `Mapping` on `HTTPException.headers`. PR [#12997](https://github.com/fastapi/fastapi/pull/12997) by [@rijenkii](https://github.com/rijenkii). + +### Refactors + +* ♻️ Simplify reading files in memory, do it sequentially instead of (fake) parallel. PR [#14884](https://github.com/fastapi/fastapi/pull/14884) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 🔧 Configure `test` workflow to run tests with `inline-snapshot=review`. PR [#14876](https://github.com/fastapi/fastapi/pull/14876) by [@YuriiMotov](https://github.com/YuriiMotov). diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 98950103c..3d277c410 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,7 +1,7 @@ import dataclasses import inspect import sys -from collections.abc import Coroutine, Mapping, Sequence +from collections.abc import Mapping, Sequence from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy from dataclasses import dataclass @@ -15,7 +15,6 @@ from typing import ( cast, ) -import anyio from fastapi import params from fastapi._compat import ( ModelField, @@ -900,16 +899,8 @@ async def _extract_form_body( # For types assert isinstance(value, sequence_types) results: list[Union[bytes, str]] = [] - - async def process_fn( - fn: Callable[[], Coroutine[Any, Any, Any]], - ) -> None: - result = await fn() - results.append(result) # noqa: B023 - - async with anyio.create_task_group() as tg: - for sub_value in value: - tg.start_soon(process_fn, sub_value.read) + for sub_value in value: + results.append(await sub_value.read()) value = serialize_sequence_value(field=field, value=results) else: value = [ diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 62b4674de..9924d0e2b 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,4 +1,4 @@ -from collections.abc import Sequence +from collections.abc import Mapping, Sequence from typing import Annotated, Any, Optional, TypedDict, Union from annotated_doc import Doc @@ -68,7 +68,7 @@ class HTTPException(StarletteHTTPException): ), ] = None, headers: Annotated[ - Optional[dict[str, str]], + Optional[Mapping[str, str]], Doc( """ Any headers to send to the client in the response. diff --git a/fastapi/routing.py b/fastapi/routing.py index 16a89ef3e..ed873fda8 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1393,6 +1393,10 @@ class APIRouter(routing.Router): app.include_router(internal_router) ``` """ + assert self is not router, ( + "Cannot include the same APIRouter instance into itself. " + "Did you mean to include a different router?" + ) if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" assert not prefix.endswith("/"), ( diff --git a/tests/test_router_circular_import.py b/tests/test_router_circular_import.py new file mode 100644 index 000000000..492a26d00 --- /dev/null +++ b/tests/test_router_circular_import.py @@ -0,0 +1,12 @@ +import pytest +from fastapi import APIRouter + + +def test_router_circular_import(): + router = APIRouter() + + with pytest.raises( + AssertionError, + match="Cannot include the same APIRouter instance into itself. Did you mean to include a different router?", + ): + router.include_router(router)