v1.6.0
Overview
v1.6.0 is a stability-focused release that fully resolves the long-standing cross-event-loop hang in #186, makes test isolation a first-class, zero-config experience, and hardens the runtime edges that previously failed in confusing ways — graceful shutdown, the background event loop, and the public import surface.
No hard API breaks: every new surface is additive and back-compat is preserved. Two default behaviors change on upgrade — see Upgrade notes below.
🐞 Issue #186 — cross-loop hangs are now fully fixed
1.5.0 only dropped the global dependency cache when its owner loop was closed, which missed the most common real-world topology: two loops alive at the same time (a session-scoped fixture loop + per-test loops, or an app loop + a worker/daemon loop). On a cache hit, a resource resolved on loop A — and any loop-bound primitive it holds (an asyncio Future/Lock/Event/Queue, an httpx/anyio connection pool, an asyncpg connection) — was handed to a still-live loop B, which deadlocks silently (or raises ... attached to a different loop on Python 3.13+). That's why use_cache=False was the only workaround.
1.6.0 partitions every piece of process-global state by its owning event loop:
- Partition the dependency cache per event loop (#248) — the default
async_get_injected_obj(..., use_cache=True)no longer serves a loop-A value to loop B. - Partition the exit-stack registry by owning loop (#249) — generator-dependency teardown (
aclose()) now runs on the loop its loop-bound primitives belong to, instead of whatever loop the manager happened to yield. - Partition
InjectableScope's cache per event loop (#250) — a manually-held scope reused across loops no longer hands a loop-A resource to loop B.
If you hit #186 (or worked around it with use_cache=False), you can drop the workaround after upgrading.
🚀 Features
- Ship a pytest plugin for automatic test isolation (#244) — installing the package now registers a pytest plugin (via the
pytest11entry point) that resets the process-global dependency cache and exit stacks around every test by default, so a value cached (or a generator left open) by one test can no longer leak into the next. Opt out globally withinjectable_autouse_cleanup = false, and request theinjectable_cleanupfixture only where you want it. - Add an import-time FastAPI compatibility guard (#243) —
fastapi-injectableresolves dependencies through FastAPI's privatesolve_dependenciesAPI. If an incompatible FastAPI release drifts that signature, you now get a clearFastAPICompatibilityErrornaming the installed version and the supported range (>=0.112.4,<1.0.0) at import time, instead of a crypticTypeErrordeep inside resolution.
🪲 Fixes
setup_graceful_shutdownnow terminates and chains existing signal handlers (#239) — previously the handler ran cleanup but swallowed the signal, so the process kept running afterSIGTERM/SIGINT. It now runs cleanup, invokes any handler already installed (instead of clobbering it), then propagates termination (KeyboardInterruptforSIGINT,SystemExit(0)otherwise). Adds aninstall_signal_handlersflag; when called off the main thread it logs a warning and registers only theatexitcleanup instead of raisingValueError.- Re-export
configure_loggingand the public exceptions from the package root (#242) —configure_logging,DependencyCleanupError, andRunCoroutineSyncMaxRetriesErrorare now importable directly fromfastapi_injectable(they were documented but raisedImportError).RunCoroutineSyncMaxRetriesErroris now actually raised on retry exhaustion, and subclassesTimeoutErrorso existingexcept TimeoutErrorhandlers keep working. - Re-check the loop field inside the lock to prevent leaked loops and threads (#245) — concurrent first-use of the background/isolated loop could create and leak multiple event loops and daemon threads; the loop is now created exactly once. Especially important on free-threaded builds where the GIL no longer serializes the unguarded read. Also ships clearer loop-strategy and resolution error messages (#246):
set_loop_strategy()raisesValueErroron unknown names instead of silently falling back tobackground_thread, an unregistered app points toregister_app()instead of a bareKeyError('app'), and "event loop is already running" points toasync_get_injected_obj(). - Accept classes and sync providers in
async_get_injected_obj(#237) — its typed surface now matchesget_injected_obj()(sync/async functions, classes, sync/async generators). Type-only change; no runtime behavior change. - Support the
@injectable(...)call form and keyword-only deps in the mypy plugin (#240) — fixes false positives from the bundled mypy plugin.
📚 Documentation
- Make README examples runnable and document the worker happy path (#241)
⚠️ Upgrade notes
No code changes are required to upgrade, but two default behaviors change:
- The pytest plugin auto-activates. After upgrading, your test suite resets
fastapi-injectable's global cache and exit stacks around every test by default. For most suites this is strictly better isolation; if you relied on cross-test caching, disable it with:[tool.pytest.ini_options] injectable_autouse_cleanup = false
setup_graceful_shutdownnow terminates the process after cleanup. If you depended on the previous (buggy) behavior where the process kept running afterSIGTERM/SIGINT, adjust accordingly. Passinstall_signal_handlers=Falseto register only theatexitcleanup.
📦 Dependencies
- build(deps): bump starlette from 1.0.1 to 1.3.1 in the uv group across 1 directory (#235) @dependabot[bot]
- build(deps): bump starlette from 0.50.0 to 1.0.1 in the uv group across 1 directory (#234) @dependabot[bot]
- build(deps): bump release-drafter/release-drafter from 7.2.1 to 7.3.1 (#233) @dependabot[bot]
Full Changelog: v1.5.0...v1.6.0