Skip to content

How a component asks the session what it holds

A component usually asks for one thing by its type. Sometimes it needs an answer instead, such as "which components can be reset?", which depends on what the session file puts in the session. It asks in setup, with a protocol describing what it looks for.

Asking with an annotation

How you annotate the parameter says what you're asking for:

annotation what you get
P the one component, or shared value, that satisfies P
P \| None = None that one, or None if nothing does
Mapping[str, P] every component satisfying P, by name
from collections.abc import Mapping
from typing import Protocol

from redsun import slot


class Resettable(Protocol):
    def reset(self) -> None: ...


class SessionPresenter:
    def __init__(self, name: str) -> None:
        self.name = name

    def setup(self, resettable: Mapping[str, Resettable]) -> None:
        self.resettable = resettable

    @slot
    def reset_all(self) -> None:
        for component in self.resettable.values():
            component.reset()

Whatever the session file puts in the session, reset_all resets all of it, and nobody has to keep a list up to date.

ADR 17 records why the annotation carries the question.

How a component matches

A component matches when it has every member the protocol lists and each method accepts every call the protocol allows. Step through who answers and who comes close:

Which components answer Mapping[str, Resettable]

SessionPresenter.setup asks for every component with a reset() it can call.

plotno resetloosereset(hard)detectorreset(force=False)motorreset()SessionPresenter.setupresettable:Mapping[str, Resettable]
plotno resetloosereset(hard)detectorreset(force=False)motorreset()SessionPresenter.setupresettable:Mapping[str, Resettable]

motor and detector answer. detector's reset takes an extra parameter, but it has a default, so reset() still works.

plotno resetloosereset(hard)detectorreset(force=False)An extra parameter with a default still matches, because reset() can still be called.motorreset()SessionPresenter.setupresettable:Mapping[str, Resettable] An extra parameter with a default still matches, because reset() can still be called.
plotno resetloosereset(hard)detectorreset(force=False)An extra parameter with a default still matches, because reset() can still be called.motorreset()SessionPresenter.setupresettable:Mapping[str, Resettable] An extra parameter with a default still matches, because reset() can still be called.

loose's reset needs a hard argument, so reset() fails on it and Session.rejected lists it with that reason. plot has no reset at all, so Session.rejected leaves it out.

plotno resetIt has none of the protocol's members, so Session.rejected leaves it out too.loosereset(hard)A renamed parameter, or an extra one without a default, doesn't match. Session.rejected lists it with the reason.detectorreset(force=False)An extra parameter with a default still matches, because reset() can still be called.motorreset()SessionPresenter.setupresettable:Mapping[str, Resettable] It has none of the protocol's members, so Session.rejected leaves it out too. A renamed parameter, or an extra one without a default, doesn't match. Session.rejected lists it with the reason. An extra parameter with a default still matches, because reset() can still be called.
plotno resetIt has none of the protocol's members, so Session.rejected leaves it out too.loosereset(hard)A renamed parameter, or an extra one without a default, doesn't match. Session.rejected lists it with the reason.detectorreset(force=False)An extra parameter with a default still matches, because reset() can still be called.motorreset()SessionPresenter.setupresettable:Mapping[str, Resettable] It has none of the protocol's members, so Session.rejected leaves it out too. A renamed parameter, or an extra one without a default, doesn't match. Session.rejected lists it with the reason. An extra parameter with a default still matches, because reset() can still be called.

This is structural subtyping: no inheritance needed, and types aren't compared, which is a type checker's job. The protocol needs no runtime_checkable, may list attributes, and may be generic (Reading[float] is matched as Reading). When a component you expected is missing, Session.satisfying shows the answer and Session.rejected says why:

>>> session.satisfying(Resettable)
{'motor': <Motor>, 'detector': <Detector>}
>>> session.rejected(Resettable)
{'loose': ["reset(hard) cannot be called as reset(): missing a required argument: 'hard'"]}

rejected lists only components that have some of the protocol's members, so the near misses are easy to find.

Asking for exactly one

A parameter annotated with the protocol alone asks for the one component, or shared value, that satisfies it:

class RoiView(QWidget):
    def setup(self, camera: HasCamera) -> None:
        self.camera = camera

Use P | None = None when the component can do without:

def setup(self, roi: HasRoi | None = None) -> None:
    self.roi = roi

What the component gets depends on how many things match:

matches camera: HasCamera roi: HasRoi \| None = None
one that one that one
none the build stops, and the error names what came close None
the only match failed to build the component is reported as not set up, and runs without it None
two or more the build stops the build stops

When two or more match, the error says which:

TypeError: 'roi' in its 'camera' parameter asks for the one object satisfying
'HasCamera', but 2 do, from 'camera', 'spare'. Narrow the protocol, or ask for
'Mapping[str, HasCamera]'.

A component never answers its own single question, but it does appear in its own Mapping[str, P], which describes the whole session the same way for everyone. Leave yourself out with one line:

others = {name: c for name, c in self.resettable.items() if name != self.name}

Asking about devices

Devices never answer a question in setup. A device is asked about in a constructor, with DevicesOf:

Who answers which question, and where
devicespresentersand viewsshared valuesin a constructorDevicesOf[P]in setupMapping[str, P]in setupP or P | None
devicespresentersand viewsshared valuesin a constructorDevicesOf[P]in setupMapping[str, P]in setupP or P | None
class MotorPresenter:
    def __init__(self, name: str, *, motors: DevicesOf[Movable]) -> None:
        self.name = name
        self.motors = motors

Devices exist before any presenter, so the constructor can ask. DevicesOf works only as Mapping[str, P] in a constructor; for every device, ask for DeviceMapping.

When not to ask

  • To get one specific value, ask for it by its class instead.
  • If components only need to hear when something happens, connect a signal to a slot in wire and skip the question.

A component can answer a question by accident

A component answers a question by what it has, whether it meant to or not, so any class with a reset method is Resettable. If that's not what you want, rename the method.