What changed
Breaking redesign of the lifecycle API for release 0.4.0, driven by an architecture review of the library cross-checked against its real-world usage in the Valley app.
API
Component.start(self)/shutdown(self)no longer take asystemparameter, andSystem.start()takes no arguments — the awkwardsystem.start(system)bootstrap is gone and the README Quick Start now works exactly as written.- Dependency injection: dependencies declared with
using(["database"])are injected as instance attributes (self.database) beforestartruns. A mapping form allows aliasing:using({"db": "async_database"})→self.db.get_componentremains as a secondary accessor for code outside the system (e.g. web route handlers). - Async lifecycle: components may define
async def start/shutdown. Newastart()/ashutdown()coroutines for use inside running event loops (FastAPI lifespans); syncstart()transparently drivesasyncio.runwhen no loop is running. Pure-sync graphs never touch asyncio. - Rollback: if a component fails to start, already-started components are shut down in reverse order and
ComponentStartErroris raised (withcomponent_name,rollback_errors, and the original exception as__cause__). No more leaked connections on partial startup. - Shutdown resilience: every component is attempted even if one fails; failures are aggregated into an
ExceptionGroup. Shutdown is idempotent and reverses the actual start order; restart is supported. SystemStateenum replaces magic state strings; doublestart()raises.- Context-manager support:
with System(...)andasync with System(...).
Packaging
- Replaced networkx with stdlib
graphlib— zero runtime dependencies. - Lowered
requires-pythonfrom >=3.13 to >=3.11 (floor set by nativeExceptionGroup). - Ships
py.typed; fixed placeholder project URLs and a maintainer e-mail typo.
Tests / docs / CI
- Test suite rewritten and expanded from 11 to 42 tests, including a new async suite (plain pytest +
asyncio.run, no pytest-asyncio dependency), covering injection, aliasing, rollback, error aggregation, state machine, context managers, and nested systems. - README rewritten around the new API (dependencies, async, error handling sections); CHANGELOG entry for 0.4.0; CI test job now runs a 3.11–3.14 matrix.
Why
The 0.3.0 API documented in the README (system.start(), def start(self)) did not match the actual signatures, dependency declaration had no injection (consumers fell back to string-keyed get_component service-locator calls everywhere), a failed startup leaked resources of already-started components, and the sync-only lifecycle forced consumers with async resources into event-loop workarounds. networkx was also a heavy dependency for a small topological sort.
Notes for the reviewer
- This is intentionally breaking; downstream consumers pin
<0.4and will migrate separately. graphlib.CycleErrorsubclassesValueError, so existingexcept ValueErrorcycle handling keeps working (it is also re-exported aspython_components.CycleError).- Verified locally:
uv run pytest(42 passed),uv run ruff checkclean,uv buildwheel includespy.typed, plus an end-to-end smoke test of a mixed sync/async graph with rollback.
🤖 Generated with Claude Code