Summary
host_conftest.py's shared app fixture builds a single async engine that's reused across tests. asyncpg connections are bound to the event loop in which they're first acquired. With pytest-asyncio's default function-scoped event loops, a connection acquired in test N becomes stale in test N+1's loop, producing:
RuntimeError: Future <Future pending> attached to a different loop
Symptoms: tests that hit the DB pass individually but fail in a batch.
Workaround currently used
Every module's tests/conftest.py overrides the framework's db fixture with a per-test NullPool engine:
import pytest_asyncio
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import NullPool
@pytest_asyncio.fixture
async def db():
engine = create_async_engine(TEST_URL, poolclass=NullPool)
factory = async_sessionmaker(engine, expire_on_commit=False)
async with factory() as session:
try:
yield session
finally:
await session.rollback()
await engine.dispose()
NullPool avoids connection pooling entirely so every test gets a fresh connection in its own loop.
Suggested fix
In host_conftest.py, ship the db fixture as NullPool-backed by default:
@pytest_asyncio.fixture
async def db(app):
# Don't share app.state.sm.db.session_factory — that's pooled.
# Use a per-test engine to match pytest-asyncio's per-test loops.
engine = create_async_engine(_TEST_DATABASE_URL, poolclass=NullPool)
factory = async_sessionmaker(engine, expire_on_commit=False)
async with factory() as session:
try:
yield session
finally:
await session.rollback()
await engine.dispose()
Or document the override prominently and ship a helper users can call from their conftest.
Impact
Every multi-test module needs a NullPool conftest override. We carry this in 4 module conftests already (lacowiki_legends, lacowiki_sampling, lacowiki_validation, and lacowiki_datasets would need it too if it had >1 async test).
Environment
pytest-asyncio>=0.24 (function-scoped event loops by default)
asyncpg (loop-bound connections)
sqlalchemy>=2.0 async engine
Summary
host_conftest.py's sharedappfixture builds a single async engine that's reused across tests. asyncpg connections are bound to the event loop in which they're first acquired. With pytest-asyncio's default function-scoped event loops, a connection acquired in test N becomes stale in test N+1's loop, producing:Symptoms: tests that hit the DB pass individually but fail in a batch.
Workaround currently used
Every module's
tests/conftest.pyoverrides the framework'sdbfixture with a per-testNullPoolengine:NullPool avoids connection pooling entirely so every test gets a fresh connection in its own loop.
Suggested fix
In
host_conftest.py, ship thedbfixture asNullPool-backed by default:Or document the override prominently and ship a helper users can call from their conftest.
Impact
Every multi-test module needs a NullPool conftest override. We carry this in 4 module conftests already (
lacowiki_legends,lacowiki_sampling,lacowiki_validation, andlacowiki_datasetswould need it too if it had >1 async test).Environment
pytest-asyncio>=0.24(function-scoped event loops by default)asyncpg(loop-bound connections)sqlalchemy>=2.0async engine