labthings_fastapi.thing_slots
Facilitate connections between Things.
It is often desirable for two Things in the same server to be able to communicate.
In order to do this in a nicely typed way that is easy to test and inspect,
LabThings-FastAPI provides the thing_slot. This allows a Thing
to declare that it depends on another Thing being present, and provides a way for
the server to automatically connect the two when the server is set up.
Thing connections are set up after all the Thing instances are initialised.
This means you should not rely on them during initialisation: if you attempt to
access a connection before it is provided, it will raise an exception. The
advantage of making connections after initialisation is that circular connections
are not a problem: Thing a may depend on Thing b and vice versa.
As with properties, thing connections will usually be declared using the function
thing_slot rather than the descriptor directly. This allows them to be
typed and documented on the class, i.e.
import labthings_fastapi as lt
class ThingA(lt.Thing):
"A class that doesn't do much."
@lt.action
def say_hello(self) -> str:
"A canonical example function."
return "Hello world."
class ThingB(lt.Thing):
"A class that relies on ThingA."
thing_a: ThingA = lt.thing_slot()
@lt.action
def say_hello(self) -> str:
"I'm too lazy to say hello, ThingA does it for me."
return self.thing_a.say_hello()
Attributes
Classes
Descriptor that instructs the server to supply other Things. |
Functions
|
Declare a connection to another |
Module Contents
- labthings_fastapi.thing_slots.ThingSubclass
- labthings_fastapi.thing_slots.ConnectedThings
- class labthings_fastapi.thing_slots.ThingSlot(*, default: str | None | collections.abc.Iterable[str] | types.EllipsisType = ...)
Bases:
Generic[ConnectedThings],labthings_fastapi.base_descriptor.FieldTypedBaseDescriptor[labthings_fastapi.thing.Thing,ConnectedThings]Descriptor that instructs the server to supply other Things.
A
ThingSlotprovides either one or severalThinginstances as a property of aThing. This allowsThings to communicate with each other within the server, including accessing attributes that are not exposed over HTTP.While it is possible to dynamically retrieve a
Thingfrom theThingServerthis is not recommended: using Thing Connections ensures all theThinginstances are available before the server starts, reducing the likelihood of run-time crashes.The usual way of creating these connections is the function
thing_slot. This class and its subclasses are not usually instantiated directly.The type of the
ThingSlotattribute is key to its operation. It should be assigned to an attribute typed either as aThingsubclass, a mapping of strings toThingor subclass instances, or an optionalThinginstance:class OtherExample(lt.Thing): pass class Example(lt.Thing): # This will always evaluate to an `OtherExample` other_thing: OtherExample = lt.thing_slot("other_thing") # This may evaluate to an `OtherExample` or `None` optional: OtherExample | None = lt.thing_slot("other_thing") # This evaluates to a mapping of `str` to `~lt.Thing` instances things: Mapping[str, OtherExample] = lt.thing_slot(["thing_a"])
Declare a ThingSlot.
- Parameters:
default –
The name of the Thing(s) that will be connected by default.
If the type is optional (e.g.
ThingSubclass | None) a default value ofNonewill result in the connection evaluating toNoneunless it has been configured by the server.If the type is not optional, a default value of
Nonewill result in an error, unless the server has set another value in its configuration.If the type is a mapping of
strtoThingthe default should be of typeIterable[str](and could be an empty list).
- _default = Ellipsis
- _things: weakref.WeakKeyDictionary[labthings_fastapi.thing.Thing, weakref.ReferenceType[labthings_fastapi.thing.Thing] | weakref.WeakValueDictionary[str, labthings_fastapi.thing.Thing] | None]
- property thing_type: tuple[type, Ellipsis]
The
Thingsubclass(es) returned by this connection.A tuple is returned to allow for optional thing connections that are typed as the union of two Thing types. It will work with
isinstance.
- property default: str | collections.abc.Iterable[str] | None | types.EllipsisType
The name of the Thing that will be connected by default, if any.
- _pick_things(things: Mapping[str, Thing], target: str | collections.abc.Iterable[str] | None | types.EllipsisType) Sequence[Thing]
Pick the Things we should connect to from a list.
This function is used internally by
ThingSlot.connectto choose the Things we return when theThingSlotis accessed.- Parameters:
- Raises:
ThingSlotError – if the supplied
Thingis of the wrong type, if a sequence is supplied when a singleThingis required, or ifNoneis supplied and the connection is not optional.TypeError – if
targetis not one of the allowed types.
KeyErrorwill also be raised if names specified intargetdo not exist inthings.- Returns:
a list of
Thinginstances to supply in response to__get__.
- connect(host: labthings_fastapi.thing.Thing, things: Mapping[str, Thing], target: str | collections.abc.Iterable[str] | None | types.EllipsisType = ...) None
Find the
Thing(s) we should supply when accessed.This method sets up a ThingSlot on
host_thingby finding theThinginstance(s) it should supply when its__get__method is called. The logic for determining this is:If
targetis specified, we look for the specifiedThing(s).Nonemeans we should returnNone- that’s only allowed if the type hint permits it.If
targetis not specified or is...we use the default value set when the connection was defined.If the default value was
...and no target was specified, we will attempt to find theThingby type. Most of the time, this is the desired behaviour.
If the type of this connection is a
Mapping,targetshould be a sequence of names. This sequence may be empty.Noneis treated as equivalent to the empty list, and a list with one name in it is treated as equivalent to a single name.If the type hint of this connection does not permit
None, and eitherNoneis specified, or notargetis given and the default is set asNone, then an error will be raised.Nonewill only be returned at runtime if it is permitted by the type hint.- Parameters:
- Raises:
ThingSlotError – if the supplied
Thingis of the wrong type, if a sequence is supplied when a singleThingis required, or ifNoneis supplied and the connection is not optional.
- instance_get(obj: labthings_fastapi.thing.Thing) ConnectedThings
Supply the connected
Thing(s).- Parameters:
obj – The
Thingon which the connection is defined.- Returns:
the
Thinginstance(s) connected.- Raises:
ThingNotConnectedError – if the ThingSlot has not yet been set up.
ReferenceError – if a connected Thing no longer exists (should not ever happen in normal usage).
Typing notes:
This must be annotated as
ConnectedThingswhich is the type variable corresponding to the type of this connection. The type determined at runtime will be within the upper bound ofConnectedThingsbut it would be possible forConnectedThingsto be more specific.In general, types determined at runtime may conflict with generic types, and at least for this class the important thing is that types determined at runtime match the attribute annotations, which is tested in unit tests.
The return statements here consequently have their types ignored.
- labthings_fastapi.thing_slots.thing_slot(default: str | collections.abc.Iterable[str] | None | types.EllipsisType = ...) Any
Declare a connection to another
Thingin the same server.lt.thing_slotmarks a class attribute as a connection to anotherThingon the same server. This will be automatically supplied when the server is started, based on the type hint and default value.In keeping with
propertyandsetting, the type of the attribute should be the type of the connectedThing. For example:import labthings_fastapi as lt class ThingA(lt.Thing): ... class ThingB(lt.Thing): "A class that relies on ThingA." thing_a: ThingA = lt.thing_slot()
This function is a convenience wrapper around the
ThingSlotdescriptor class, and should be used in preference to using the descriptor directly. The main reason to use the function is that it suppresses type errors when using static type checkers such asmypyorpyright(see note below).The type hint of a Thing Connection should be one of the following:
- A
Thingsubclass. An instance of this subclass will be returned when the attribute is accessed.
- A
- An optional
Thingsubclass (e.g.MyThing | None). This will either return a
MyThinginstance orNone.
- An optional
Example:
import labthings_fastapi as lt class ThingA(lt.Thing): "An example Thing." class ThingB(lt.Thing): "An example Thing with connections." thing_a: ThingA = lt.thing_slot() maybe_thing_a: ThingA | None = lt.thing_slot() all_things_a: Mapping[str, ThingA] = lt.thing_slot() @lt.action def show_connections(self) -> str: "Tell someone about our connections." self.thing_a # should always evaluate to a ThingA instance self.maybe_thing_a # will be a ThingA instance or None self.all_things_a # will a mapping of names to ThingA instances return f"{self.thing_a=}, {self.maybe_thing_a=}, {self.all_things_a=}"
The example above is very contrived, but shows how to apply the different types.
If no default value is supplied, and no value is configured for the connection, the server will attempt to find a
Thingthat matches the specified type when the server is started. If no matchingThinginstances are found, the descriptor will returnNoneor an empty mapping. If that is not allowed by the type hint, the server will fail to start with an error.The default value may be a string specifying a
Thingname, or a sequence of strings (for connections that return mappings). In those cases, the relevantThingwill be returned from the server. If a name is given that either doesn’t correspond to aThingon the server, or is aThingthat doesn’t match the type of this connection, the server will fail to start with an error.The default may also be
Nonewhich is appropriate when the type is optional or a mapping. If the type is aThingsubclass, a default value ofNoneforces the connection to be specified in configuration.- Parameters:
default – The name(s) of the Thing(s) that will be connected by default. If the default is omitted or set to
...the server will attempt to find a matchingThinginstance (or instances). A default value ofNoneis allowed if the connection is type hinted as optional.- Returns:
A
ThingSlotdescriptor.
Typing notes:
In the example above, using
ThingSlotdirectly would assign an object with typeThingSlot[ThingA]to the attributething_a, which is typed asThingA. This would cause a type error. Usingthing_slotsuppresses this error, as its return type is a`Any``.The use of
Anyor an alternative type-checking exemption seems to be inevitable when implementing descriptors that are typed via attribute annotations, and it is done by established libraries such aspydantic.