Upgrading from 0.9.x¶
0.10.0 rewrote the core, and 1.0 keeps that core. Coming from 0.9.x, the public API is not compatible and there is no compatibility layer; this page lists what changed and how to migrate. Then read What's New in 1.0 and What's New in 1.1 for what came after.
A unit is a base class¶
The @cocoa decorator and the Canary runtime container are both gone. Subclass Canary and
the unit runs its own life:
class Database(Canary):
config = dep(Config)
@start
async def connect(self) -> None: ...
@stop
async def close(self) -> None: ...
async with UserService() as service: # the whole graph comes up in dependency order
...
A base class rather than a decorator so that service.init(), async with service and
self.config are all visible to type checkers and IDEs — a decorator cannot widen a class's
static type.
dep() replaces deps=[...]¶
Dependencies are declared with a descriptor instead of listed in a decorator argument:
- You choose the attribute name. It is no longer the snake_case of the dependency's class name, so an implementation can be bound under an abstract name.
- The type is inferred.
self.sinkis aLoggingAlertSinkwith no extra annotation. - The declaration never needs evaluating, so it is unaffected by
from __future__ import annotations,if TYPE_CHECKINGor function-local classes. 0.9.x's class-level annotation injection failed silently in all three cases.
Phases are first-class¶
@init / @start / @stop are Phase instances — both decorators and engine arguments.
Adding a phase requires no registration:
after declares a predecessor, so calling start() without init() raises LifecycleError
instead of silently skipping a phase.
Overriding means overriding¶
Hooks resolve by attribute name, matching ordinary method semantics: a subclass overriding a
hook of the same name replaces it, and super() composes. 0.9.x deduplicated by function
identity, which turned an override into an addition.
Lifecycle methods can be overridden too:
The engine is two functions¶
Phases of your own are driven by two functions, which Canary's methods wrap:
enter(unit, phase)enters a phase across the dependency graph, dependencies first;leave(unit, phase)leaves it, the unit first.
The runtime container and its separate lifecycle state machine are gone; each unit keeps its own state in the scope's dependency graph. Dependency-chain depth is no longer bounded by Python's recursion limit (previously about 493). See Architecture.
Removed¶
@cocoa, theCanary(*roots)runtime container,canary.order,canary.instances,canary[Type],canary.lifespan,start_concurrency=, the assembly summary and the event-loop lag probe.LifecycleStateand its eight-state machine.- snake_case-by-class-name injection, class-level annotation injection,
Configand logger injection. canary_framework.webwas removed during 0.9.3 development and is not restored here.
Migration¶
| 0.9.x | 1.0 |
|---|---|
@cocoa(deps=[Database]) + self.database |
class X(Canary) + database = dep(Database) |
@on_init / @on_start / @on_stop |
@init / @start / @stop |
canary = Canary(Root) |
root = Root() |
await canary.init() / .start() / .stop() |
await root.init() / .start() / .stop() |
async with Canary(Root) as c |
async with Root() as root |
canary[Database] |
Read it from a unit that declares it, or scope_of(root).instances[Database] |
Canary(Root, start_concurrency=8) |
Concurrency is the default; nothing to configure |
app = FastAPI(lifespan=canary.lifespan) |
Write the three-line asynccontextmanager yourself |