mirror of
https://github.com/fastapi/fastapi.git
synced 2026-02-06 12:21:13 -05:00
Compare commits
11 Commits
translate-
...
0.128.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36985f5f25 | ||
|
|
661cdfb8a4 | ||
|
|
f6cc650a12 | ||
|
|
201feedd68 | ||
|
|
19f13ead4c | ||
|
|
01e85c03bd | ||
|
|
08233d7ffc | ||
|
|
fe8c33ea64 | ||
|
|
f9f7992604 | ||
|
|
8e50c55fd9 | ||
|
|
3b8b310eda |
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -49,31 +49,45 @@ jobs:
|
||||
matrix:
|
||||
os: [ windows-latest, macos-latest ]
|
||||
python-version: [ "3.14" ]
|
||||
uv-resolution:
|
||||
- highest
|
||||
starlette-src:
|
||||
- starlette-pypi
|
||||
- starlette-git
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.9"
|
||||
coverage: coverage
|
||||
uv-resolution: lowest-direct
|
||||
- os: macos-latest
|
||||
python-version: "3.10"
|
||||
coverage: coverage
|
||||
uv-resolution: highest
|
||||
- os: windows-latest
|
||||
python-version: "3.12"
|
||||
coverage: coverage
|
||||
uv-resolution: lowest-direct
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.13"
|
||||
coverage: coverage
|
||||
uv-resolution: highest
|
||||
# Ubuntu with 3.13 needs coverage for CodSpeed benchmarks
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.13"
|
||||
coverage: coverage
|
||||
uv-resolution: highest
|
||||
codspeed: codspeed
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.14"
|
||||
coverage: coverage
|
||||
uv-resolution: highest
|
||||
starlette-src: starlette-git
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
UV_RESOLUTION: ${{ matrix.uv-resolution }}
|
||||
STARLETTE_SRC: ${{ matrix.starlette-src }}
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
@@ -92,11 +106,14 @@ jobs:
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
- name: Install Dependencies
|
||||
run: uv sync --locked --no-dev --group tests --extra all
|
||||
run: uv sync --no-dev --group tests --extra all
|
||||
- name: Install Starlette from source
|
||||
if: matrix.starlette-src == 'starlette-git'
|
||||
run: uv pip install "git+https://github.com/Kludex/starlette@main"
|
||||
- run: mkdir coverage
|
||||
- name: Test
|
||||
if: matrix.codspeed != 'codspeed'
|
||||
run: uv run bash scripts/test.sh
|
||||
run: uv run --no-sync bash scripts/test.sh
|
||||
env:
|
||||
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
|
||||
@@ -108,7 +125,7 @@ jobs:
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
|
||||
with:
|
||||
mode: simulation
|
||||
run: uv run coverage run -m pytest tests/ --codspeed
|
||||
run: uv run --no-sync coverage run -m pytest tests/ --codspeed
|
||||
# Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
|
||||
- name: Store coverage files
|
||||
if: matrix.coverage == 'coverage'
|
||||
|
||||
@@ -7,6 +7,25 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.128.3
|
||||
|
||||
### Refactors
|
||||
|
||||
* ♻️ Re-implement `on_event` in FastAPI for compatibility with the next Starlette, while keeping backwards compatibility. PR [#14851](https://github.com/fastapi/fastapi/pull/14851) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Upgrades
|
||||
|
||||
* ⬆️ Upgrade Starlette supported version range to `starlette>=0.40.0,<1.0.0`. PR [#14853](https://github.com/fastapi/fastapi/pull/14853) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Translations
|
||||
|
||||
* 🌐 Update translations for ru (update-outdated). PR [#14834](https://github.com/fastapi/fastapi/pull/14834) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Internal
|
||||
|
||||
* 👷 Run tests with Starlette from git. PR [#14849](https://github.com/fastapi/fastapi/pull/14849) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Run tests with lower bound uv sync, upgrade `fastapi[all]` minimum dependencies: `ujson >=5.8.0`, `orjson >=3.9.3`. PR [#14846](https://github.com/fastapi/fastapi/pull/14846) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.128.2
|
||||
|
||||
### Features
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
checker(q="somequery")
|
||||
```
|
||||
|
||||
…и передаст возвращённое значение как значение зависимости в нашу *функцию-обработчике пути* в параметр `fixed_content_included`:
|
||||
…и передаст возвращённое значение как значение зависимости в параметр `fixed_content_included` нашей *функции-обработчика пути*:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *}
|
||||
|
||||
|
||||
@@ -6,13 +6,29 @@
|
||||
|
||||
## Использование `WSGIMiddleware` { #using-wsgimiddleware }
|
||||
|
||||
Нужно импортировать `WSGIMiddleware`.
|
||||
/// info | Информация
|
||||
|
||||
Для этого требуется установить `a2wsgi`, например с помощью `pip install a2wsgi`.
|
||||
|
||||
///
|
||||
|
||||
Нужно импортировать `WSGIMiddleware` из `a2wsgi`.
|
||||
|
||||
Затем оберните WSGI‑приложение (например, Flask) в middleware (Промежуточный слой).
|
||||
|
||||
После этого смонтируйте его на путь.
|
||||
|
||||
{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *}
|
||||
{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *}
|
||||
|
||||
/// note | Примечание
|
||||
|
||||
Ранее рекомендовалось использовать `WSGIMiddleware` из `fastapi.middleware.wsgi`, но теперь он помечен как устаревший.
|
||||
|
||||
Вместо него рекомендуется использовать пакет `a2wsgi`. Использование остаётся таким же.
|
||||
|
||||
Просто убедитесь, что пакет `a2wsgi` установлен, и импортируйте `WSGIMiddleware` из `a2wsgi`.
|
||||
|
||||
///
|
||||
|
||||
## Проверьте { #check-it }
|
||||
|
||||
|
||||
@@ -145,8 +145,6 @@ Successfully installed fastapi pydantic
|
||||
* Создайте файл `main.py` со следующим содержимым:
|
||||
|
||||
```Python
|
||||
from typing import Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
@@ -158,7 +156,7 @@ def read_root():
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
def read_item(item_id: int, q: Union[str, None] = None):
|
||||
def read_item(item_id: int, q: str | None = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
```
|
||||
|
||||
|
||||
@@ -161,8 +161,6 @@ $ pip install "fastapi[standard]"
|
||||
Создайте файл `main.py` со следующим содержимым:
|
||||
|
||||
```Python
|
||||
from typing import Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
@@ -174,7 +172,7 @@ def read_root():
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
def read_item(item_id: int, q: Union[str, None] = None):
|
||||
def read_item(item_id: int, q: str | None = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
```
|
||||
|
||||
@@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None):
|
||||
|
||||
Если ваш код использует `async` / `await`, используйте `async def`:
|
||||
|
||||
```Python hl_lines="9 14"
|
||||
from typing import Union
|
||||
|
||||
```Python hl_lines="7 12"
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
@@ -197,7 +193,7 @@ async def read_root():
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: int, q: Union[str, None] = None):
|
||||
async def read_item(item_id: int, q: str | None = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
```
|
||||
|
||||
@@ -288,9 +284,7 @@ INFO: Application startup complete.
|
||||
|
||||
Объявите тело запроса, используя стандартные типы Python, спасибо Pydantic.
|
||||
|
||||
```Python hl_lines="4 9-12 25-27"
|
||||
from typing import Union
|
||||
|
||||
```Python hl_lines="2 7-10 23-25"
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -300,7 +294,7 @@ app = FastAPI()
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
is_offer: Union[bool, None] = None
|
||||
is_offer: bool | None = None
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@@ -309,7 +303,7 @@ def read_root():
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
def read_item(item_id: int, q: Union[str, None] = None):
|
||||
def read_item(item_id: int, q: str | None = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
|
||||
|
||||
|
||||
@@ -101,13 +101,13 @@
|
||||
Поскольку по умолчанию, отдельные значения интерпретируются как query-параметры, вам не нужно явно добавлять `Query`, вы можете просто сделать так:
|
||||
|
||||
```Python
|
||||
q: Union[str, None] = None
|
||||
q: str | None = None
|
||||
```
|
||||
|
||||
Или в Python 3.10 и выше:
|
||||
Или в Python 3.9:
|
||||
|
||||
```Python
|
||||
q: str | None = None
|
||||
q: Union[str, None] = None
|
||||
```
|
||||
|
||||
Например:
|
||||
@@ -116,7 +116,7 @@ q: str | None = None
|
||||
|
||||
/// info | Информация
|
||||
|
||||
`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`,`Path` и других, которые вы увидите позже.
|
||||
`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`, `Path` и других, которые вы увидите позже.
|
||||
|
||||
///
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
Вы можете добавить параметры `summary` и `description`:
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *}
|
||||
{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *}
|
||||
|
||||
## Описание из строк документации { #description-from-docstring }
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
Вы можете указать описание ответа с помощью параметра `response_description`:
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *}
|
||||
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *}
|
||||
|
||||
/// info | Дополнительная информация
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
///
|
||||
|
||||
/// check
|
||||
/// check | Проверка
|
||||
|
||||
OpenAPI указывает, что каждой *операции пути* необходимо описание ответа.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.128.2"
|
||||
__version__ = "0.128.3"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import contextlib
|
||||
import email.message
|
||||
import functools
|
||||
import inspect
|
||||
import json
|
||||
import types
|
||||
from collections.abc import (
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Collection,
|
||||
Coroutine,
|
||||
Generator,
|
||||
Mapping,
|
||||
Sequence,
|
||||
)
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from contextlib import (
|
||||
AbstractAsyncContextManager,
|
||||
AbstractContextManager,
|
||||
AsyncExitStack,
|
||||
asynccontextmanager,
|
||||
)
|
||||
from enum import Enum, IntEnum
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
@@ -143,6 +152,50 @@ def websocket_session(
|
||||
return app
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
# Vendored from starlette.routing to avoid importing private symbols
|
||||
class _AsyncLiftContextManager(AbstractAsyncContextManager[_T]):
|
||||
"""
|
||||
Wraps a synchronous context manager to make it async.
|
||||
|
||||
This is vendored from Starlette to avoid importing private symbols.
|
||||
"""
|
||||
|
||||
def __init__(self, cm: AbstractContextManager[_T]) -> None:
|
||||
self._cm = cm
|
||||
|
||||
async def __aenter__(self) -> _T:
|
||||
return self._cm.__enter__()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[type[BaseException]],
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[types.TracebackType],
|
||||
) -> Optional[bool]:
|
||||
return self._cm.__exit__(exc_type, exc_value, traceback)
|
||||
|
||||
|
||||
# Vendored from starlette.routing to avoid importing private symbols
|
||||
def _wrap_gen_lifespan_context(
|
||||
lifespan_context: Callable[[Any], Generator[Any, Any, Any]],
|
||||
) -> Callable[[Any], AbstractAsyncContextManager[Any]]:
|
||||
"""
|
||||
Wrap a generator-based lifespan context into an async context manager.
|
||||
|
||||
This is vendored from Starlette to avoid importing private symbols.
|
||||
"""
|
||||
cmgr = contextlib.contextmanager(lifespan_context)
|
||||
|
||||
@functools.wraps(cmgr)
|
||||
def wrapper(app: Any) -> _AsyncLiftContextManager[Any]:
|
||||
return _AsyncLiftContextManager(cmgr(app))
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _merge_lifespan_context(
|
||||
original_context: Lifespan[Any], nested_context: Lifespan[Any]
|
||||
) -> Lifespan[Any]:
|
||||
@@ -160,6 +213,30 @@ def _merge_lifespan_context(
|
||||
return merged_lifespan # type: ignore[return-value]
|
||||
|
||||
|
||||
class _DefaultLifespan:
|
||||
"""
|
||||
Default lifespan context manager that runs on_startup and on_shutdown handlers.
|
||||
|
||||
This is a copy of the Starlette _DefaultLifespan class that was removed
|
||||
in Starlette. FastAPI keeps it to maintain backward compatibility with
|
||||
on_startup and on_shutdown event handlers.
|
||||
|
||||
Ref: https://github.com/Kludex/starlette/pull/3117
|
||||
"""
|
||||
|
||||
def __init__(self, router: "APIRouter") -> None:
|
||||
self._router = router
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self._router._startup()
|
||||
|
||||
async def __aexit__(self, *exc_info: object) -> None:
|
||||
await self._router._shutdown()
|
||||
|
||||
def __call__(self: _T, app: object) -> _T:
|
||||
return self
|
||||
|
||||
|
||||
# Cache for endpoint context to avoid re-extracting on every request
|
||||
_endpoint_context_cache: dict[int, EndpointContext] = {}
|
||||
|
||||
@@ -903,13 +980,33 @@ class APIRouter(routing.Router):
|
||||
),
|
||||
] = Default(generate_unique_id),
|
||||
) -> None:
|
||||
# Handle on_startup/on_shutdown locally since Starlette removed support
|
||||
# Ref: https://github.com/Kludex/starlette/pull/3117
|
||||
# TODO: deprecate this once the lifespan (or alternative) interface is improved
|
||||
self.on_startup: list[Callable[[], Any]] = (
|
||||
[] if on_startup is None else list(on_startup)
|
||||
)
|
||||
self.on_shutdown: list[Callable[[], Any]] = (
|
||||
[] if on_shutdown is None else list(on_shutdown)
|
||||
)
|
||||
|
||||
# Determine the lifespan context to use
|
||||
if lifespan is None:
|
||||
# Use the default lifespan that runs on_startup/on_shutdown handlers
|
||||
lifespan_context: Lifespan[Any] = _DefaultLifespan(self)
|
||||
elif inspect.isasyncgenfunction(lifespan):
|
||||
lifespan_context = asynccontextmanager(lifespan)
|
||||
elif inspect.isgeneratorfunction(lifespan):
|
||||
lifespan_context = _wrap_gen_lifespan_context(lifespan)
|
||||
else:
|
||||
lifespan_context = lifespan
|
||||
self.lifespan_context = lifespan_context
|
||||
|
||||
super().__init__(
|
||||
routes=routes,
|
||||
redirect_slashes=redirect_slashes,
|
||||
default=default,
|
||||
on_startup=on_startup,
|
||||
on_shutdown=on_shutdown,
|
||||
lifespan=lifespan,
|
||||
lifespan=lifespan_context,
|
||||
)
|
||||
if prefix:
|
||||
assert prefix.startswith("/"), "A path prefix must start with '/'"
|
||||
@@ -4473,6 +4570,58 @@ class APIRouter(routing.Router):
|
||||
generate_unique_id_function=generate_unique_id_function,
|
||||
)
|
||||
|
||||
# TODO: remove this once the lifespan (or alternative) interface is improved
|
||||
async def _startup(self) -> None:
|
||||
"""
|
||||
Run any `.on_startup` event handlers.
|
||||
|
||||
This method is kept for backward compatibility after Starlette removed
|
||||
support for on_startup/on_shutdown handlers.
|
||||
|
||||
Ref: https://github.com/Kludex/starlette/pull/3117
|
||||
"""
|
||||
for handler in self.on_startup:
|
||||
if is_async_callable(handler):
|
||||
await handler()
|
||||
else:
|
||||
handler()
|
||||
|
||||
# TODO: remove this once the lifespan (or alternative) interface is improved
|
||||
async def _shutdown(self) -> None:
|
||||
"""
|
||||
Run any `.on_shutdown` event handlers.
|
||||
|
||||
This method is kept for backward compatibility after Starlette removed
|
||||
support for on_startup/on_shutdown handlers.
|
||||
|
||||
Ref: https://github.com/Kludex/starlette/pull/3117
|
||||
"""
|
||||
for handler in self.on_shutdown:
|
||||
if is_async_callable(handler):
|
||||
await handler()
|
||||
else:
|
||||
handler()
|
||||
|
||||
# TODO: remove this once the lifespan (or alternative) interface is improved
|
||||
def add_event_handler(
|
||||
self,
|
||||
event_type: str,
|
||||
func: Callable[[], Any],
|
||||
) -> None:
|
||||
"""
|
||||
Add an event handler function for startup or shutdown.
|
||||
|
||||
This method is kept for backward compatibility after Starlette removed
|
||||
support for on_startup/on_shutdown handlers.
|
||||
|
||||
Ref: https://github.com/Kludex/starlette/pull/3117
|
||||
"""
|
||||
assert event_type in ("startup", "shutdown")
|
||||
if event_type == "startup":
|
||||
self.on_startup.append(func)
|
||||
else:
|
||||
self.on_shutdown.append(func)
|
||||
|
||||
@deprecated(
|
||||
"""
|
||||
on_event is deprecated, use lifespan event handlers instead.
|
||||
|
||||
@@ -43,7 +43,7 @@ classifiers = [
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
]
|
||||
dependencies = [
|
||||
"starlette>=0.40.0,<0.51.0",
|
||||
"starlette>=0.40.0,<1.0.0",
|
||||
"pydantic>=2.7.0",
|
||||
"typing-extensions>=4.8.0",
|
||||
"typing-inspection>=0.4.2",
|
||||
@@ -108,9 +108,9 @@ all = [
|
||||
# For Starlette's schema generation, would not be used with FastAPI
|
||||
"pyyaml >=5.3.1",
|
||||
# For UJSONResponse
|
||||
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0",
|
||||
"ujson >=5.8.0",
|
||||
# For ORJSONResponse
|
||||
"orjson >=3.2.1",
|
||||
"orjson >=3.9.3",
|
||||
# To validate email fields
|
||||
"email-validator >=2.0.0",
|
||||
# Uvicorn with uvloop
|
||||
@@ -167,7 +167,7 @@ tests = [
|
||||
"anyio[trio]>=3.2.1,<5.0.0",
|
||||
"coverage[toml]>=6.5.0,<8.0",
|
||||
"dirty-equals==0.9.0",
|
||||
"flask>=1.1.2,<4.0.0",
|
||||
"flask>=3.0.0,<4.0.0",
|
||||
"inline-snapshot>=0.21.1",
|
||||
"mypy==1.14.1",
|
||||
"pwdlib[argon2]>=0.2.1",
|
||||
|
||||
@@ -241,3 +241,79 @@ def test_merged_mixed_state_lifespans() -> None:
|
||||
|
||||
with TestClient(app) as client:
|
||||
assert client.app_state == {"router": True}
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings(
|
||||
r"ignore:\s*on_event is deprecated, use lifespan event handlers instead.*:DeprecationWarning"
|
||||
)
|
||||
def test_router_async_shutdown_handler(state: State) -> None:
|
||||
"""Test that async on_shutdown event handlers are called correctly, for coverage."""
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/")
|
||||
def main() -> dict[str, str]:
|
||||
return {"message": "Hello World"}
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def app_shutdown() -> None:
|
||||
state.app_shutdown = True
|
||||
|
||||
assert state.app_shutdown is False
|
||||
with TestClient(app) as client:
|
||||
assert state.app_shutdown is False
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert state.app_shutdown is True
|
||||
|
||||
|
||||
def test_router_sync_generator_lifespan(state: State) -> None:
|
||||
"""Test that a sync generator lifespan works via _wrap_gen_lifespan_context."""
|
||||
from collections.abc import Generator
|
||||
|
||||
def lifespan(app: FastAPI) -> Generator[None, None, None]:
|
||||
state.app_startup = True
|
||||
yield
|
||||
state.app_shutdown = True
|
||||
|
||||
app = FastAPI(lifespan=lifespan) # type: ignore[arg-type]
|
||||
|
||||
@app.get("/")
|
||||
def main() -> dict[str, str]:
|
||||
return {"message": "Hello World"}
|
||||
|
||||
assert state.app_startup is False
|
||||
assert state.app_shutdown is False
|
||||
with TestClient(app) as client:
|
||||
assert state.app_startup is True
|
||||
assert state.app_shutdown is False
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "Hello World"}
|
||||
assert state.app_startup is True
|
||||
assert state.app_shutdown is True
|
||||
|
||||
|
||||
def test_router_async_generator_lifespan(state: State) -> None:
|
||||
"""Test that an async generator lifespan (not wrapped) works."""
|
||||
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
state.app_startup = True
|
||||
yield
|
||||
state.app_shutdown = True
|
||||
|
||||
app = FastAPI(lifespan=lifespan) # type: ignore[arg-type]
|
||||
|
||||
@app.get("/")
|
||||
def main() -> dict[str, str]:
|
||||
return {"message": "Hello World"}
|
||||
|
||||
assert state.app_startup is False
|
||||
assert state.app_shutdown is False
|
||||
with TestClient(app) as client:
|
||||
assert state.app_startup is True
|
||||
assert state.app_shutdown is False
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "Hello World"}
|
||||
assert state.app_startup is True
|
||||
assert state.app_shutdown is True
|
||||
|
||||
Reference in New Issue
Block a user