Skip to content

redsun.session

The session

Session

Session(
    config: Source | Sequence[Source] | None = None,
    *,
    log_level: int | str | None = None,
    profile: ProfileKind | None = None,
    profile_dir: str | Path | None = None,
)

Bases: BuildableSession

One running application, whose components are declared as annotations.

class MyApp(QtSession):
    config = "session.yaml"

    stage: AsDevice[MyStage]
    motor_ctrl: AsPresenter[MotorPresenter]
    motor_widget: Annotated[AsView[MotorView], Declare(step_size=5.0)]

    def wire(self) -> Iterator[Link]:
        yield self.motor_ctrl.sig_moved, self.motor_widget.update

An annotation is a declaration only if it names a layer, so a session may hold ordinary attributes alongside its components. The attribute name is both the component name and its configuration key; Alias and FromConfig override each. Reading a declared attribute on a built session gives the instance, typed by its annotation.

A component that failed to build is set on nothing, so reading its name raises AttributeError rather than answering None. Inside wire it reads as a stand-in instead, and a link naming it is skipped with a warning.

Prepare an empty session, to be filled by build.

config layers over whatever the class declares rather than replacing it, so a caller naming one key changes that key and leaves the rest. log_level, a logging constant or its name, sets the redsun logger's level; None leaves it as it is.

profile records where the session spends its time, with pyinstrument from the profile extra: "start" up to the end of build, "run" up to shutdown. The profile is an HTML file named after the run's log file, under profiles/<session> beside the logs, keeping the most recent runs; profile_dir puts it in that folder instead and keeps everything. Only the thread the session is made on is sampled: device connections, plans and services show as waits or not at all.

Raises:

Type Description
ValueError

If profile is neither "start" nor "run".

TypeError

If profile_dir is given without profile.

ImportError

If profile is given and pyinstrument is not installed.

config class-attribute

config: Source | Sequence[Source] | None = None

The configuration this session is declared with.

One source or several, each a path to a YAML file or a mapping already in hand. Several layer in the order given, and a subclass's layer over its bases', so a base holds what every session of an instrument shares and a subclass holds what makes it that session.

providers class-attribute

providers: list[type] = []

The shared services this session installs before any component.

Each is an ordinary class whose methods marked with redsun.provides put values in the session for components to ask for by type. A provider has no name, no layer and no wiring.

hook_points class-attribute

hook_points: Mapping[str, type] = {}

The points this session calls a hook at, by the protocol each demands.

Empty here: every hook point belongs to a toolkit, so a toolkit session such as redsun.qt.QtSession names its own.

frontend class-attribute

frontend: type[Frontend] = Frontend

The toolkit this session is built against.

Set by subclassing, as redsun.qt.QtSession does. The default attaches nothing and constrains no view.

services property

services: Mapping[str, Service]

The session's services, started or not.

transport property

transport: str

What the session's services speak, one for all of them.

Named once under services in the configuration; channel-access when it names nothing.

devices property

devices: Mapping[str, Device]

The devices that built successfully.

presenters property

presenters: Mapping[str, NamedComponent]

The presenters that built successfully.

views property

views: Mapping[str, AttachableComponent]

The views that built successfully, ready for a frontend to attach.

view_arguments property

view_arguments: Mapping[str, object]

What the session passes every view's constructor, besides its name.

Nothing here. A session bound to a toolkit passes what its views are built with, by keyword.

declarations property

declarations: Mapping[str, Declaration]

The declarations collected from the class.

settings property

settings: Settings

What this session remembers about how one user likes to run it.

Per user and per machine rather than part of the session file, and registered in the store, so an action asks for it by type.

Raises:

Type Description
RuntimeError

If read before build, which is where it is opened.

is_built property

is_built: bool

Whether build has completed.

hooks property

hooks: Mapping[str, object]

The hook provider this session installs at each point, built once.

A subclass firing a point calls this rather than resolving again, so that every point of one build acts on one set of providers. A provider with a shutdown method is shut down with the session, once however many points it serves, the last built first.

Raises:

Type Description
HookError

If a point is claimed by more than one provider, a provider cannot be built, or one does not implement the protocol its point calls.

name property

name: str

What this session is called.

The configuration's session, or this session's own class name when the configuration says nothing.

storage property

storage: StorageConfig

The session's storage configuration.

Raises:

Type Description
RuntimeError

If read before build.

path_provider property

path_provider: SessionPathProvider

The session's path provider, shared by every device taking one.

Raises:

Type Description
RuntimeError

If read before build.

callbacks property

callbacks: dict[str, CallbackType]

The document routers the session built, by name.

connections property

connections: list[Connection]

The links established so far.

unconnected property

unconnected: Unconnected

Ports of the built components that no connection reaches.

The complement of connections: what a component offers and nothing uses.

Raises:

Type Description
WiringError

If a component exposes two signals under one port name.

from_config classmethod

from_config(
    source: Source | Sequence[Source],
    *,
    log_level: int | str | None = None,
    profile: ProfileKind | None = None,
    profile_dir: str | Path | None = None,
) -> Self

Return a session described entirely by source.

Every component the configuration names is declared, its layer coming from the section it appears under, so a session needs no class of its own. The frontend key chooses the class to build on; naming none builds on this one, which is what a session with no toolkit wants.

The session comes back unbuilt, so that whatever the configuration cannot say is still said in Python before build runs.

Raises:

Type Description
ConfigurationError

If the configuration does not name the session: with no class of its own, it has no class name to fall back on.

ValueError

If the configuration names a frontend no session is built against.

ImportError

If it names one whose packages are not installed.

TypeError

If it names one this session is not built against.

make_store

make_store() -> Store

Return the registry this session builds its components out of.

Named after the session, and not entered in the process-wide registry Store.create keeps.

A session owning an application of its own overrides this to return that application's store, which app-model registers and Application.destroy frees.

build

build() -> Self

Run each step of BuildableSession in turn, announcing all but two.

Devices are built first and on their own; one that fails is logged and skipped. The components follow in layer order, and within a layer in the order they are built from one another.

A session built against a toolkit fills start_runtime and present rather than overriding this method. read_configuration and start_runtime run before the span opens and are not announced. A step that raises stops the build: the exception is logged, and shutdown gives back what the finished steps took before the exception leaves.

open_span

open_span() -> AbstractContextManager[
    Callable[[str], None]
]

Return the span the build announces its steps to.

By default this yields the reporter already in place, which does nothing.

on_release

on_release(release: Callable[[], None]) -> None

Register how to give something back, as the step takes it.

shutdown runs the releases registered so far, so a build that stopped halfway gives back only what it took.

wire

wire() -> Iterable[Link]

Yield the links of the session, each a signal and the slot it reaches.

def wire(self) -> Iterator[Link]:
    yield self.motor_ctrl.sig_moved, self.motor_widget.update
    yield self.stage.readback, self.motor_widget.on_reading

A signal is a psygnal signal of a component, or a signal of an ophyd-async device, whose reading dictionary the slot is called with. A slot is a bound method marked with slot, which may be a coroutine function, and is delivered on the thread it declares, then the one its class declares, then the one the frontend gives it.

links_between gives every link a pairing of two components makes, to yield from.

Every component that built exists by the time this runs. One that failed reads as a stand-in, and a link naming it is skipped with a warning. Yields nothing by default.

shutdown

shutdown() -> None

Run every registered release, in the reverse of the order taken.

Connections go first, so nothing is delivered to a component that is already finalizing. The releases follow: the shutdown method of every component that has one, then whatever a toolkit put in place. Calling it a second time, or on a session that was never built, runs nothing: a release is dropped as it runs. What the build made is forgotten, so the session can be built again.

serialize

serialize() -> dict[str, Any]

Return the configuration that would rebuild this session.

The merged configuration, holding the entry each built component asked for through redsun.Serializable. A component that implements none of it, that failed to build, or that asked for a key its constructor would refuse keeps the entry the session was built from, and no other component is affected by that.

Layered sources are merged before anything is built, so what comes back is one flat configuration whatever the session was built from.

write

write(path: str | Path) -> Path

Write the configuration that would rebuild this session to path.

One flat file whatever the session was built from, so it opens on its own with nothing to assemble first. Comments do not survive, the file being written rather than edited, and the keys come out in the order the merged configuration holds them.

Raises:

Type Description
ConfigurationInUse

If path is a source this session was built from.

has_changes

has_changes() -> bool

Whether any component asks to be written differently than at build.

The session compares against what each component serialized once the build finished, not against the configuration it was built from.

A value changed and changed back reads as unchanged, and a component that does not serialize itself never reports a change.

satisfying

satisfying(protocol: TypeForm[P]) -> dict[str, P]

Return the built components satisfying protocol, by name.

rejected

rejected(protocol: type) -> dict[str, list[str]]

Return why each component that nearly satisfies protocol does not.

disconnect_all

disconnect_all() -> None

Undo every connection and subscription made through this session.

Build steps

build runs these methods of Session in this order. A subclass replaces one by overriding it.

read_configuration

read_configuration() -> None

Merge the sources, install the hooks, read the declarations, open the logs.

start_runtime

start_runtime() -> None

Put in place what a component may not be constructed without.

The async backend, which a coroutine slot cannot be connected without. A session bound to a toolkit makes the toolkit's objects here as well, before the first component exists and before anything can watch the build.

start_services

start_services() -> None

Start the launched services together, and attach to the rest.

The step takes as long as the slowest service. A service that does not start is logged, and devices naming it are skipped. Each stop is a release, so shutdown stops services after every component, the last declared first. Before that it closes what the transport holds open to them, so no connection is cut under it and a rebuilt session reconnects at once.

A session whose configuration sets mock starts none: its devices connect to simulated backends, which reach no service.

build_devices

build_devices() -> None

Construct the devices, which are built from no other component.

They are built before the store opens, each from its own declaration alone. A device naming a service receives that service's prefix as prefix, and is skipped when the service is not declared, did not start, or gives no prefix.

connect_built_devices

connect_built_devices() -> None

Connect every autoconnect device at once.

To a simulated backend when the configuration sets mock, to what the device names otherwise. A device not connected within CONNECT_TIMEOUT is dropped and recorded as failed, like one that fails to build, and its shutdown is never called. One that connected has its shutdown registered as a release.

open_registry

open_registry() -> None

Open the store the components are built out of, and fill it.

The settings and the shared services come first and the questions the components ask are answered next, so that everything a constructor may reach for is registered before the first one runs.

build_presenters

build_presenters() -> None

Construct the presenter layer, in the order it depends in.

build_views

build_views() -> None

Construct the view layer, in the order it depends in.

setup_components

setup_components() -> None

Hand every component what another component owns.

Every presenter and view exists by now, so a setup may take a value another component shares, a census of the session, or a component itself. One that cannot run is reported and changes nothing else: the component keeps its place, its wiring and what its constructor made, and what its setup was going to assign is missing where it is used.

Raises:

Type Description
RuntimeError

If the registry step has not opened the store yet.

TypeError

If a setup asks for something nothing in the session declares.

seal

seal() -> None

Check what was built, then close the session to further building.

apply_wiring

apply_wiring() -> None

Make the links the class yields, then those the file lists, then its pairs.

A link made already is not made again.

Raises:

Type Description
WiringError

If wire yields nothing iterable, a link that cannot be made, or a pairing that connects nothing.

present

present() -> None

Assemble what was built into whatever shows it.

Nothing here: a session bound to no toolkit shows nothing, which is what a headless test wants. One bound to a toolkit puts its views where each asks to be.

log_summary

log_summary() -> None

Log what the build made, counted against what was declared.

Raises:

Type Description
BuildError

If the configuration sets strict and a component could not be built or set up, naming each one and why.

BuildableSession

Bases: Protocol

The steps a session's build runs, each one a method of its own.

build calls them in the order they are written here and does nothing else. A session bound to no toolkit answers start_runtime and present with nothing, and one bound to a toolkit fills exactly those two: what has to exist before a component can be constructed, and how what was built is assembled into whatever shows it.

Every step takes nothing and returns nothing. What a step needs it reads from the session, and what it leaves it leaves on the session. A step taking something that has to be given back registers how with on_release at the moment it takes it, and shutdown runs those in reverse, after a finished build or one that failed partway.

Inherit it rather than satisfying it structurally. The members are abstract, so a session missing one is refused when it is constructed and a type checker refuses it too.

BuildableSession lists the same methods; they are described above, with Session.

DesktopSession

Bases: BuildableSession, Protocol[WindowT_co]

A session whose views are attached to a window and shown on a screen.

The window's type is the parameter, since it is the toolkit's and no two toolkits share one: a session built on Qt satisfies DesktopSession[QMainWindow].

main_window is a property here, so it is a data descriptor in every implementer's method resolution order: answer it with a property of its own, never by assigning self.main_window in __init__.

main_window abstractmethod property

main_window: WindowT_co

The window the views are attached to.

run abstractmethod

run() -> NoReturn

Build, show the window, and hand over to the event loop.

Declaring components

AsDevice module-attribute

AsDevice: TypeAlias = Annotated[T, Layer.DEVICE]

An ophyd_async.core.Device, built before every other layer.

AsPresenter module-attribute

AsPresenter: TypeAlias = Annotated[T, Layer.PRESENTER]

A component holding application logic, taking name first.

Satisfies redsun.NamedComponent, and declares no placement.

AsView module-attribute

AsView: TypeAlias = Annotated[T, Layer.VIEW]

A component presenting an interface, taking name first.

Satisfies redsun.AttachableComponent, so it declares the redsun.Placement it asks the frontend to attach it at.

AsService module-attribute

AsService: TypeAlias = Annotated[Service, ServiceMark()]

A server the session's devices talk to, started before any component is built.

redsun.Launch describes a service the session runs, and redsun.Attach one already running elsewhere. Without either, the service comes from the session's services entry for it. An alias carrying the marker can be declared once and used by several sessions:

CameraIoc: TypeAlias = Annotated[
    AsService, Launch("mylab.iocs.camera", ready="Server startup complete.", prefix="CAM:")
]


class MyApp(Session):
    camera_ioc: CameraIoc

AsHook module-attribute

AsHook: TypeAlias = Annotated[T, Hook()]

A callback the session calls at one point of the toolkit's startup.

The attribute name is the point, redsun.Serves names them instead, and redsun.Declare carries the constructor arguments. A hook is not a component: it is never injected, nothing may depend on it, and it has no say in what the session builds.

Declare dataclass

Declare(**kwargs: Any)

Inline keyword arguments, overriding anything the configuration gives.

kwargs class-attribute instance-attribute

kwargs: dict[str, Any] = field(default_factory=dict)

Keyword arguments for the constructor of the component.

FromConfig dataclass

FromConfig(key: str)

Configuration key, when it cannot be the attribute name.

Needed only for keys that are not identifiers, or that deliberately differ from the attribute they are read into.

key instance-attribute

key: str

Key of the configuration entry the component is read from.

Alias dataclass

Alias(name: str)

Component name, when it must differ from the attribute name.

name instance-attribute

name: str

Name the component is known by.

Launch dataclass

Launch(
    module: str,
    *,
    ready: str | None = None,
    prefix: str | None = None,
    args: Sequence[str]
    | Mapping[str, ArgValue]
    | None = None,
    stop_timeout: float | None = None,
)

A service the session runs as python -m <module> <args>.

A keyword left as None is taken from the service's services entry, and otherwise from redsun.services.Service.

module instance-attribute

module: str

Module to run.

ready class-attribute instance-attribute

ready: str | None = None

Text of the output line marking the service ready.

prefix class-attribute instance-attribute

prefix: str | None = None

Prefix given to each device naming the service.

args class-attribute instance-attribute

args: Sequence[str] | Mapping[str, ArgValue] | None = None

Command-line arguments following the module, as a list or as options.

stop_timeout class-attribute instance-attribute

stop_timeout: float | None = None

Seconds each step of stopping waits for the process to exit.

Attach dataclass

Attach(prefix: str, *, address: str | None = None)

A service already running elsewhere, which only lends its prefix.

prefix instance-attribute

prefix: str

Prefix given to each device naming the service.

address class-attribute instance-attribute

address: str | None = None

Where the service answers, when the network search does not find it.

Serves dataclass

Serves(*moments: str)

The hook points one provider serves, when the attribute name is not one.

Declaring several is how one provider instance serves several points: the annotation names the class once, so one object is built for them all.

moments instance-attribute

moments: tuple[str, ...]

Names of the hook points served.

Layer

Bases: StrEnum

The layer a declared component belongs to.

Carried as the metadata of a declaration's annotation. redsun.session.components spells the three out. A member is its own name in a message, so it needs no .value, and its section is that name pluralised.

DEVICE class-attribute instance-attribute

DEVICE = 'device'

The layer of the devices.

PRESENTER class-attribute instance-attribute

PRESENTER = 'presenter'

The layer of the presenters.

VIEW class-attribute instance-attribute

VIEW = 'view'

The layer of the views.

section property

section: str

The configuration section this layer's components are declared under.

Declaration

Declaration(
    cls: type,
    name: str,
    kind: Layer,
    cfg_kwargs: dict[str, Any],
    *,
    attribute: str | None = None,
    source: str | None = None,
    refusal: Exception | None = None,
    frontend: type[Frontend] = Frontend,
)

A declared component, before and after it is built.

key is a distinct type per component name, so two instances of one class stay separable in a type-keyed graph. A device's service and autoconnect keywords are kept here, not passed to its constructor. A view's placement keyword is kept here too, read and checked against the frontend: placement is where the view attaches, or None when its class answers from a property and only the built instance can say. refusal is why the class cannot be built in its layer, or None; a refused declaration is never built. attribute is the annotation the session class declares it under and source the configuration entry its keywords are read from; both are the name unless a marker says otherwise.

Raises:

Type Description
TypeError

If a device's keywords name its service ambiguously, or give an autoconnect that is not a bool.

What a component is held to

NamedComponent

Bases: Protocol

A component that knows the name it was declared under.

Every component must satisfy this, presenters and views alike, and keep the name it was constructed with.

A plain instance attribute, a class attribute or a property all satisfy it.

name property

name: str

Identity key of the component.

AttachableComponent

Bases: NamedComponent, Protocol

A component the frontend can attach, and where it asks to go.

placement is the only difference between a view and a presenter, and it is what the frontend reads to attach the view. When it is answered from the class, a session refuses a view its frontend cannot attach before anything is built.

placement property

placement: Placement

Where the component asks to be attached when its declaration names no placement.

HasSetup

Bases: Protocol[SetupP]

A component taking what another component owns, once every one exists.

The session fills the parameters by type, the way it fills a constructor's, and calls it in a step of its own. It answers nothing: what it is given is the component's to keep.

HasShutdown

Bases: Protocol

A component finalizing itself when the session shuts down.

The session registers the method as a release when the component is built, so a component that has one is finalized without asking for it.

HasAsyncShutdown

Bases: Protocol

A component whose teardown is a coroutine, run on the session's loop.

Serializable

Bases: Protocol

A component that supplies the configuration entry rebuilding it.

serialize returns the keyword arguments the component's own entry would carry. The session writes them under that component's name and nowhere else, so a component reaches no entry but its own, and the next session reads back what this one wrote.

Implementing it is optional, and a component that leaves it out keeps whatever the configuration already said about it.

A value that moves on its own, such as a stage position or a frame count, is not a constructor argument and does not belong in the result.

serialize

serialize() -> Mapping[str, Any]

Return the keyword arguments this component would be rebuilt from.

Hook points

The protocol of each hook point, in the order a session reaches them.

CreatesApplication

Bases: Protocol[AppT_co]

Supplies the toolkit's application object instead of the container.

create_application abstractmethod

create_application(argv: list[str]) -> AppT_co

Return the application object the session runs on.

ConfiguresApplication

Bases: Protocol[AppT_contra]

Adjusts the application before any view is constructed.

configure_application abstractmethod

configure_application(app: AppT_contra) -> None

Act on app, which every view is about to be built against.

WrapsBuild

Bases: Protocol[AppT_contra]

Surrounds the build, from before the first component to after the window.

The one hook point that is a span, not a moment: a splash screen appears before anything is built, reports progress, and closes once the window is on screen.

during_build abstractmethod

during_build(
    app: AppT_contra,
) -> AbstractContextManager[Callable[[str], None]]

Return a context manager open for the whole build.

What it yields is called with the name of each step as it starts.

ConfiguresMainView

Bases: Protocol[ViewT_contra]

Adjusts the main window after it is built and before it is shown.

configure_main_view abstractmethod

configure_main_view(view: ViewT_contra) -> None

Act on view, the window the session is about to show.

ConfirmsClose

Bases: Protocol

Decides whether the session may close, and can refuse.

The session acts on this point's answer, where it only tells the other points what happened. False leaves the session running.

confirm_close abstractmethod

confirm_close() -> bool

Return whether the session may close now.

Frontends and placements

Frontend

The toolkit an application is built against.

requires pairs each redsun.Placement the frontend attaches with the toolkit type it demands of the view asking for it. A view asking for a placement the frontend does not list, or one whose class is not the type its placement demands, is refused before it is built. The placements themselves and the attaching live in the frontend's own package.

An empty table constrains nothing, which is what an application that names no toolkit gets.

thread_of says where the slots of a component run when the component does not say. A slot held for a thread is called there once that thread calls psygnal.emit_queued, which a session built on the frontend does from the toolkit's event loop.

check_view classmethod

check_view(view: type, where: str) -> None

Refuse a view class this frontend cannot build.

Runs where the view is declared, beside check_placement. Nothing is refused here; a frontend constraining how its views are constructed overrides it.

Raises:

Type Description
TypeError

In an override, naming what view lacks.

thread_of classmethod

thread_of(consumer: object) -> SlotThread

Return the thread the slots of consumer run on when nothing else says.

Asked after the slot itself and the class of consumer. None here: the slot runs on the thread that emits.

read_placement classmethod

read_placement(value: object) -> Placement

Return the placement a session file's value names, in this frontend's words.

A frontend that attaches views overrides it; this one reads no word.

Raises:

Type Description
ValueError

Always, naming the frontend.

check_placement classmethod

check_placement(
    view: type | object, placement: Placement, where: str
) -> None

Confirm the frontend attaches placement, and view is what it demands.

Parameters:

Name Type Description Default
view type | object

The class before anything is built and the instance afterwards. Either answers the question, the demand being on the class.

required
placement Placement

Where the view asks to be attached.

required
where str

How to name the view in a refusal, such as "view 'panel'".

required

Raises:

Type Description
TypeError

If the frontend lists what it attaches and this is not one of them, or if the view is not the toolkit type that placement demands.

Placement is described with redsun.view.

Settings

Settings

Settings(path: Path)

What a session remembers about how one user likes to run it.

Kept per user and per machine, apart from the session file.

A session builds one for itself and registers it, so an action asks for it by type. Reading a session that has never written one gives the defaults asked for; the file appears the first time something is set.

settings.set("ask_on_close", False)
settings.get("ask_on_close", True)

Read path if it is there, and remember where to write it back.

path property

path: Path

Where the settings are read from and written to.

for_session classmethod

for_session(name: str) -> Self

Return the settings of the session called name.

Sessions do not share a file.

get

get(key: str, default: Any = None) -> Any

Return what key was last set to, or default.

set

set(key: str, value: JsonValue) -> None

Remember value under key, and write the file.

Written as it is set rather than at shutdown.

Raises:

Type Description
TypeError

If value is not JSON-serializable.

Errors

The exceptions a session raises are in redsun.errors.

Build steps

BUILD_STEPS module-attribute

BUILD_STEPS: Final[tuple[str, ...]] = (
    "services",
    "devices",
    "connect",
    "registry",
    "presenters",
    "views",
    "setup",
    "seal",
    "wiring",
    "presentation",
    "report",
)

The steps a build reports, in order, to whatever is watching it.

A during_build hook is told one of these names as each step starts.

Session.build runs two steps before the first of these, reading the configuration and starting the toolkit's runtime, and reports neither.