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.
Attributes
The value returned by the descriptor, when called on an instance. |
|
Exceptions
Descriptor has not yet been added to a class. |
|
A Descriptor has been added to a class more than once. |
Classes
A base class for descriptors in LabThings-FastAPI. |
|
A BaseDescriptor that determines its type like a dataclass field. |
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.
- exception labthings_fastapi.base_descriptor.DescriptorNotAddedToClassError
Bases:
RuntimeErrorDescriptor has not yet been added to a class.
This error is raised if certain properties of descriptors are accessed before
__set_name__has been called on the descriptor.__set_name__is part of the descriptor protocol, and is called when a class is defined to notify the descriptor of its name and owning class.If you see this error, it often means that a descriptor has been instantiated but not attached to a class, for example:
import labthings as lt class Test(lt.Thing): myprop: int = lt.property(default=0) # This is OK orphaned_prop: int = lt.property(default=0) # Not OK Test.myprop.model # Evaluates to a pydantic model orphaned_prop.model # Raises this exception
Initialize self. See help(type(self)) for accurate signature.
- exception labthings_fastapi.base_descriptor.DescriptorAddedToClassTwiceError
Bases:
RuntimeErrorA Descriptor has been added to a class more than once.
This error is raised if
__set_name__is called more than once on a descriptor. This happens when either the same descriptor instance is used twice in one class definition, or if a descriptor instance is used on more than one class.Note
FunctionalPropertyincludes a special case that will ignore the__set_name__call corresponding to the setter. This allows the property to be defined likeprop4below, even though it does assign the descriptor to two names. That behaviour is specific toFunctionalPropertyandFunctionalSettingand is not part ofBaseDescriptorbecauseBaseDescriptorhas no setter.mypydoes not allow custom property-like descriptors to follow the syntax used by the built-inpropertyof giving both the getter and setter functions the same name: this causes an error because it is a redefinition. We suggest using a different name for the setter to work around this, hence the need for an exception.class MyDescriptor(BaseDescriptor): "An example descriptor that inherits from BaseDescriptor." def __init__(getter=None): "Initialise the descriptor, allowing use as a decorator." self._getter = getter def setter(self, setter): "Add a setter to the descriptor." self._setter = setter return self class Example: "An example class with descriptors." # prop1 is fine - only used once. prop1 = MyDescriptor() # prop2 reuses the name ``prop2`` which may confuse ``mypy`` but # will only call ``__set_name__`` once. @MyDescriptor def prop2(self): "A dummy property" return False @prop2.setter def prop2(self, val): "Set the dummy property" pass # prop3a and prop3b will cause this error prop3a = MyDescriptor() prop3b = MyDescriptor() # prop4 and set_prop4 will cause this error on BaseDescriptor # but there is a specific exception in FunctionalProperty # to allow this form. @MyDescriptor def prop4(self): "An example property with two names" return True @prop4.setter def set_prop4(self, val): "A setter for prop4 that is not named prop4." pass
Note
Because this exception is raised in
__set_name__it will not appear to come from the descriptor assignment, but instead it will be raised at the end of the class definition. The descriptor name(s) should be in the error message.Initialize self. See help(type(self)) for accurate signature.
- class labthings_fastapi.base_descriptor.BaseDescriptor
Bases:
Generic[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
- 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")
Initialise a BaseDescriptor.
- __doc__ = None
- __set_name__(owner: type[labthings_fastapi.thing.Thing], 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 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: labthings_fastapi.thing.Thing, type: BaseDescriptor.__get__.type | None = None) Value
- __get__(obj: None, type: BaseDescriptor.__get__.type) typing_extensions.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: labthings_fastapi.thing.Thing) 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.
- class labthings_fastapi.base_descriptor.FieldTypedBaseDescriptor
Bases:
Generic[Value],BaseDescriptor[Value]A BaseDescriptor that determines its type like a dataclass field.
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[labthings_fastapi.thing.Thing], 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.
- 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.