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 ininstances, its object issetattred 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
configvalue 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
Implfiles pin actual types). - Port instances are wired by
setattr(parent, slot, child)โ so methods can referenceself.portXsafely once the root is built. - If
cls()raisesTypeError,buildretries by passingNonefor each declared__init__parameter โ convenient for classes with optional constructor args.