11. Tolerating a component that fails to build¶
Date: 2026-09-03
Status¶
Accepted. Supersedes in part 3. Structural subtyping for presenters and views, whose rule that a presenter or view failing to build re-raises and ends the build no longer holds. What remains in force there is the dual gate: the constructor's positional shape is checked at declaration, the protocol on the built instance.
Context¶
An application is a set of components, and a session runs against hardware that is not always there. A device failing to build was already logged and skipped, so that an unplugged instrument did not end the session. A presenter or view failing to build re-raised, so that one panel refusing to construct left the user with no window at all and a traceback in a terminal they may not have been watching.
The asymmetry was not the only problem. Nothing downstream of the device phase tolerated a component that was not there either:
| phase | reads components through | a component that failed |
|---|---|---|
_build_devices |
declarations | logged, skipped |
_build_presenters |
declarations | logged, re-raised |
_build_views |
declarations | logged, re-raised |
_register_providers |
_instance_of |
RuntimeError |
_apply_wiring |
_instance_of, then wire() |
RuntimeError |
_inject_dependencies |
_instance_of |
RuntimeError |
devices, presenters and views read the same way, so the one path that was
meant to survive a failure did not: a container that logged "Failed to build
device 'bad'" and carried on raised RuntimeError on the next read of
app.devices.
Decision¶
A component that fails to build is recorded and skipped, whatever layer it
belongs to. The build logs the failure at ERROR against the component's
name, keeps the exception under that name in _failed, and goes on to the next
component. build returns, and the application starts with the components it
has.
Reading built components walks what was built, not what was declared.
_built_of returns the instances a mapping of declarations produced, dropping
the entries with none. devices, presenters and views return that, and so
do the provider, wiring, injection and presenter-shutdown phases. The three
mappings can therefore be shorter than the declarations: len(app.views) is no
longer the number of declare_view calls.
A wire body naming a component that failed does not end the wiring.
Reading a declare_* attribute of a component that failed gives a stand-in
that answers any port with another stand-in, and connect returns None
without connecting when either end is one. The connections that name only
components that built are made. connect returns Connection | None as a
result, because a link that was not made has no Connection to report.
Only a component the build failed on resolves to a stand-in. Reading an attribute of a component that built and naming a port it does not have still raises, so a typo is still an error.
A wiring rule naming a component that failed is skipped, not fatal.
VirtualContainer._resolve_port raises ComponentNotBuilt, a WiringError
carrying the component name; AppContainer._apply_wiring_config catches it,
warns, and goes on to the next rule when the name is one the build failed on.
A rule naming a component that was never declared, one naming a port a built
component does not expose, a signature mismatch and a malformed rule all stay
fatal, because none of them is a component that failed.
The build's closing line says what is missing. It counts what was built
against what was declared, names the components that failed, and is logged at
WARNING rather than INFO when there are any:
Container built: 3/4 devices, 2/2 presenters, 4/5 views
Not built: bad_camera (device), log_panel (view)
Consequences¶
- A session starts with an instrument unplugged or a panel refusing to construct, and the log says which part is missing.
- A
wirebody written before this change keeps working: every connection it declares is still attempted, and only the ones touching a component that failed are dropped. AppContainer.connectreturnsConnection | None. A caller reading the returned link has to narrow it.- The exceptions are kept by name but are not public. A
failuresproperty is additive and can be added when something needs to read them. - A view raising part-way through its own construction leaves behind whatever widgets it had already created. The container never receives a reference to them and cannot destroy them; a view parents its widgets at construction so that an early return leaves them owned.