asyncgenerator fix

This commit is contained in:
John Lancaster
2026-07-30 20:51:16 -05:00
parent 70695ff218
commit 1ed5856db0
6 changed files with 19 additions and 19 deletions
+3 -3
View File
@@ -139,14 +139,14 @@ This example shows the ownership boundaries. Adapt state storage and dependency
```python
from contextlib import AsyncExitStack, asynccontextmanager
from collections.abc import AsyncIterator
from collections.abc import AsyncGeneratorr
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlmodel.ext.asyncio.session import AsyncSession
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
async with AsyncExitStack() as stack:
engine = create_async_engine(settings.database_url)
stack.push_async_callback(engine.dispose)
@@ -160,7 +160,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
yield
async def get_session() -> AsyncIterator[AsyncSession]:
async def get_session() -> AsyncGenerator[AsyncSession]:
async with app.state.session_factory() as session:
yield session
```
@@ -64,14 +64,14 @@ Resolve settings at the composition boundary and call `get_engine(settings.datab
The lifespan context manager only connects the cached resource to FastAPI ownership:
```python
from collections.abc import AsyncIterator
from collections.abc import AsyncGeneratorr
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
database_url = app.state.settings.database_url
engine = get_engine(database_url)
app.state.engine = engine
@@ -130,7 +130,7 @@ Otherwise, a later call can return a maker that still references the old engine
A small [`asynccontextmanager`](https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager) can make repository methods composable. It borrows an existing session when supplied; otherwise it creates and closes one from a supplied factory:
```python
from collections.abc import AsyncIterator
from collections.abc import AsyncGeneratorr
from contextlib import asynccontextmanager
@@ -139,7 +139,7 @@ async def session_scope(
*,
database_url: str,
session: AsyncSession | None = None,
) -> AsyncIterator[AsyncSession]:
) -> AsyncGenerator[AsyncSession]:
if session is not None:
yield session
return
@@ -172,7 +172,7 @@ async def transaction_scope(
*,
database_url: str,
session: AsyncSession | None = None,
) -> AsyncIterator[AsyncSession]:
) -> AsyncGenerator[AsyncSession]:
if session is not None:
if not session.in_transaction():
raise RuntimeError("A supplied session must have an active transaction")
@@ -277,7 +277,7 @@ This preserves atomicity without making repository objects hold mutable `AsyncSe
## Canonical FastAPI Dependency Pattern
```python
from collections.abc import AsyncIterator
from collections.abc import AsyncGenerator
from fastapi import Depends
from fastapi import Request
@@ -294,7 +294,7 @@ def resolve_session_factory(request: Request) -> SessionFactory:
async def get_db_session(
session_factory: SessionFactory = Depends(resolve_session_factory),
) -> AsyncIterator[AsyncSession]:
) -> AsyncGenerator[AsyncSession]:
async with session_factory() as session:
yield session
```
@@ -42,7 +42,7 @@ Use migrations to provision an integration database when migrations are part of
For tests that exercise code which calls `commit()`, start an outer transaction on one test connection. Bind the test `AsyncSession` to that connection and use `join_transaction_mode="create_savepoint"`. SQLAlchemy documents this as its test-suite pattern: session commits resolve a SAVEPOINT while fixture teardown rolls back the outer transaction.
```python
from collections.abc import AsyncIterator
from collections.abc import AsyncGeneratorr
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncEngine
@@ -50,7 +50,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
@pytest_asyncio.fixture
async def session(test_engine: AsyncEngine) -> AsyncIterator[AsyncSession]:
async def session(test_engine: AsyncEngine) -> AsyncGenerator[AsyncSession]:
async with test_engine.connect() as connection:
transaction = await connection.begin()
test_session = AsyncSession(
@@ -78,7 +78,7 @@ def app_with_test_session(
app: FastAPI,
session: AsyncSession,
) -> FastAPI:
async def get_test_session() -> AsyncIterator[AsyncSession]:
async def get_test_session() -> AsyncGenerator[AsyncSession]:
yield session
app.dependency_overrides[get_session] = get_test_session
@@ -104,7 +104,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine
@pytest_asyncio.fixture
async def test_engine() -> AsyncIterator[AsyncEngine]:
async def test_engine() -> AsyncGenerator[AsyncEngine]:
engine, _ = create_database("sqlite+aiosqlite://")
async with engine.begin() as connection:
await connection.run_sync(SQLModel.metadata.create_all)
@@ -126,7 +126,7 @@ def get_settings() -> Settings:
The argument-free [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) provider is appropriate here because both the project entry point and Uvicorn's zero-argument factory need process-lifetime access. Each reload or worker process gets its own settings instance. Do not add override arguments to `get_settings()`; inject a `Settings` instance directly into `create_app()` in tests or alternate composition roots. See the [Pydantic settings implementation guide](../../pydantic-settings/SKILL.md) for source precedence, independent settings boundaries, cache clearing, and runtime reload guidance.
```python title="src/my_app/main.py"
from collections.abc import AsyncIterator
from collections.abc import AsyncGeneratorr
from contextlib import asynccontextmanager
import uvicorn
@@ -137,7 +137,7 @@ from my_app.config import Settings, get_settings
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
app.state.ready = True
try:
yield
+3 -3
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from collections.abc import AsyncIterator
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import pytest
@@ -14,7 +14,7 @@ from personal_mcp.web.app import create_app
@pytest_asyncio.fixture
async def client() -> AsyncIterator[AsyncClient]:
async def client() -> AsyncGenerator[AsyncClient]:
"""Provides an AsyncClient bound to a fresh application instance."""
app = create_app()
async with AsyncClient(
@@ -30,7 +30,7 @@ def mcp_session_factory():
"""Provides an in-process context manager factory for MCP SDK sessions."""
@asynccontextmanager
async def create_session(*, initialize: bool = True) -> AsyncIterator[ClientSession]:
async def create_session(*, initialize: bool = True) -> AsyncGenerator[ClientSession]:
app = create_app()
mcp_url = f"http://testserver{app.state.settings.mounts.mcp}"
async with (