How to install hooks¶
A hook changes how the application itself starts and looks, rather than what it contains. With a hook you can create the Qt application object yourself, apply a style to every window, or show a splash screen while the session builds. The class you write for this is called a hook provider. It's an ordinary class, and it never changes which components the session builds.
A hook acts at one of a fixed set of moments, called hook points. Each point is named after the method the session calls on your provider. Some points take one provider and others take several, as the table below says.
You can install a provider from the session class or from the session file. Every example below shows both, as tabs, and picking a tab switches every tab on the site to the same choice.
Declare the provider with AsHook, as an attribute named after the hook
point.
Name the provider in the hooks section, under the name of the hook
point, with the path to import it from, or with its short name when
redsun ships it.
Pick a hook point¶
| point | receives | runs | providers | protocol |
|---|---|---|---|---|
create_application |
argv |
first, and only when no QApplication exists yet |
one | CreatesApplication |
configure_application |
the application | before any view is made | several | ConfiguresApplication |
during_build |
the application | around the build steps | several | WrapsBuild |
configure_main_view |
the main window | when the window is made, before it is shown | several | ConfiguresMainView |
confirm_close |
nothing | when the window is asked to close | one | ConfirmsClose |
A provider serves a point by having the method of its protocol. It may inherit from the protocol too, which lets a type checker hold it to the signature.
All five belong to the Qt frontend, so they only work on a
QtSession. A plain Session calls
no hook points and refuses a hook.
Write a provider¶
A provider is a class with the point's method:
from qtpy.QtWidgets import QApplication
class DarkTheme:
def __init__(self, accent: str = "#4c8eda") -> None:
self.accent = accent
def configure_application(self, app: QApplication) -> None:
app.setStyleSheet(f"QWidget {{ background: #202020; color: {self.accent}; }}")
It doesn't inherit from anything, because having the method is enough.
Install it¶
Use a provider redsun ships¶
redsun ships providers you can install without writing your own. A session
file names one by a short name instead of a class path, and the
session file reference lists the names.
Choose a style shows one of them in use.
Pass constructor arguments¶
Install several at one point¶
A point that takes several providers calls them one after another, so a style and a provider of your own can both act on the application.
The attribute named after the point declares one provider. Give every further one a name of its own and say which point it serves:
AppIcon here is a second provider with a configure_application method.
The providers of the class run first, in the order they are declared, and
then those of the file, in the order of the list. A point that takes one
provider refuses a second, wherever it was named.
Serve more than one hook point¶
A provider that keeps something between two points is installed at both as one object, so the state it keeps is shared.
Serves lists the points, and the attribute name is then free:
Anchor the entry and alias it:
&theme names the entry and *theme reuses it, so both keys hold one
provider. Two separate entries with the same provider and arguments are
refused, since the file cannot say whether you meant one object or two.
Show a splash screen during the build¶
during_build covers a span of time, not a moment, so it returns a context
manager that the session enters before the first build step and closes after
the last. The session calls what the manager yields with each step's name as
the step starts:
from collections.abc import Callable, Generator
from contextlib import contextmanager
from qtpy.QtGui import QPixmap
from qtpy.QtWidgets import QApplication, QSplashScreen
class Splash:
def __init__(self, image: str) -> None:
self.pixmap = QPixmap(image)
if self.pixmap.isNull():
raise ValueError(f"no image at {image!r}")
@contextmanager
def during_build(self, app: QApplication) -> Generator[Callable[[str], None]]:
screen = QSplashScreen(self.pixmap)
screen.show()
app.processEvents()
def report(step: str) -> None:
screen.showMessage(step)
app.processEvents()
try:
yield report
finally:
screen.close()
Check the image in the constructor, because a missing file gives an empty
QPixmap instead of an error, and a splash that shows nothing.
The steps reported are the members of BuildStep, in order:
Size a progress bar from len(BuildStep) rather than counting by hand, so
it stays right when the steps change. Each step is reported when it starts,
so fill the bar to the end after the yield. The finally closes the splash
even when the build fails.
Undo what a hook did¶
A provider puts back what it changed, so that nothing of it outlives the
session. Give it a shutdown method. The session calls it when it shuts
down, once per provider however many points it serves, the last built first:
class DarkTheme:
def configure_application(self, app: QApplication) -> None:
self._app = app
self._previous = app.styleSheet()
app.setStyleSheet("...")
def shutdown(self) -> None:
self._app.setStyleSheet(self._previous)
A during_build provider needs none: its context manager closes what it
opened.
Read a failure¶
A provider that fails doesn't stop the session, but a mistake in how you declared it does.
When a provider can't be built, or its hook method raises, the session logs
the error, skips the provider at every point it serves, and carries on. The
build summary lists it as Hooks skipped: DarkTheme (configure_application),
and in a Qt session the failures button of the status bar lists it too. A
skipped provider gets no shutdown call. A provider whose constructor
raised is logged as cannot construct hook provider 'X' ....
Two cases stop the build instead. A create_application provider that
fails stops it, because the session can't run on another application. And
with strict: true in the session file, every skipped provider stops it.
A wrong declaration raises HookError and names the
point. In a session file, an entry of the wrong shape is found when the file
is read and raised as ConfigurationError,
located as hooks.<point>.<key>. An entry in a list is located by its index,
as hooks.configure_application[1].kwargs.
| message | cause |
|---|---|
MyApp.x declares a hook at 'x', which is not a hook point MyApp calls; ... |
the attribute name is not a point this session calls |
hooks key 'x' is not a hook point MyApp calls; ... |
the same, in the file |
hook provider 'X' at 'y' does not implement Z |
the method is missing or misspelled |
the provider of 'create_application' failed, and a session does not start on another application: ... |
the provider of that point could not be built |
hooks.x.provider: Field required |
provider is missing from the file entry |
hooks.x.name: Extra inputs are not permitted |
an argument was written beside provider instead of under kwargs |
unknown hook provider 'x'; built-in names: ... |
the short name is not one redsun ships; check its spelling against the list |
hook provider 'p' is named twice, at 'a' and at 'b', with the same keys |
two identical entries; anchor one, or change the arguments |
hooks entry 'x' lists one provider twice |
one entry was aliased twice in the list of a point; list it once |
hook point 'x' takes one provider, and MyApp and its configuration name 2 between them |
the point takes one provider; remove all but one |
See also¶
- Hook points lists the points, the order providers run in, and what a provider owes the session.
- Frontends shows the points in the life of a Qt session.
- ADR 10 records why the points are what they are, and ADR 31 why a point takes several providers and a failing one is skipped.
- Wire components together connects components, which hooks do not do.