labthings_fastapi.thing_slots ============================= .. py:module:: labthings_fastapi.thing_slots .. autoapi-nested-parse:: 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. .. code-block:: python 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 ---------- .. autoapisummary:: labthings_fastapi.thing_slots.ThingSubclass labthings_fastapi.thing_slots.ConnectedThings Classes ------- .. autoapisummary:: labthings_fastapi.thing_slots.ThingSlot Module Contents --------------- .. py:data:: ThingSubclass .. py:data:: ConnectedThings .. py:class:: ThingSlot(*, default: str | None | collections.abc.Iterable[str] | types.EllipsisType = ...) Bases: :py:obj:`Generic`\ [\ :py:obj:`ConnectedThings`\ ], :py:obj:`labthings_fastapi.base_descriptor.FieldTypedBaseDescriptor`\ [\ :py:obj:`ConnectedThings`\ ] Descriptor that instructs the server to supply other Things. A `.ThingSlot` provides either one or several `.Thing` instances as a property of a `.Thing`\ . This allows `.Thing`\ s 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 `.Thing` from the `.ThingServer` this is not recommended: using Thing Connections ensures all the `.Thing` instances 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 `.ThingSlot` attribute is key to its operation. It should be assigned to an attribute typed either as a `.Thing` subclass, a mapping of strings to `.Thing` or subclass instances, or an optional `.Thing` instance: .. code-block:: python 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 `.Thing` instances things: Mapping[str, OtherExample] = lt.thing_slot(["thing_a"]) Declare a ThingSlot. :param 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 of ``None`` will result in the connection evaluating to ``None`` unless it has been configured by the server. If the type is not optional, a default value of ``None`` will result in an error, unless the server has set another value in its configuration. If the type is a mapping of `str` to `.Thing` the default should be of type `Iterable[str]` (and could be an empty list). .. py:attribute:: _default :value: Ellipsis .. py:attribute:: _things :type: weakref.WeakKeyDictionary[labthings_fastapi.thing.Thing, weakref.ReferenceType[labthings_fastapi.thing.Thing] | weakref.WeakValueDictionary[str, labthings_fastapi.thing.Thing] | None] .. py:property:: thing_type :type: tuple[type, Ellipsis] The `.Thing` subclass(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`\ . .. py:property:: is_mapping :type: bool Whether we return a mapping of strings to Things, or a single Thing. .. py:property:: is_optional :type: bool Whether ``None`` or an empty mapping is an allowed value. .. py:property:: default :type: str | collections.abc.Iterable[str] | None | types.EllipsisType The name of the Thing that will be connected by default, if any. .. py:method:: __set__(obj: labthings_fastapi.thing.Thing, value: ThingSubclass) -> None Raise an error as this is a read-only descriptor. :param obj: the `.Thing` on which the descriptor is defined. :param value: the value being assigned. :raises AttributeError: this descriptor is not writeable. .. py:method:: _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.connect` to choose the Things we return when the `.ThingSlot` is accessed. :param things: the available `.Thing` instances on the server. :param target: the name(s) we should connect to, or `None` to set the connection to `None` (if it is optional). A special value is `...` which will pick the `.Thing` instannce(s) matching this connection's type hint. :raises ThingSlotError: if the supplied `.Thing` is of the wrong type, if a sequence is supplied when a single `.Thing` is required, or if `None` is supplied and the connection is not optional. :raises TypeError: if ``target`` is not one of the allowed types. `KeyError` will also be raised if names specified in ``target`` do not exist in ``things``\ . :return: a list of `.Thing` instances to supply in response to ``__get__``\ . .. py:method:: 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_thing`` by finding the `.Thing` instance(s) it should supply when its ``__get__`` method is called. The logic for determining this is: * If ``target`` is specified, we look for the specified `.Thing`\ (s). ``None`` means we should return ``None`` - that's only allowed if the type hint permits it. * If ``target`` is 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 the `.Thing` by type. Most of the time, this is the desired behaviour. If the type of this connection is a ``Mapping``\ , ``target`` should be a sequence of names. This sequence may be empty. ``None`` is 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 either ``None`` is specified, or no ``target`` is given and the default is set as ``None``\ , then an error will be raised. ``None`` will only be returned at runtime if it is permitted by the type hint. :param host: the `.Thing` on which the connection is defined. :param things: the available `.Thing` instances on the server. :param target: the name(s) we should connect to, or `None` to set the connection to `None` (if it is optional). The default is `...` which will use the default that was set when this `.ThingSlot` was defined. :raises ThingSlotError: if the supplied `.Thing` is of the wrong type, if a sequence is supplied when a single `.Thing` is required, or if `None` is supplied and the connection is not optional. .. py:method:: instance_get(obj: labthings_fastapi.thing.Thing) -> ConnectedThings Supply the connected `.Thing`\ (s). :param obj: The `.Thing` on which the connection is defined. :return: the `.Thing` instance(s) connected. :raises ThingNotConnectedError: if the ThingSlot has not yet been set up. :raises ReferenceError: if a connected Thing no longer exists (should not ever happen in normal usage). Typing notes: This must be annotated as ``ConnectedThings`` which is the type variable corresponding to the type of this connection. The type determined at runtime will be within the upper bound of ``ConnectedThings`` but it would be possible for ``ConnectedThings`` to 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.