Container teardown¶
Container.close() and Container.aclose() tear down the
SINGLETON instances registered through the container's
resolver. The Container is also usable as a sync or async
context manager, in which case the teardown runs
automatically on __exit__ / __aexit__.
from tripack_container import Container
with Container() as container:
container.bind(Pool, make_pool, lifecycle=Lifecycle.SINGLETON)
container.resolve(Pool)
# pool.close() runs here, even if the body raised.
API¶
class Container:
def close(self) -> None: ...
async def aclose(self) -> None: ...
def __enter__(self) -> Self: ...
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
async def __aenter__(self) -> Self: ...
async def __aexit__(self, exc_type, exc_value, traceback) -> None: ...
Both close and aclose delegate to the runtime's
Resolver.close / Resolver.aclose, which:
- iterate the SINGLETON teardown registry in LIFO order so dependents close before what they depend on,
- collect exceptions raised by individual targets into a
single
ExceptionGroupso a failing teardown does not prevent the others from running, - are idempotent (a second call is a no-op via an internal
_closedflag), - skip async-only targets on the sync
closepath - reach them throughacloseinstead.
SINGLETON vs SCOPED teardown¶
Container.close / aclose only deal with SINGLETON
teardowns. SCOPED teardowns are owned by each Scope opened
via Container.scope() / Container.ascope() and run on
scope exit, not on container exit:
container = Container()
container.bind(Pool, make_pool, lifecycle=Lifecycle.SINGLETON)
container.bind(Session, make_session, lifecycle=Lifecycle.SCOPED)
with container: # opens the container's lifetime
with container.scope(): # opens a request scope
container.resolve(Session) # SCOPED: torn down on scope exit
container.resolve(Pool) # SINGLETON: torn down on container exit
Pool outlives every request's Session; the scope's exit
closes the session, the container's exit closes the pool.
Sync vs async path¶
Use the sync path when every SINGLETON in the container has a
sync close method, or when async-only targets are tolerable
to leak (the sync path skips them silently). Use the async path
when SINGLETONs include async-only aclose methods or when
the application's shutdown lives in an async context anyway.
# Sync application shutdown:
with Container() as container:
...
# Async application shutdown:
async with Container() as container:
...
The async path falls back to sync close for sync-only
targets, so it handles mixed registries correctly.
Combining with scopes¶
Container and Scope are independent context managers - they
can be nested freely:
async with Container() as container:
container.bind(Pool, make_pool, lifecycle=Lifecycle.SINGLETON)
container.bind(Session, make_session, lifecycle=Lifecycle.SCOPED)
async with container.ascope():
session = await container.aresolve(Session)
# session.aclose() ran here
async with container.ascope():
another = await container.aresolve(Session)
# another.aclose() ran here
# pool.aclose() runs here, after both scopes are gone.