Skip to content

Adding sync component support and minor refactors

Latest

Choose a tag to compare

@lucassant95 lucassant95 released this 08 Jul 23:32
79bf966

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 a system parameter, and System.start() takes no arguments — the awkward system.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) before start runs. A mapping form allows aliasing: using({"db": "async_database"})self.db. get_component remains as a secondary accessor for code outside the system (e.g. web route handlers).
  • Async lifecycle: components may define async def start/shutdown. New astart()/ashutdown() coroutines for use inside running event loops (FastAPI lifespans); sync start() transparently drives asyncio.run when 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 ComponentStartError is raised (with component_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.
  • SystemState enum replaces magic state strings; double start() raises.
  • Context-manager support: with System(...) and async with System(...).

Packaging

  • Replaced networkx with stdlib graphlibzero runtime dependencies.
  • Lowered requires-python from >=3.13 to >=3.11 (floor set by native ExceptionGroup).
  • 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.4 and will migrate separately.
  • graphlib.CycleError subclasses ValueError, so existing except ValueError cycle handling keeps working (it is also re-exported as python_components.CycleError).
  • Verified locally: uv run pytest (42 passed), uv run ruff check clean, uv build wheel includes py.typed, plus an end-to-end smoke test of a mixed sync/async graph with rollback.

🤖 Generated with Claude Code