Skip to content

v1.6.0

Choose a tag to compare

@github-actions github-actions released this 21 Jun 07:22
· 9 commits to refs/heads/main since this release
ef1bde7

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 pytest11 entry 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 with injectable_autouse_cleanup = false, and request the injectable_cleanup fixture only where you want it.
  • Add an import-time FastAPI compatibility guard (#243) — fastapi-injectable resolves dependencies through FastAPI's private solve_dependencies API. If an incompatible FastAPI release drifts that signature, you now get a clear FastAPICompatibilityError naming the installed version and the supported range (>=0.112.4,<1.0.0) at import time, instead of a cryptic TypeError deep inside resolution.

🪲 Fixes

  • setup_graceful_shutdown now terminates and chains existing signal handlers (#239) — previously the handler ran cleanup but swallowed the signal, so the process kept running after SIGTERM/SIGINT. It now runs cleanup, invokes any handler already installed (instead of clobbering it), then propagates termination (KeyboardInterrupt for SIGINT, SystemExit(0) otherwise). Adds an install_signal_handlers flag; when called off the main thread it logs a warning and registers only the atexit cleanup instead of raising ValueError.
  • Re-export configure_logging and the public exceptions from the package root (#242) — configure_logging, DependencyCleanupError, and RunCoroutineSyncMaxRetriesError are now importable directly from fastapi_injectable (they were documented but raised ImportError). RunCoroutineSyncMaxRetriesError is now actually raised on retry exhaustion, and subclasses TimeoutError so existing except TimeoutError handlers 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() raises ValueError on unknown names instead of silently falling back to background_thread, an unregistered app points to register_app() instead of a bare KeyError('app'), and "event loop is already running" points to async_get_injected_obj().
  • Accept classes and sync providers in async_get_injected_obj (#237) — its typed surface now matches get_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:

  1. 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
  2. setup_graceful_shutdown now terminates the process after cleanup. If you depended on the previous (buggy) behavior where the process kept running after SIGTERM/SIGINT, adjust accordingly. Pass install_signal_handlers=False to register only the atexit cleanup.

📦 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