31. Hook points take several providers and tolerate one that fails¶
Date: 2026-10-11
Status¶
Accepted
Supersedes "one provider per point" of 10. Toolkit hook points for the points whose answer the session does not act on. The rest of that record stands: a hook never changes what the session builds, and every hook point belongs to a toolkit.
Context¶
ADR 10 gave each hook point one provider and said there was no case for
more. The first style redsun shipped was that case. A style is a
configure_application provider, so a session that picked it could no
longer install a provider of its own at that point, to set the application's
icon for example.
Writing the style showed three more gaps:
- A provider that raised stopped the whole build. A style that could not write its icons kept an instrument from starting, while a component that fails to build is logged and skipped (ADR 11).
- What a provider owes the session was written only in a how-to. The first design of the style left its changes on the application when the session shut down, and nothing but a review noticed.
- A session file could name a provider
redsunships by a short name, but a plugin's manifest could hold short names too, and they were silently ignored.
Decision¶
- A hook point takes one provider or several. A session class says which
in
hook_points: a protocol alone means one provider, and the protocol wrapped inManymeans any number. A point takes one when the session uses what the provider returns:create_applicationandconfirm_close. The other points ofQtSessiontake several. - Providers run in a fixed order: those the class declares, in
declaration order, then those the session file lists, in the order of the
list.
Session.hooksmaps every point to a tuple in that order. - A provider that fails is skipped. If it cannot be built, or its hook
method raises, the session logs the error, drops the provider from every
point it serves, lists it in the build summary and carries on. A skipped
provider gets no
shutdowncall.strictturns a skipped provider into a failed build, as it does for a component. - Two points keep their own rule. A
create_applicationprovider that cannot be built stops the build, because a session told which application to run on must not start on another. Aconfirm_closeprovider that raises is skipped, and the session decides by its own rule. - A provider undoes in
shutdownwhat it changed on the application or outside its window. The rule is on the reference page and in the protocols' docstrings, andredsun.testingfails a test that leaves the application's stylesheet, style or font changed. - Only
redsunships short names. A plugin's manifest with ahookssection is invalid; a plugin's provider is named by its class path.
Before¶
One provider at the point, so a style and an icon needed one class doing both:
class MyStyleAndIcon(NapariStyle):
def configure_application(self, app: QApplication) -> None:
super().configure_application(app)
app.setWindowIcon(QIcon("lab.png"))
class MyApp(QtSession):
configure_application: AsHook[MyStyleAndIcon]
A session file had no way to say it. And session.hooks[point] was one
provider:
After¶
class MyIcon:
def configure_application(self, app: QApplication) -> None:
app.setWindowIcon(QIcon("lab.png"))
class MyApp(QtSession):
configure_application: AsHook[NapariStyle]
icon: Annotated[AsHook[MyIcon], Serves(QtHook.CONFIGURE_APPLICATION)]
Other options were rejected:
- A style as a concept of its own, with a
style:key in the session file and a build step. It would settle this case and make every later need a new key and a new step. With several providers at a point, a new need arrives as a provider. - Keeping one provider per point and subclassing, as in "Before". A session file cannot do it.
- Skipping a
create_applicationprovider that failed, and running on the session's own application. The session would start as a different program without saying so. - Letting plugins ship short names. Two plugins could ship the same name, and nobody has asked for it. It can be added later without breaking a file that is valid today.
Consequences¶
Session.hookschanged shape, which breaks code that read one provider from it.- A frontend written before this record declares its points as before, each
still taking one provider, but it now reads a tuple where it calls one.
Code such as
hook = self.hooks.get(point)followed by a check of its type finds a tuple and calls nothing, without an error. It becomes(provider,) = self.hooks[point], or a call toSession.call_hooks. - A frontend calls a point with
Session.call_hooks, which skips a provider that raises, or readsSession.hooksand reports a failure withSession.skip_hook. - A provider that changed the application and then failed is not asked to
undo it, since it gets no
shutdowncall. The log names the provider and the point, which is where to look. - A settings dialog for a style needs no new mechanism: one provider can
serve
configure_applicationandconfigure_main_view, and add a menu entry at the second.