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 ```python
from contextlib import AsyncExitStack, asynccontextmanager from contextlib import AsyncExitStack, asynccontextmanager
from collections.abc import AsyncIterator from collections.abc import AsyncGeneratorr
from fastapi import FastAPI from fastapi import FastAPI
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]: async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
async with AsyncExitStack() as stack: async with AsyncExitStack() as stack:
engine = create_async_engine(settings.database_url) engine = create_async_engine(settings.database_url)
stack.push_async_callback(engine.dispose) stack.push_async_callback(engine.dispose)
@@ -160,7 +160,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
yield yield
async def get_session() -> AsyncIterator[AsyncSession]: async def get_session() -> AsyncGenerator[AsyncSession]:
async with app.state.session_factory() as session: async with app.state.session_factory() as session:
yield 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: The lifespan context manager only connects the cached resource to FastAPI ownership:
```python ```python
from collections.abc import AsyncIterator from collections.abc import AsyncGeneratorr
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]: async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
database_url = app.state.settings.database_url database_url = app.state.settings.database_url
engine = get_engine(database_url) engine = get_engine(database_url)
app.state.engine = engine 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: 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 ```python
from collections.abc import AsyncIterator from collections.abc import AsyncGeneratorr
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@@ -139,7 +139,7 @@ async def session_scope(
*, *,
database_url: str, database_url: str,
session: AsyncSession | None = None, session: AsyncSession | None = None,
) -> AsyncIterator[AsyncSession]: ) -> AsyncGenerator[AsyncSession]:
if session is not None: if session is not None:
yield session yield session
return return
@@ -172,7 +172,7 @@ async def transaction_scope(
*, *,
database_url: str, database_url: str,
session: AsyncSession | None = None, session: AsyncSession | None = None,
) -> AsyncIterator[AsyncSession]: ) -> AsyncGenerator[AsyncSession]:
if session is not None: if session is not None:
if not session.in_transaction(): if not session.in_transaction():
raise RuntimeError("A supplied session must have an active 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 ## Canonical FastAPI Dependency Pattern
```python ```python
from collections.abc import AsyncIterator from collections.abc import AsyncGenerator
from fastapi import Depends from fastapi import Depends
from fastapi import Request from fastapi import Request
@@ -294,7 +294,7 @@ def resolve_session_factory(request: Request) -> SessionFactory:
async def get_db_session( async def get_db_session(
session_factory: SessionFactory = Depends(resolve_session_factory), session_factory: SessionFactory = Depends(resolve_session_factory),
) -> AsyncIterator[AsyncSession]: ) -> AsyncGenerator[AsyncSession]:
async with session_factory() as session: async with session_factory() as session:
yield 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. 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 ```python
from collections.abc import AsyncIterator from collections.abc import AsyncGeneratorr
import pytest_asyncio import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncEngine from sqlalchemy.ext.asyncio import AsyncEngine
@@ -50,7 +50,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
@pytest_asyncio.fixture @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: async with test_engine.connect() as connection:
transaction = await connection.begin() transaction = await connection.begin()
test_session = AsyncSession( test_session = AsyncSession(
@@ -78,7 +78,7 @@ def app_with_test_session(
app: FastAPI, app: FastAPI,
session: AsyncSession, session: AsyncSession,
) -> FastAPI: ) -> FastAPI:
async def get_test_session() -> AsyncIterator[AsyncSession]: async def get_test_session() -> AsyncGenerator[AsyncSession]:
yield session yield session
app.dependency_overrides[get_session] = get_test_session app.dependency_overrides[get_session] = get_test_session
@@ -104,7 +104,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine
@pytest_asyncio.fixture @pytest_asyncio.fixture
async def test_engine() -> AsyncIterator[AsyncEngine]: async def test_engine() -> AsyncGenerator[AsyncEngine]:
engine, _ = create_database("sqlite+aiosqlite://") engine, _ = create_database("sqlite+aiosqlite://")
async with engine.begin() as connection: async with engine.begin() as connection:
await connection.run_sync(SQLModel.metadata.create_all) 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. 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" ```python title="src/my_app/main.py"
from collections.abc import AsyncIterator from collections.abc import AsyncGeneratorr
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import uvicorn import uvicorn
@@ -137,7 +137,7 @@ from my_app.config import Settings, get_settings
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]: async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
app.state.ready = True app.state.ready = True
try: try:
yield yield
+3 -3
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import AsyncIterator from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import pytest import pytest
@@ -14,7 +14,7 @@ from personal_mcp.web.app import create_app
@pytest_asyncio.fixture @pytest_asyncio.fixture
async def client() -> AsyncIterator[AsyncClient]: async def client() -> AsyncGenerator[AsyncClient]:
"""Provides an AsyncClient bound to a fresh application instance.""" """Provides an AsyncClient bound to a fresh application instance."""
app = create_app() app = create_app()
async with AsyncClient( async with AsyncClient(
@@ -30,7 +30,7 @@ def mcp_session_factory():
"""Provides an in-process context manager factory for MCP SDK sessions.""" """Provides an in-process context manager factory for MCP SDK sessions."""
@asynccontextmanager @asynccontextmanager
async def create_session(*, initialize: bool = True) -> AsyncIterator[ClientSession]: async def create_session(*, initialize: bool = True) -> AsyncGenerator[ClientSession]:
app = create_app() app = create_app()
mcp_url = f"http://testserver{app.state.settings.mounts.mcp}" mcp_url = f"http://testserver{app.state.settings.mounts.mcp}"
async with ( async with (