Skip to content

Use Case 3: Wiring & Runtime Injection

Scenario: you have concrete classes with annotated slots and you want a fully wired instance โ€” including runtime values (repos, clients) and config values (retries, labels, thresholds) โ€” without writing any assembly code.


๐Ÿ“ฆ What's New?

Component Description
build Recursively instantiates the port tree from annotations.
instances= Inject objects verbatim (never reconstructed) by name.
config= Propagate plain config values by name across the whole tree.

๐Ÿš€ Example

This mirrors tests/test_container.py exactly. Define leaf, mid-level, and root classes purely with annotations:

Python
from hexa import build


class _Repo:
    def __init__(self):
        self.name = "injected-repo"


class _Leaf:
    field: int = 0


class _Mid:
    leaf: _Leaf
    threshold: float = 0.0


class _Root:
    repo: _Repo
    mid: _Mid
    retries: int = 0
    label: str = ""


# Provide a runtime instance for a slot, and config values by name.
repo = _Repo()
root = build(
    _Root,
    instances={"repo": repo},
    config={"retries": 3, "label": "fast", "threshold": 2.5},
)

assert root.repo is repo            # injected verbatim, not reconstructed
assert root.retries == 3            # config applied
assert root.label == "fast"
assert root.mid.leaf is not None    # nested ports built recursively
assert root.mid.threshold == 2.5    # config propagates by name into nested nodes

Without any overrides, the same annotations still produce a working tree โ€” every slot is instantiated from its annotated type and config fields keep their class defaults:

Python
root = build(_Root)
assert isinstance(root.repo, _Repo)
assert isinstance(root.mid, _Mid)
assert isinstance(root.mid.leaf, _Leaf)
assert root.retries == 0 and root.label == ""

Optional / typing-generic config fields are handled the same way โ€” a str | None annotated attribute takes its config value when provided and defaults to the class default otherwise:

Python
class _OptionalConfig:
    trust_fallback: str | None = None
    starters: list | None = None
    port: _Leaf


built = build(_OptionalConfig, config={"trust_fallback": "amount"})
assert built.trust_fallback == "amount"
assert built.starters is None        # unrelated optional field keeps its default

๐Ÿ’ก Tips

  • instances= wins over reconstruction. If a name is in instances, its object is setattred verbatim at every node that annotates it. Use it for things that must not be re-created (repos, handlers, clients).
  • config= targets non-port annotated fields. Builtin, typing, and PEP 604-union (X | None) annotated fields are treated as config, applied by name and propagated to every node.
  • Abstract classes are never instantiated; they take a config value if one is provided (useful for defaults on CLI-style options).
  • Only concrete annotated types become wired ports. String forward references are skipped (the container assumes Impl files pin actual types).
  • Port instances are wired by setattr(parent, slot, child) โ€” so methods can reference self.portX safely once the root is built.
  • If cls() raises TypeError, build retries by passing None for each declared __init__ parameter โ€” convenient for classes with optional constructor args.