labthings_fastapi.base_descriptor

A base class for descriptors in LabThings.

Descriptors are used to describe Interaction Affordances in LabThings-FastAPI. There is some behaviour common to most of these, and BaseDescriptor centralises the code that implements it.

BaseDescriptor provides consistent handling of name, title, and description, as well as implementing the convention that descriptors return themselves when accessed as class attributes. It also provides BaseDescriptor.descriptor_info to return an object that may be used to refer to the descriptor (see later).

FieldTypedBaseDescriptor is a subclass of BaseDescriptor that adds “field typing”, i.e. the ability to determine the type of the descriptor’s value from a type annotation on the class attribute. This is particularly important for Properties.

BaseDescriptorInfo is a class that describes a descriptor, optionally bound to an instance. This allows us to pass around references to descriptors without confusing type checkers, and without needing to separately pass the instance along with the descriptor.

DescriptorInfoCollection is a mapping of descriptor names to BaseDescriptorInfo objects, and may be used to retrieve all descriptors of a particular type on a Thing.

Attributes

Value

The value returned by the descriptor, when called on an instance.

Owner

A Thing subclass that owns a descriptor.

Descriptor

The type of a descriptor that's referred to by a BaseDescriptorInfo object.

FTDescriptorT

The type of a field typed descriptor.

DescriptorInfoT

The type of BaseDescriptorInfo returned by a descriptor

OptionallyBoundInfoT

The type of OptionallyBoundInfo returned by a descriptor.

_class_attribute_docstring_cache

Classes

OptionallyBoundInfo

A class that may be bound to an owning object or to a class.

BaseDescriptorInfo

A class that describes a BaseDescriptor.

BaseDescriptor

A base class for descriptors in LabThings-FastAPI.

FieldTypedBaseDescriptorInfo

A description of a FieldTypedBaseDescriptor.

FieldTypedBaseDescriptor

A BaseDescriptor that determines its type like a dataclass field.

DescriptorInfoCollection

Easy access to DescriptorInfo objects of a particular type.

OptionallyBoundDescriptor

A descriptor that will return an OptionallyBoundInfo object.

Functions

get_class_attribute_docstrings(→ Mapping[str, str])

Retrieve docstrings for the attributes of a class.

Module Contents

labthings_fastapi.base_descriptor.Value

The value returned by the descriptor, when called on an instance.

labthings_fastapi.base_descriptor.Owner

A Thing subclass that owns a descriptor.

labthings_fastapi.base_descriptor.Descriptor

The type of a descriptor that’s referred to by a BaseDescriptorInfo object.

labthings_fastapi.base_descriptor.FTDescriptorT

The type of a field typed descriptor.

labthings_fastapi.base_descriptor.DescriptorInfoT

The type of BaseDescriptorInfo returned by a descriptor

labthings_fastapi.base_descriptor.OptionallyBoundInfoT

The type of OptionallyBoundInfo returned by a descriptor.

class labthings_fastapi.base_descriptor.OptionallyBoundInfo(obj: Owner | None, cls: type[Owner] | None = None)

Bases: Generic[Owner]

A class that may be bound to an owning object or to a class.

Initialise an OptionallyBoundInfo object.

This initialises the object, optionally binding it to obj if it is not None.

Parameters:
  • obj – The object to which this info object is bound. If it is None (default), the object will be unbound and will refer to the descriptor as attached to the class. This may mean that some methods are unavailable.

  • cls – The class to which this info object refers. May be omitted if obj is supplied.

Raises:
  • ValueError – if neither obj nor cls is supplied.

  • TypeError – if obj and cls are both supplied, but obj is not an instance of cls. Note that cls does not have to be equal to obj.__class__, it just has to pass isinstance.

_descriptor_cls = None
_bound_to_obj
property owning_class: type[Owner]

Retrieve the class this info object is describing.

property owning_object: Owner | None

Retrieve the object to which this info object is bound, if present.

property is_bound: bool

Whether this info object is bound to an instance.

If this property is False then this object refers only to a class. If it is True then we are describing a particular instance.

owning_object_or_error() Owner

Return the Thing instance to which we are bound, or raise an error.

This is mostly a convenience function that saves type-checking boilerplate.

Returns:

the owning object.

Raises:

NotBoundToInstanceError – if this object is not bound.

class labthings_fastapi.base_descriptor.BaseDescriptorInfo(descriptor: Descriptor, obj: Owner | None, cls: type[Owner] | None = None)

Bases: OptionallyBoundInfo[Owner], Generic[Descriptor, Owner, Value]

A class that describes a BaseDescriptor.

This class is used internally by LabThings to describe Properties, Actions, and other attributes of a Thing. It’s not usually encountered directly by someone using LabThings, except as a base class for Action, BaseProperty and others.

LabThings uses descriptors to represent the Interaction Affordances of a Thing. However, passing descriptors around isn’t very elegant for two reasons:

  • Holding references to Descriptor objects can confuse static type checkers.

  • Descriptors are attached to a class but do not know which object they

    are defined on.

This class allows the attributes of a descriptor to be accessed, and holds a reference to the underlying descriptor and its owning class. It may optionally hold a reference to a Thing instance, in which case it is said to be “bound”. This means there’s no need to separately pass the Thing along with the descriptor, which should help keep things simple in several places in the code.

Initialise an OptionallyBoundInfo object.

This sets up a BaseDescriptorInfo object, describing descriptor and optionally bound to obj.

Parameters:
  • descriptor – The descriptor that this object will describe.

  • obj – The object to which this BaseDescriptorInfo is bound. If it is None (default), the object will be unbound and will refer to the descriptor as attached to the class. This may mean that some methods are unavailable.

  • cls – The class to which we are bound. Only required if obj is None.

Raises:

ValueError – if both obj and cls are None.

_descriptor_ref
_descriptor_cls = None
_bound_to_obj
get_descriptor() Descriptor

Retrieve the descriptor object.

Returns:

The descriptor object

Raises:

RuntimeError – if the descriptor was garbage collected. This should never happen.

property name: str

The name of the descriptor.

This should be the same as the name of the attribute in Python.

property title: str

The title of the descriptor.

property description: str | None

A description (usually the docstring) of the descriptor.

get() Value

Get the value of the descriptor.

This method only works on a bound info object, it will raise an error if called via a class rather than a Thing instance.

Returns:

the value of the descriptor.

Raises:

NotBoundToInstanceError – if called on an unbound object.

__eq__(other: Any) bool

Determine if this object is equal to another one.

Parameters:

other – the object we’re comparing to.

Returns:

whether the two objects are equal.

__repr__() str

Represent the DescriptorInfo object as a string.

Returns:

a string representing the info object.

class labthings_fastapi.base_descriptor.BaseDescriptor

Bases: Generic[Owner, Value]

A base class for descriptors in LabThings-FastAPI.

This class implements several behaviours common to descriptors in LabThings:

  • The descriptor remembers the name it’s assigned to in name, for use in

    Generated documentation.

  • The descriptor inspects its owning class, and looks for an attribute

    docstring (i.e. a string constant immediately following the attribute assignment).

  • When called as a class attribute, the descriptor returns itself, as done by

    e.g. property.

  • The docstring and name are used to provide a title and description

    that may be used in Generated documentation and elsewhere.

class Example:
    my_prop = BaseDescriptor()
    '''My Property.

    This is a nice long docstring describing my property, which
    can span multiple lines.
    '''


p = Example.my_prop
assert p.name == "my_prop"
assert p.title == "My Property."
assert p.description.startswith("This is")

BaseDescriptor is a “non-data descriptor” (meaning it doesn’t implement __set__). This allows it to be overwritten by assigning to an object’s attribute, which can be useful in test code. This can easily be changed in subclasses by implementing __set__.

Initialise a BaseDescriptor.

_name: str | None = None
_title: str | None = None
_description: str | None = None
__doc__ = None
_set_name_called: bool = False
_owner_name: str = ''
__set_name__(owner: type[Owner], name: str) None

Take note of the name to which the descriptor is assigned.

This is called when the descriptor is assigned to an attribute of a class. This function remembers the name, so it can be used in Generated documentation.

This function also inspects the owning class, and will retrieve the docstring for its attribute. This allows us to use a string immediately after the descriptor is defined, rather than passing the docstring as an argument. See get_class_attribute_docstrings for more details.

Parameters:
  • owner – the Thing subclass to which we are being attached.

  • name – the name to which we have been assigned.

Raises:

DescriptorAddedToClassTwiceError – if the descriptor has been assigned to two class attributes.

assert_set_name_called() None

Raise an exception if __set_name__ has not yet been called.

Raises:

DescriptorNotAddedToClassError – if __set_name__ has not yet been called.

property name: str

The name of this descriptor.

When the descriptor is assigned to an attribute of a class, we remember the name of the attribute. There will be some time in between the descriptor being instantiated and the name being set.

We call BaseDescriptor.assert_set_name_called so an exception will be raised if this property is accessed before the descriptor has been assigned to a class attribute.

The name of Interaction Affordances is used in their URL and in the Generated documentation served by LabThings.

Raises:

DescriptorNotAddedToClassError – if __set_name__ has not yet been called.

property owning_class: type[Owner]

The class on which this descriptor is defined.

Raises:
property title: str

A human-readable title for the descriptor.

The Thing Description requires a human-readable title for all Interaction Affordances described. This property will generate a suitable string from either the name or the docstring.

The title is either the first line of the docstring, or the name of the descriptor. Note that, if there’s no summary line in the descriptor’s instance docstring, or if __set__name__ has not yet been called (i.e. if this attribute is accessed before the class on which the descriptor is defined has been fully set up), the NameNotSetError from self.name will propagate, i.e. this property will either return a string or fail with an exception.

Note also that, if the docstring for this descriptor is defined on the class rather than passed in (via a getter function or action function’s docstring), it will also not be available until after __set_name__ has been called.

property description: str | None

A description of the descriptor for use in documentation.

This property will return the docstring describing the descriptor. As the first line of the docstring (if present) is used as the title in Generated documentation it will be removed from this property.

__get__(obj: Owner, type: BaseDescriptor.__get__.type | None = None) Value
__get__(obj: None, type: BaseDescriptor.__get__.type) Self

Return the value or the descriptor, as per property.

If obj is None (i.e. the descriptor is accessed as a class attribute), we return the descriptor, i.e. self.

If obj is not None, we return a value. To remove the need for this boilerplate in every subclass, we will call __instance_get__ to get the value.

Parameters:
  • obj – the Thing instance to which we are attached.

  • type – the Thing subclass on which we are defined.

Returns:

the value of the descriptor returned from __instance_get__ when accessed on an instance, or the descriptor object if accessed on a class.

abstract instance_get(obj: Owner) Value

Return the value of the descriptor.

This method is called from __get__ if the descriptor is accessed as an instance attribute. This means that obj is guaranteed to be present.

__get__ may be called on either an instance or a class, and if it is called on the class, the convention is that we should return the descriptor object (i.e. self), as done by builtins.property.

BaseDescriptor.__get__ takes care of this logic, so we need only consider the case where we are called as an instance attribute. This simplifies type annotations and removes the need for overload definitions in every subclass.

Parameters:

obj – is the Thing instance on which this descriptor is being accessed.

Returns:

the value of the descriptor (i.e. property value, or bound method).

Raises:

NotImplementedError – if it is not overridden.

_descriptor_info(info_class: type[DescriptorInfoT], obj: Owner | None = None) DescriptorInfoT

Return a BaseDescriptorInfo object for this descriptor.

The return value of this function is an object that may be passed around without confusing type checkers, but still allows access to all of its functionality. Essentially, it just misses out __get__ so that it is no longer a Descriptor.

If owner is supplied, the returned object is bound to a particular object, and if not it is unbound, i.e. knows only about the class.

Parameters:
  • info_class – the BaseDescriptorInfo subclass to return.

  • obj – The Thing instance to which the return value is bound.

Returns:

An object that may be used to refer to this descriptor.

descriptor_info(owner: Owner | None = None) BaseDescriptorInfo[Self, Owner, Value]

Return a BaseDescriptorInfo object for this descriptor.

This generates an object that refers to the descriptor, optionally bound to a particular object. It’s intended to make it easier to pass around references to particular affordances, without needing to retrieve and store Descriptor objects directly (which gets confusing). If owner is supplied, the returned object is bound to a particular object, and if not it is unbound, i.e. knows only about the class.

Parameters:

owner – The Thing instance to which the return value is bound.

Returns:

An object that may be used to refer to this descriptor.

class labthings_fastapi.base_descriptor.FieldTypedBaseDescriptorInfo(descriptor: Descriptor, obj: Owner | None, cls: type[Owner] | None = None)

Bases: BaseDescriptorInfo[FTDescriptorT, Owner, Value], Generic[FTDescriptorT, Owner, Value]

A description of a FieldTypedBaseDescriptor.

This adds value_type to BaseDescriptorInfo so we can fully describe a FieldTypedBaseDescriptor.

Initialise an OptionallyBoundInfo object.

This sets up a BaseDescriptorInfo object, describing descriptor and optionally bound to obj.

Parameters:
  • descriptor – The descriptor that this object will describe.

  • obj – The object to which this BaseDescriptorInfo is bound. If it is None (default), the object will be unbound and will refer to the descriptor as attached to the class. This may mean that some methods are unavailable.

  • cls – The class to which we are bound. Only required if obj is None.

Raises:

ValueError – if both obj and cls are None.

property value_type: type[Value]

The type of the descriptor’s value.

set(value: Value) None

Set the value of the descriptor.

This method may only be called if the DescriptorInfo object is bound to a Thing instance. It will raise an error if called on a class.

Parameters:

value – the new value.

Raises:

NotBoundToInstanceError – if called on an unbound info object.

class labthings_fastapi.base_descriptor.FieldTypedBaseDescriptor

Bases: Generic[Owner, Value], BaseDescriptor[Owner, Value]

A BaseDescriptor that determines its type like a dataclass field.

This adds two things to BaseDescriptor:

  1. Descriptors inheriting from this class will inspect the type annotations of

    their owning class when determining value_type.

  2. This class and its children will be “data descriptors” because there is a

    stub implementation of __set__. This means that the attribute may not be assigned to (unless __set__ is overridden). This is the behaviour that builtins.property has.

Initialise the FieldTypedBaseDescriptor.

Very little happens at initialisation time: most of the type determination happens in __set_name__ and value_type so that type hints can be lazily evaluated.

_type: type | None = None
_unevaluated_type_hint: str | None = None
__set_name__(owner: type[Owner], name: str) None

Take note of the name and type.

This function is where we determine the type of the property. It may be specified in two ways: either by subscripting the descriptor or by annotating the attribute. This example is for DataProperty as this class is not intended to be used directly.

class MyThing(Thing):
    subscripted_property = DataProperty[int](default=0)
    annotated_property: int = DataProperty(default=0)

The second form often works better with autocompletion, though it is usually called via a function to avoid type checking errors.

Neither form allows us to access the type during __init__, which is why we find the type here. If there is a problem, exceptions raised will appear to come from the class definition, so it’s important to include the name of the attribute.

See Descriptors for links to the Python docs about when this function is called.

For subscripted types (i.e. the first form above), we use typing.get_args to retrieve the value type. This will be evaluated immediately, resolving any forward references.

We use typing.get_type_hints to resolve type hints on the owning class. This takes care of a lot of subtleties like un-stringifying forward references. In order to support forward references, we only check for the existence of a type hint during __set_name__ and will evaluate it fully during value_type.

Parameters:
  • owner – the Thing subclass to which we are being attached.

  • name – the name to which we have been assigned.

Raises:
value_type() type[Value]

The type of this descriptor’s value.

This is only available after __set_name__ has been called, which happens at the end of the class definition. If it is called too early, a DescriptorNotAddedToClassError will be raised.

Accessing this property will attempt to resolve forward references, i.e. type annotations that are strings. If there is an error resolving the forward reference, a MissingTypeError will be raised.

Returns:

the type of the descriptor’s value.

Raises:

MissingTypeError – if the type is None, not resolvable, or not specified.

descriptor_info(owner: Owner | None = None) FieldTypedBaseDescriptorInfo[Self, Owner, Value]

Return a BaseDescriptorInfo object for this descriptor.

This generates an object that refers to the descriptor, optionally bound to a particular object. It’s intended to make it easier to pass around references to particular affordances, without needing to retrieve and store Descriptor objects directly (which gets confusing). If owner is supplied, the returned object is bound to a particular object, and if not it is unbound, i.e. knows only about the class.

Parameters:

owner – The Thing instance to which the return value is bound.

Returns:

An object that may be used to refer to this descriptor.

__set__(obj: Owner, value: Value) None

Mark the BaseDescriptor as a data descriptor.

Even for read-only descriptors, it’s important to define a __set__ method. The presence of this method prevents Python overwriting the descriptor when a value is assigned. This base implementation returns an AttributeError to signal that the descriptor is read-only. Overriding it with a method that does not raise an exception will allow the descriptor to be written to.

Parameters:
  • obj – The object on which to set the value.

  • value – The value to set the descriptor to.

Raises:

AttributeError – always, as this is read-only by default.

class labthings_fastapi.base_descriptor.DescriptorInfoCollection(obj: Owner | None, cls: type[Owner] | None = None)

Bases: Mapping[str, DescriptorInfoT], OptionallyBoundInfo[Owner], Generic[Owner, DescriptorInfoT]

Easy access to DescriptorInfo objects of a particular type.

This class works as a Mapping, so you can retrieve individual DescriptorInfo objects by name, or iterate over the names of the descriptors.

It may be initialised with an object, in which case the contained DescriptorInfo objects will be bound to that object. If initialised without an object, the contained DescriptorInfo objects will be unbound, i.e. referring only to the class.

This class is subclassed by each of the LabThings descriptors (Properties, Actions, etc.) and generated by a corresponding OptionallyBoundDescriptor on Thing for convenience.

Initialise the DescriptorInfoCollection.

This initialises the object, optionally binding it to obj if it is not None.

Parameters:
  • obj – The object to which this info object is bound. If it is None (default), the object will be unbound and will refer to the descriptor as attached to the class. This may mean that some methods are unavailable.

  • cls – The class to which this info object refers. May be omitted if obj is supplied.

_descriptorinfo_class: type[DescriptorInfoT]

The class of DescriptorInfo objects contained in this collection.

This class attribute must be set in subclasses.

property descriptorinfo_class: type[DescriptorInfoT]

The class of DescriptorInfo objects contained in this collection.

__getitem__(key: str) DescriptorInfoT

Retrieve a DescriptorInfo object given the name of the descriptor.

Parameters:

key – The name of the descriptor whose info object is required.

Returns:

The DescriptorInfo object for the named descriptor.

Raises:

KeyError – if the key does not refer to a descriptor of the right type.

__iter__() collections.abc.Iterator[str]

Iterate over the names of the descriptors of the specified type.

Yield:

The names of the descriptors.

__len__() int

Return the number of descriptors of the specified type.

Returns:

The number of descriptors of the specified type.

class labthings_fastapi.base_descriptor.OptionallyBoundDescriptor(cls: type[OptionallyBoundInfoT])

Bases: Generic[Owner, OptionallyBoundInfoT]

A descriptor that will return an OptionallyBoundInfo object.

This descriptor will return an instance of a particular class, initialised with either the object, or its class, depending on how it is accessed.

This is useful for returning collections of BaseDescriptorInfo objects from a Thing subclass.

Initialise the descriptor.

Parameters:

cls – The class of OptionallyBoundInfo objects that this descriptor will return.

_cls
__get__(obj: Owner | None, cls: type[Owner] | None = None) OptionallyBoundInfoT

Return an OptionallyBoundInfo object.

Parameters:
  • obj – The object to which the info is bound, or None if unbound.

  • cls – The class on which the info is defined.

Returns:

An OptionallyBoundInfo object.

labthings_fastapi.base_descriptor._class_attribute_docstring_cache: weakref.WeakKeyDictionary[type, Mapping[str, str]]
labthings_fastapi.base_descriptor.get_class_attribute_docstrings(cls: type) Mapping[str, str]

Retrieve docstrings for the attributes of a class.

Python formally supports __doc__ attributes on classes and functions, and this means that classes and methods can self-describe in a way that is picked up by documentation tools. There isn’t currently a language feature specifically provided to annotate other attributes of a class, but there is a convention that seems almost universally adopted by documentation tools, which is to add a string literal immediately after the attribute assignment. While it’s not a formal language feature, Python does explicitly allow these string literals (which don’t have any other purpose) to enable documentation tools to document attributes.

This function inspects a class, and returns a dictionary mapping attribute names to docstrings, where the docstring is a string immediately following the attribute. For example:

class Example:
    my_constant: int = 10
    "A number that is all mine."


docs = get_class_attribute_docstrings(Example)

assert docs["my_constant"] == "A number that is all mine."

Note

This function relies on re-parsing the source of the class, so it will not work on classes that are not defined in a file (for example, if you just paste the example above into a Python interpreter). In that case, an empty dictionary is returned.

The same limitation means dynamically defined classes will result in an empty dictionary.

Note

This function uses a cache, so subsequent calls on the same class will return a cached value. As dynamic classes are not supported, this is not expected to be a problem.

Parameters:

cls – The class to inspect

Returns:

A mapping of attribute names to docstrings. Note that this will be wrapped in a types.MappingProxyType to prevent accidental modification.

Raises:

TypeError – if the supplied object is not a class.