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. 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. The values stored under each type key (e.g. an
asyncpg.Connection) are expected to be stable references —
callers that read the mapping repeatedly (such as
SubJobEnqueuer.enqueue) assume the connection identity does
not change between dispatches. If a future feature needs to
swap a LOOP-scope value mid-loop (e.g., reconnect after a pool
reset), it must coordinate with consumers of this mapping
because the consumer's per-dispatch transaction lifecycle
requires the same connection across both transaction-open and
transaction-close.
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
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 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 | |