Custom DI adapter
Note
If you installed python-cq[injection], you can skip this page. The default DI integration with python-injection is already wired up. This guide is only useful if you want to plug in a different DI framework (or none at all).
python-cq does not depend on any specific dependency injection container. Instead, it talks to DI through the DIAdapter protocol. Implement this protocol once and you can use the library with any container you already have in your project.
The CQ class
CQ ties together the handler registries and the DI adapter. The module-level decorators (command_handler, event_handler, query_handler) and bus factories (new_command_bus, new_event_bus, new_query_bus) all derive from a default CQ instance built at import time.
You create your own CQ instance to wire the library against a custom DIAdapter:
from cq import CQ, ContextCommandPipeline
cq = CQ(my_di_adapter).register_defaults()
command_handler = cq.command_handler
event_handler = cq.event_handler
query_handler = cq.query_handler
new_command_bus = cq.new_command_bus
new_event_bus = cq.new_event_bus
new_query_bus = cq.new_query_bus
When you build a ContextCommandPipeline against a non-default CQ, pass its DI adapter explicitly so the pipeline dispatches through the right buses:
If you use the default CQ, ContextCommandPipeline() (with no argument) is enough.
Implementing a DIAdapter
DIAdapter is a Protocol with four methods, three of them required:
from collections.abc import Awaitable, Callable
from cq import CQ, DIAdapter, CommandBus, EventBus, QueryBus
from typing import Any, AsyncContextManager
class MyDIAdapter(DIAdapter):
def command_scope(self) -> AsyncContextManager[None]:
"""
Return an async context manager that delimits a command dispatch.
Responsibilities:
1. Open a DI scope for the duration of the command.
2. Build a `RelatedEvents` instance inside that scope and make it
resolvable, so command handlers can inject it.
3. Silently ignore nested re-entrant activations (see below).
Re-entrancy: `command_scope` is opened twice for a single logical
command when a `ContextCommandPipeline` wraps a command dispatch.
Implementations must detect that case (for example, by checking a
contextvar) and skip opening a second scope.
"""
...
def lazy[T](self, tp: type[T]) -> Callable[[], Awaitable[T]]:
"""
Return a callable that, when called and awaited, resolves `tp` from
the container. Used to wire up bus references lazily so that buses
configured later in the import graph are still picked up.
"""
...
def register_defaults(
self,
command_bus: Callable[..., CommandBus[Any]],
event_bus: Callable[..., EventBus],
query_bus: Callable[..., QueryBus[Any]],
) -> None:
"""
Register the bus factories with the container so that handlers can
declare `CommandBus`, `EventBus`, or `QueryBus` as dependencies.
Optional: the default implementation is a no-op, which is fine if
your container does not need explicit registration.
"""
...
def wire[T](self, tp: type[T]) -> Callable[..., Awaitable[T]]:
"""
Return an async factory that instantiates `tp` with injected
dependencies. Used internally to build handler instances.
"""
...
cq = CQ(MyDIAdapter()).register_defaults()
The reference implementation for python-injection lives in cq.ext.injection.InjectionAdapter. It is a good starting point if you need to model your own adapter on a working example.