10. Toolkit hook points¶
Date: 2026-08-29
Status¶
Accepted
Supersedes the build phase registry and the two container hook points of 8. Container hooks and the build phase registry. The rest of that record stands: hook points are still named by the method they call, still take one provider each, and are still generic protocols with a per-toolkit alias.
Context¶
Five hook points shipped. Two are used, and both by one provider, which supplies the application object and puts a stylesheet on it. Nothing declares the other three.
That is not merely low uptake. Reading back what the three were for shows two different mistakes.
configure_build exists to serve the phase registry. The registry was
introduced because build was a straight-line body with no way to add a step to
it, and configure_build is the point at which a provider calls
register_phase. Its whole purpose is letting a hook rearrange the build.
configure_session has no recorded rationale at all. It was added because
"after everything is built" sounded like a moment somebody would want.
The registry itself has no caller outside this project's own tests.
register_phase, unregister_phase, phases and sig_phase_complete are
public API that nothing uses.
Underneath both is a question ADR 0008 did not settle: what a hook is allowed to do. Left open, it grew to include changing what the container builds and in what order, which put lifecycle callbacks and component assembly in the same mechanism. The case that exposed it was a bundle wanting one object shared between several view components. Routing that through a hook was reachable and wrong: it is a dependency, and the container already resolves dependencies.
Decision¶
A hook never varies how a component is built. It is a callback at a fixed point in the toolkit's startup sequence, it runs only if declared, and it has no say in what the container assembles or in what order. Anything that shares state between components is a provider in the dependency graph.
The build sequence is a straight-line body again. register_phase,
unregister_phase, phases and _phases are removed, and with them
configure_build, which had nothing left to address. Four orderings are
load-bearing - the bus before everything, devices before presenters, every
component before wiring, every provider registration before any injection - and
a body states them where a registry only preserved them.
configure_session is removed. A component that wants to act on the session
it is part of asks for what it needs through the container's own injection,
which is checked, ordered, and visible to a reader of the class. A hook running
after the build is a second way to reach the same components with none of that.
Every hook point belongs to a toolkit. AppContainer declares none, and
naming one on it is refused. QtAppContainer declares four:
create_application, configure_application, during_build and
configure_main_view. A container for another toolkit declares the moments that
toolkit actually has, rather than inheriting moments defined for a different
one. This is what ADR 0008 already said about the objects a point receives,
applied to the set of points as well.
during_build is a span, not a moment. A splash screen appears before
anything is built, reports progress while it is, and closes once the window is
on screen, so it surrounds the sequence rather than sitting at a point in it.
The hook returns a context manager, entered before the first component is built
and left after the window is shown, yielding a callable that the container calls
with the name of each step.
Four properties follow, and are the reason for the shape:
- The close is guaranteed, a failed build included. A device that fails to
construct, leaving a splash on screen over an application with no window, is
the outcome a
show/closepair gets wrong and awithblock cannot. - The container never learns what a splash is. There is no
show,updateorclosevocabulary inAppContainer; it opens a block and calls a reporter. - No signal is needed.
sig_phase_completeis removed, and it was the onlypsygnalSignalonAppContainer, so__weakref__leaves its__slots__. - Nothing branches on whether a hook was declared. With no provider the
container enters a
nullcontextover a reporter that discards what it is given, so the build has one path.
The point is named for its shape rather than its first use: a busy cursor, a profiler or a logging context belongs at the same moment and is not a splash.
The span opens on run, not on build. build is what a test drives, and
a test wants no splash; run is the desktop entry point, and is where the
window is shown, which is where the span has to close.
One provider per point. Several hooks at one moment is not supported. There is no case for it, and the constraint it would impose is worth stating: one attribute name holds one provider, so a sequence would require a point to stop being named by the attribute that declares it.
Consequences¶
AppContainercalls no hook point. Ahookssection naming one is refused by name, listing what the container does call, which for the base container is nothing.- A session that themed its application or supplied its own
QApplicationis unaffected:create_applicationandconfigure_applicationkeep their names, their signatures and their behaviour. - A session that registered a build phase has no replacement, by intention. The work belongs in a component, which the container builds, orders and injects.
- The build reports its steps as
virtual container,devices,presenters,views,providers,wiringandinjection. Those names are now part of the public surface, since aduring_buildprovider displays them. - A
during_buildprovider needs noshutdown. Its context manager closes what it opened, and teardown for the other points is unchanged. - The container-bound protocol aliases are gone. Every remaining protocol is parameterised on a toolkit object, so the type-level tests that pinned a provider to a container implementation no longer have a subject.
- A container for a new toolkit is written the way
QtAppContaineris: declare the points in_hook_keys, and call each where the toolkit reaches it. That path is exercised by a container declaring two points of its own, so the machinery is tested without a toolkit present.