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
The value returned by the descriptor, when called on an instance. |
|
A Thing subclass that owns a descriptor. |
|
The type of a descriptor that's referred to by a |
|
The type of a field typed descriptor. |
|
The type of |
|
The type of |
|
Classes
A class that may be bound to an owning object or to a class. |
|
A class that describes a |
|
A base class for descriptors in LabThings-FastAPI. |
|
A description of a |
|
A |
|
Easy access to DescriptorInfo objects of a particular type. |
|
A descriptor that will return an OptionallyBoundInfo object. |
Functions
|
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
BaseDescriptorInfoobject.
- labthings_fastapi.base_descriptor.FTDescriptorT
The type of a field typed descriptor.
- labthings_fastapi.base_descriptor.DescriptorInfoT
The type of
BaseDescriptorInforeturned by a descriptor
- labthings_fastapi.base_descriptor.OptionallyBoundInfoT
The type of
OptionallyBoundInforeturned 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
OptionallyBoundInfoobject.This initialises the object, optionally binding it to
objif it is notNone.- 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
objis supplied.
- Raises:
ValueError – if neither
objnorclsis supplied.TypeError – if
objandclsare both supplied, butobjis not an instance ofcls. Note thatclsdoes not have to be equal toobj.__class__, it just has to passisinstance.
- _descriptor_cls = None
- _bound_to_obj
- 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
Falsethen this object refers only to a class. If it isTruethen we are describing a particular instance.
- owning_object_or_error() Owner
Return the
Thinginstance 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 forAction,BasePropertyand 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
Thinginstance, in which case it is said to be “bound”. This means there’s no need to separately pass theThingalong with the descriptor, which should help keep things simple in several places in the code.Initialise an
OptionallyBoundInfoobject.This sets up a BaseDescriptorInfo object, describing
descriptorand optionally bound toobj.- Parameters:
descriptor – The descriptor that this object will describe.
obj – The object to which this
BaseDescriptorInfois bound. If it isNone(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
objisNone.
- Raises:
ValueError – if both
objandclsareNone.
- _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.
- 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
Thinginstance.- Returns:
the value of the descriptor.
- Raises:
NotBoundToInstanceError – if called on an unbound object.
- class labthings_fastapi.base_descriptor.BaseDescriptor
-
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
- The descriptor remembers the name it’s assigned to in
- 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
titleanddescription that may be used in Generated documentation and elsewhere.
- The docstring and name are used to provide a
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")
BaseDescriptoris 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.
- __doc__ = None
- __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_docstringsfor more details.- Parameters:
owner – the
Thingsubclass 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_calledso an exception will be raised if this property is accessed before the descriptor has been assigned to a class attribute.The
nameof 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:
DescriptorNotAddedToClassError – if the owning class is not set.
UnexpectedGarbageCollectionError – if the owning class has been finalized.
- 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), theNameNotSetErrorfromself.namewill 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
titlein 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
objisNone(i.e. the descriptor is accessed as a class attribute), we return the descriptor, i.e.self.If
objis notNone, we return a value. To remove the need for this boilerplate in every subclass, we will call__instance_get__to get the value.
- 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 thatobjis 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 bybuiltins.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
Thinginstance 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
BaseDescriptorInfoobject 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
owneris 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
BaseDescriptorInfosubclass to return.obj – The
Thinginstance 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
BaseDescriptorInfoobject 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
owneris 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
Thinginstance 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_typetoBaseDescriptorInfoso we can fully describe aFieldTypedBaseDescriptor.Initialise an
OptionallyBoundInfoobject.This sets up a BaseDescriptorInfo object, describing
descriptorand optionally bound toobj.- Parameters:
descriptor – The descriptor that this object will describe.
obj – The object to which this
BaseDescriptorInfois bound. If it isNone(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
objisNone.
- Raises:
ValueError – if both
objandclsareNone.
- set(value: Value) None
Set the value of the descriptor.
This method may only be called if the DescriptorInfo object is bound to a
Thinginstance. 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
BaseDescriptorthat determines its type like a dataclass field.This adds two things to
BaseDescriptor:- Descriptors inheriting from this class will inspect the type annotations of
their owning class when determining
value_type.
- 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 thatbuiltins.propertyhas.
Initialise the FieldTypedBaseDescriptor.
Very little happens at initialisation time: most of the type determination happens in
__set_name__andvalue_typeso that type hints can be lazily evaluated.- __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
DataPropertyas 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_argsto retrieve the value type. This will be evaluated immediately, resolving any forward references.We use
typing.get_type_hintsto 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 duringvalue_type.- Parameters:
owner – the
Thingsubclass to which we are being attached.name – the name to which we have been assigned.
- Raises:
InconsistentTypeError – if the type is specified twice and the two types are not identical.
MissingTypeError – if no type hints have been given.
- 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, aDescriptorNotAddedToClassErrorwill 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
MissingTypeErrorwill 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
BaseDescriptorInfoobject 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
owneris 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
Thinginstance 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
BaseDescriptoras 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 anAttributeErrorto 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
DescriptorInfoobjects by name, or iterate over the names of the descriptors.It may be initialised with an object, in which case the contained
DescriptorInfoobjects will be bound to that object. If initialised without an object, the containedDescriptorInfoobjects 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
OptionallyBoundDescriptoronThingfor convenience.Initialise the DescriptorInfoCollection.
This initialises the object, optionally binding it to
objif it is notNone.- 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
objis 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.
- 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
BaseDescriptorInfoobjects from aThingsubclass.Initialise the descriptor.
- Parameters:
cls – The class of
OptionallyBoundInfoobjects that this descriptor will return.
- _cls
- 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.MappingProxyTypeto prevent accidental modification.- Raises:
TypeError – if the supplied object is not a class.