Skip to content

Graph diagnostics

The data behind FrozenContainer.graph() and FrozenContainer.explain().

ProviderKey

ProviderKey = (
    type[object]
    | TokenKeyBase
    | str
    | GenericAlias
    | Underlying
)

What a provider can be bound and resolved under: a class, a Token, a name, or a parameterised generic.

The parameterised case needs no member of its own. A generic written in expression position — Repo[User], Reader[User] — has the static type type[Repo[User]], which type[object] already covers; the types.GenericAlias member covers the runtime object a builtin or ABC origin produces, such as list[Handler]. A deprecated typing alias (typing.List[X]) is a key at neither level: Container.freeze() rejects it and names the canonical spelling to write instead.

An Underlying is the fifth: the identity Container.decorate moves a decorated binding's inner form to, so the wrapper can occupy the public key.

Condition

Condition = bool | Callable[[], bool]

What when= accepts on a registration.

A bool is read where it is written. A callable is called once per Container.freeze(), with no arguments, and its result is read for truth — so a predicate over configuration or the environment is evaluated when the graph is built, not when a value is resolved.

Underlying dataclass

The key a decorated binding's inner form is registered under.

Container.decorate leaves the wrapper on the public key and moves what it wraps here, so both are ordinary nodes of the validated graph: the wrapper reaches its inner form over a real edge, and the inner form keeps the lifetime, the cache entry, and the teardown it had undecorated.

applied counts the decorators already applied below the public key, so the registered binding is Underlying(key, 0) and a second decorator over the same key sees Underlying(key, 1). Construct one to inspect a decorated binding — FrozenContainer.explain and DependencyGraph.find accept it — not to register anything.

Example
>>> from depin import Container, ProviderShape, Underlying
>>> class Store:
...     def get(self) -> str:
...         return 'plain'
>>> class Loud:
...     def __init__(self, inner: Store) -> None:
...         self.inner = inner
...     def get(self) -> str:
...         return self.inner.get().upper()
>>> di = Container().bind(Store).decorate(Store, Loud).freeze()
>>> di.graph().node(Underlying(Store, 0)).shape is ProviderShape.CLASS
True

DependencyGraph

The validated dependency graph, as data.

Returned by FrozenContainer.graph(). Nodes come in resolution order: a node never precedes one it depends on. The view describes the plan Container.freeze() validated, so an active FrozenContainer.override does not change it.

Example
>>> from depin import Container
>>> class Config: ...
>>> class Service:
...     def __init__(self, config: Config) -> None: ...
>>> di = Container().bind(Config).bind(Service).freeze()
>>> print(di.graph().mermaid())
graph LR
  n0["Config<br/>singleton, class"]
  n1["Service<br/>singleton, class"]
  n1 -->|config| n0

nodes property

nodes: tuple[GraphNode, ...]

Every provider in the graph, in resolution order.

roots property

roots: tuple[GraphNode, ...]

The nodes no other node depends on, in resolution order.

node

node(
    key: ProviderKey, *, tag: str | None = None
) -> GraphNode

Return the node bound under key and tag.

Raises:

Type Description
MissingProviderError

Nothing is bound under that key and tag. Use find to ask without raising.

find

find(
    key: ProviderKey, *, tag: str | None = None
) -> GraphNode | None

Return the node bound under key and tag, or None when nothing is.

dot

dot() -> str

Render the graph as a Graphviz digraph document.

mermaid

mermaid() -> str

Render the graph as a Mermaid graph LR document.

GraphNode dataclass

One provider in the validated graph.

dependencies is in the provider's own parameter order, which is what makes every rendering of the graph reproducible.

Example
>>> from depin import Container, ProviderShape
>>> class Config: ...
>>> di = Container().bind(Config).freeze()
>>> node = di.graph().node(Config)
>>> node.scope.value, node.shape is ProviderShape.CLASS
('singleton', True)

GraphEdge dataclass

One provider parameter and the binding identity it resolves to.

satisfied is false for a parameter that no binding provides, which Container.freeze() allows only when the parameter carries a default or admits None. has_default and optional say which, and has_default wins when both hold, because depin never replaces a value the author wrote.

Example
>>> from depin import Container
>>> class Config: ...
>>> class Cache: ...
>>> class Service:
...     def __init__(self, config: Config, cache: Cache | None) -> None: ...
>>> di = Container().bind(Config).bind(Service).freeze()
>>> bound, unbound = di.graph().node(Service).dependencies
>>> bound.parameter, bound.satisfied
('config', True)
>>> unbound.parameter, unbound.satisfied, unbound.optional, unbound.has_default
('cache', False, True, False)

ProviderShape

Bases: Enum

How a provider produces its value, and whether it owns a teardown.

Reported by GraphNode.shape. Container.freeze() infers it from the binding: a class, a factory's kind, or a value.

Attributes:

Name Type Description
CLASS

A class, instantiated with its resolved constructor arguments.

FUNCTION

A synchronous factory, called with its resolved arguments.

ASYNC_FUNCTION

A coroutine factory, awaited. Requires aresolve.

GENERATOR

A generator factory that yields once and resumes at teardown. Cannot be transient.

ASYNC_GENERATOR

An async generator factory that yields once and resumes at teardown. Requires aresolve and cannot be transient.

CONTEXT_MANAGER

A factory returning a context manager, entered on construction and exited at teardown. Cannot be transient.

ASYNC_CONTEXT_MANAGER

A factory returning an async context manager. Requires aresolve and cannot be transient.

VALUE

A value bound directly with Container.value; nothing is called.

FRAME

A value the active scope frame supplies, bound with Container.scope_value; nothing is called.

ALIAS

A second name for another binding, declared with Container.alias. Nothing is called and nothing is cached here — the target owns the value, its cache entry, and its teardown.

COLLECTION

A list of several bindings, declared with Container.collect. Nothing is cached here — each member keeps its own lifetime, and every resolution returns a new list over them.

Example
>>> from depin import Container, ProviderShape
>>> class Config: ...
>>> di = Container().bind(Config).freeze()
>>> di.graph().node(Config).shape is ProviderShape.CLASS
True