Dependency Injection¶
ProviderRegistry, Scope, and lifecycle types.
di ¶
Public DI declaration surface.
__all__
module-attribute
¶
ProviderRegistry ¶
Mutable provider registry with seal guard and edge capture.
Structurally satisfies the taskq._di.types.ProviderRegistry
Protocol without explicit inheritance — the Protocol is
runtime_checkable for isinstance checks, but the concrete class
does not inherit from it to avoid forcing runtime_checkable
constraints on the implementation.
Source code in src/taskq/_di/registry.py
providers
property
¶
Shallow copy preventing external mutation.
Why: the internal map is dict[type, ProviderEntry[object]] per the erasure boundary. Returning a copy prevents callers from mutating registry state.
register_value ¶
Source code in src/taskq/_di/registry.py
register_factory ¶
Source code in src/taskq/_di/registry.py
register_class ¶
register_class(
type_: type[T],
scope: Scope,
*,
lifecycle: ProviderLifecycle | None = None,
) -> None
Source code in src/taskq/_di/registry.py
has_provider ¶
get ¶
Source code in src/taskq/_di/registry.py
validate ¶
validate(
actors: list[ActorRef[Any, Any]] | None = None,
rate_limit_registry: RateLimitRegistry | None = None,
) -> None
Walk all providers and actors; raise on first error; seal on success.
Pure graph-walk over registration-time metadata. Never invokes a
factory, calls a resolver, or performs await — the entire algorithm
is synchronous introspection on _providers and _dep_edges.
actors: explicit list of ActorRef instances whose DI parameter annotations are walked for MissingProvider checks and plan-cache population. Defaults to None (no actor walk — only provider→provider edges are validated).
rate_limit_registry: when provided, each actor's rate_limits
and reservations lists are checked against the registry's
dicts. Unknown names raise MissingProvider at startup.
When None (default), the name-check phase is
skipped entirely.
Why: ActorRef[Any, Any] is the sanctioned erasure¶
boundary for the heterogeneous actor registry — each ActorRef has¶
different P and R type parameters, and validate() does not need¶
per-actor narrowing. The same erasure is used at the existing API¶
boundary in worker/run.py:283 and cli.py:98.¶
Source code in src/taskq/_di/registry.py
LifecycleDetectionWarning ¶
Bases: UserWarning
Emitted for DI usage hazards detected during DI operations.
Specific timing varies per emission site and is documented at each emission. Fires for redundant call-site Scope overrides , sync close detection on registered classes, and other drift-prone API patterns. Subclass of UserWarning so pytest captures it under default filters via pytest.warns(LifecycleDetectionWarning).
Scope ¶
Bases: IntEnum
DI scope lifetime. Higher value = narrower scope.
PROCESS: worker process startup -> exit (singletons). THREAD: worker thread spawn -> thread close (placeholder for multi-thread worker). LOOP: worker loop start -> loop close (pools, long-lived clients). TRANSIENT: fresh per injection point - no cache.
ProviderLifecycle ¶
Bases: Enum
Provider lifecycle shapes for resolution-time dispatch.
registry ¶
Concrete ProviderRegistry — registration API and seal mechanics.
The public surface (register_value / register_factory / register_class,
has_provider, get[T], providers, validate) matches the
published block. The validate() method delegates the five-phase startup
validation algorithm to _di._validate.run_validation.
ProviderRegistry ¶
Mutable provider registry with seal guard and edge capture.
Structurally satisfies the taskq._di.types.ProviderRegistry
Protocol without explicit inheritance — the Protocol is
runtime_checkable for isinstance checks, but the concrete class
does not inherit from it to avoid forcing runtime_checkable
constraints on the implementation.
Source code in src/taskq/_di/registry.py
providers
property
¶
Shallow copy preventing external mutation.
Why: the internal map is dict[type, ProviderEntry[object]] per the erasure boundary. Returning a copy prevents callers from mutating registry state.
register_value ¶
Source code in src/taskq/_di/registry.py
register_factory ¶
Source code in src/taskq/_di/registry.py
register_class ¶
register_class(
type_: type[T],
scope: Scope,
*,
lifecycle: ProviderLifecycle | None = None,
) -> None
Source code in src/taskq/_di/registry.py
has_provider ¶
get ¶
Source code in src/taskq/_di/registry.py
validate ¶
validate(
actors: list[ActorRef[Any, Any]] | None = None,
rate_limit_registry: RateLimitRegistry | None = None,
) -> None
Walk all providers and actors; raise on first error; seal on success.
Pure graph-walk over registration-time metadata. Never invokes a
factory, calls a resolver, or performs await — the entire algorithm
is synchronous introspection on _providers and _dep_edges.
actors: explicit list of ActorRef instances whose DI parameter annotations are walked for MissingProvider checks and plan-cache population. Defaults to None (no actor walk — only provider→provider edges are validated).
rate_limit_registry: when provided, each actor's rate_limits
and reservations lists are checked against the registry's
dicts. Unknown names raise MissingProvider at startup.
When None (default), the name-check phase is
skipped entirely.
Why: ActorRef[Any, Any] is the sanctioned erasure¶
boundary for the heterogeneous actor registry — each ActorRef has¶
different P and R type parameters, and validate() does not need¶
per-actor narrowing. The same erasure is used at the existing API¶
boundary in worker/run.py:283 and cli.py:98.¶
Source code in src/taskq/_di/registry.py
scopes ¶
Concrete ScopeContainer with LIFO teardown and factory-shape dispatch.
Implements the log-and-continue teardown policy: each teardown callback
runs in its own try/except; failures are logged at ERROR; remaining
teardowns always fire. A parallel _teardowns list replaces
AsyncExitStack.aclose() which re-raises the first exception and
swallows the rest (research line 743-758).
ScopeContainer ¶
Concrete scope-lifetime container owning a cache, teardown list, and AsyncExitStack.
The container is responsible for ALL factory invocation, caching, and teardown registration. The solver engine NEVER calls a factory directly and NEVER touches an AsyncExitStack.
Source code in src/taskq/_di/scopes.py
last_cache_hit
property
¶
Whether the most recent get_or_create returned a cached value.
get_or_create
async
¶
Resolve type_ via entry, creating and caching the instance if needed.
Source code in src/taskq/_di/scopes.py
aclose
async
¶
Close the container with the log-and-continue teardown policy.
Source code in src/taskq/_di/scopes.py
ProcessScope ¶
Bases: ScopeContainer
PROCESS-lifetime scope — worker process startup → exit.
Source code in src/taskq/_di/scopes.py
bootstrap
async
¶
Resolve all PROCESS-scoped providers.
Why: no try/except around get_or_create — earlier providers' teardowns are already registered on self._teardowns; the caller's AsyncExitStack runs aclose() on unwind, which iterates whatever teardowns were registered before the failure. Partial-bootstrap leaks are not possible: every successful get_or_create has its teardown queued before the next get_or_create starts.
Source code in src/taskq/_di/scopes.py
shutdown
async
¶
ThreadScope ¶
Bases: ScopeContainer
THREAD-lifetime scope — placeholder for multi-thread workers (trivially empty in M3).
Source code in src/taskq/_di/scopes.py
bootstrap
async
¶
Resolve all THREAD-scoped providers (empty in M3 single-thread deployment).
Source code in src/taskq/_di/scopes.py
shutdown
async
¶
LoopScope ¶
Bases: ScopeContainer
LOOP-lifetime scope — worker loop start → loop close.
Source code in src/taskq/_di/scopes.py
bootstrap
async
¶
bootstrap(
registry: ProviderRegistry,
process_scope: ProcessScope,
thread_scope: ThreadScope | None = None,
) -> None
Resolve all LOOP-scoped providers.
Why: process_scope / thread_scope parameters are accepted for API completeness even though bootstrap doesn't read them directly — wider scopes' instances are reachable through the _resolver callable (which holds the registry + container map).
Source code in src/taskq/_di/scopes.py
shutdown
async
¶
resolved_cache ¶
Return a read-only view of the LOOP-scope resolved values.
The returned mapping is a live view onto the underlying cache —
providers resolved after this method is called (e.g., lazy
providers) become visible without re-fetching, and replacements
made via :meth:replace_value become visible to subsequent
.get() reads. Callers MUST NOT attempt to mutate the mapping;
doing so raises TypeError.
Stability invariant: once bootstrap returns, the registered
set of LOOP-scope provider types is FROZEN for the lifetime of
the loop. asyncpg.Connection values are stable references —
callers that read the mapping repeatedly (such as
SubJobEnqueuer.enqueue) assume the connection identity does
not change between dispatches, because the consumer's
per-dispatch transaction lifecycle requires the same connection
across both transaction-open and transaction-close.
asyncpg.Pool values are the one sanctioned exception: they
may be replaced mid-loop via :meth:replace_value (credential
hot-reload — see taskq.worker.deps.reload_credentials).
Pools are stateless acquisition factories, so swapping the
reference does not violate any transaction-identity invariant;
consumers that read the mapping per dispatch (rather than
capturing it once) always see the current pool.
Source code in src/taskq/_di/scopes.py
replace_value ¶
Replace the cached instance for type_.
The sanctioned mid-loop swap for VALUE providers whose resource
was hot-swapped externally — currently asyncpg.Pool after a
SIGHUP credential reload (taskq.worker.deps.reload_credentials
drains and closes the old pool; without this, DI-injected
db: asyncpg.Pool consumers would keep using a closed pool).
The old value is NOT torn down here — its lifecycle belongs to
the swapper. Only the cache entry is replaced; the (sealed)
registry is untouched. Raises :class:KeyError when type_ has
no cached value — replacing an absent entry would silently
insert a value no provider declared.
Source code in src/taskq/_di/scopes.py
get_or_create
async
¶
Source code in src/taskq/_di/scopes.py
ResolvedActorScope
dataclass
¶
The yielded value of build_actor_scope.
ctx — the JobContext supplied by the consumer (passthrough). di_kwargs — the DI-resolved kwargs dict for the actor function.
Usage::
async with build_actor_scope(...) as resolved:
await run_actor(job, resolved.ctx, **resolved.di_kwargs)
make_resolver ¶
make_resolver(
registry: ProviderRegistry,
scope_containers: dict[Scope, ScopeContainer],
) -> _Resolver
Construct the resolver callable expected by ScopeContainer.init.
The resolver invokes solve_dependencies with the registry and scope containers. The closure captures scope_containers by reference so the resolver sees containers added after its construction (the dict is populated incrementally as each scope container is bootstrapped).
Source code in src/taskq/_di/scopes.py
build_actor_scope
async
¶
build_actor_scope(
*,
registry: ProviderRegistry,
process_scope: ProcessScope,
thread_scope: ThreadScope,
loop_scope: LoopScope,
actor_func: Callable[..., Awaitable[object]],
actor_name: str,
passthrough_kwargs: dict[str, object],
) -> AsyncGenerator[ResolvedActorScope, None]
Per-invocation actor scope: opens TRANSIENT stack, resolves DI kwargs.
Yields ResolvedActorScope(ctx, di_kwargs). On exit (regardless of outcome), closes the TRANSIENT stack in LIFO order via the log-and-continue teardown policy.
passthrough_kwargs MUST contain the JobContext (key "ctx") and the validated payload (key "payload"). The consumer constructs both per-job and supplies them as passthrough; the registry's graph walk is configured to skip these parameter names, so they are never resolved from providers.
Source code in src/taskq/_di/scopes.py
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | |