labthings_fastapi.properties
Define properties of Thing objects.
Properties are attributes of a Thing that may be read or written to
over HTTP, and they are described in Generated documentation. They are implemented with
a function property (usually referenced as lt.property), which is
intentionally similar to Python’s built in property.
Properties can be defined in two ways as shown below:
import labthings_fastapi as lt
class Counter(lt.Thing):
"A counter that knows what's remaining."
count: int = lt.property(default=0, readonly=True)
"The number of times we've incremented the counter."
target: int = lt.property(default=10)
"The number of times to increment before we stop."
@lt.property
def remaining(self) -> int:
"The number of steps remaining."
return self.target - self.count
@remaining.setter
def _set_remaining(self, value: int) -> None:
self.target = self.count + value
The first two properties are simple variables: they may be read and assigned
to, and will behave just like a regular variable. Their syntax is similar to
dataclasses or pydantic in that property is used as a “field specifier”
to set options like the default value, and the type annotation is on the
class attribute. Documentation is in strings immediately following the
properties, which is understood by most automatic documentation tools.
remaining is defined using a “getter” function, meaning this code will
be run each time counter.remaining is accessed. Its type will be the
return type of the function, and its docstring will come from the function
too. Setters with only a getter are read-only.
Adding a “setter” to properties is optional, and makes them read-write.
Attributes
The set of supported constraint arguments for properties. |
|
The value returned by a property. |
|
The |
|
An instance of (a subclass of) BaseProperty. |
Exceptions
The default value has been specified more than once. |
|
The default value has not been specified. |
Classes
A descriptor that marks Properties on Things. |
|
A property that uses a getter and a setter. |
|
Access to the metadata of a Property. |
|
Access to metadata on all the properties of a |
|
A base class for settings. |
|
A |
|
Access to the metadata of a setting. |
|
Access to metadata on all the properties of a |
Functions
|
Process default arguments to get a default factory function. |
Module Contents
- labthings_fastapi.properties.CONSTRAINT_ARGS
The set of supported constraint arguments for properties.
- exception labthings_fastapi.properties.OverspecifiedDefaultError
Bases:
ValueErrorThe default value has been specified more than once.
This error is raised when a
DataPropertyis instantiated with both adefaultvalue and adefault_factoryprovided.Initialize self. See help(type(self)) for accurate signature.
- exception labthings_fastapi.properties.MissingDefaultError
Bases:
ValueErrorThe default value has not been specified.
This error is raised when a
DataPropertyis instantiated without adefaultvalue or adefault_factoryfunction.Initialize self. See help(type(self)) for accurate signature.
- labthings_fastapi.properties.Value
The value returned by a property.
- labthings_fastapi.properties.BasePropertyT
An instance of (a subclass of) BaseProperty.
- labthings_fastapi.properties.default_factory_from_arguments(default: Value | types.EllipsisType = ..., default_factory: Callable[[], Value] | None = None) Callable[[], Value]
Process default arguments to get a default factory function.
This function takes the
defaultanddefault_factoryarguments and will either return thedefault_factoryif it is provided, or will wrap the default value provided in a factory function.Note that this wrapping does not copy the default value each time it is called, so mutable default values are only safe if supplied as a factory function.
This is used to avoid repeating the logic of checking whether a default value or a factory function has been provided, and it returns a factory rather than a default value so that it may be called multiple times to get copies of the default value.
This function also ensures the default is specified exactly once, and raises exceptions if it is not.
This logic originally lived only in the initialiser of
DataPropertybut it was needed in thepropertyandsettingfunctions in order to correctly type them (so that specifying both or neither of thedefaultanddefault_factoryarguments would raise an error with mypy).- Parameters:
default – the default value, or an ellipsis if not specified.
default_factory – a function that returns the default value.
- Returns:
a function that returns the default value.
- Raises:
OverspecifiedDefaultError – if both
defaultanddefault_factoryare specified.MissingDefaultError – if neither
defaultnordefault_factoryare specified.
- class labthings_fastapi.properties.BaseProperty(constraints: collections.abc.Mapping[str, Any] | None = None)
Bases:
labthings_fastapi.base_descriptor.FieldTypedBaseDescriptor[Owner,Value],Generic[Owner,Value]A descriptor that marks Properties on Things.
This class is used to determine whether an attribute of a
Thingshould be treated as a Property (see Properties - essentially, it means the value should be available over HTTP).BasePropertyshould not be used directly, instead it is recommended to usepropertyto declare properties on yourThingsubclass.Initialise a BaseProperty.
- Parameters:
constraints – is passed as keyword arguments to
pydantic.Fieldto add validation constraints to the property. Seepydantic.Fieldfor details. The module-level constantCONSTRAINT_ARGSlists the supported constraint arguments.- Raises:
UnsupportedConstraintError – if unsupported constraint arguments are supplied. See
CONSTRAINT_ARGSfor the supported arguments.
- _model: type[pydantic.BaseModel] | None = None
- constraints
- model() type[pydantic.BaseModel]
A Pydantic model for the property’s type.
pydanticmodels are used to serialise and deserialise values from and to JSON. If the property is defined with a type hint that is not apydantic.BaseModelsubclass, this property will ensure it is wrapped in apydantic.RootModelso it can be used with FastAPI.If
BaseProperty.value_typeis already apydantic.BaseModelsubclass, this returns it unchanged.- Returns:
a Pydantic model for the property’s type.
- add_to_fastapi(app: fastapi.FastAPI, thing: Owner) None
Add this action to a FastAPI app, bound to a particular Thing.
- Parameters:
app – The FastAPI application we are adding endpoints to.
thing – The
Thingwe are adding the endpoints for.
- Raises:
NotConnectedToServerError – if the
Thingdoes not have apathset.
- property_affordance(thing: labthings_fastapi.thing.Thing, path: str | None = None) labthings_fastapi.thing_description._model.PropertyAffordance
Represent the property in a Thing Description.
- Parameters:
- Returns:
A description of the property in Thing Description format.
- Raises:
NotConnectedToServerError – if the
Thingdoes not have apathset.
- abstract __set__(obj: Owner, value: Any) None
Set the property (stub method).
This is a stub
__set__method to mark this as a data descriptor.- Parameters:
obj – The Thing on which we are setting the value.
value – The new value for the Thing.
- Raises:
NotImplementedError – as this must be overridden by concrete classes.
- descriptor_info(owner: Owner | None = None) PropertyInfo[typing_extensions.Self, Owner, Value]
Return an object that allows access to this descriptor’s metadata.
- Parameters:
owner – An instance to bind the descriptor info to. If
None, the returned object will be unbound and will only refer to the class.- Returns:
A
PropertyInfoinstance describing this property.
- class labthings_fastapi.properties.FunctionalProperty(fget: Callable[[Owner], Value], constraints: collections.abc.Mapping[str, Any] | None = None)
Bases:
BaseProperty[Owner,Value],Generic[Owner,Value]A property that uses a getter and a setter.
For properties that should work like variables, use
DataProperty. For properties that need to run code every time they are read, use this class.Functional properties should work very much like Python’s
builtins.propertyexcept that they are also available over HTTP.Set up a FunctionalProperty.
Create a descriptor for a property that uses a getter function.
This class also inherits from
builtins.propertyto help type checking tools understand that it functions like a property.- Parameters:
fget – the getter function, called when the property is read.
constraints – is passed as keyword arguments to
pydantic.Fieldto add validation constraints to the property. Seepydantic.Fieldfor details.
- Raises:
MissingTypeError – if the getter does not have a return type annotation.
- _fget
- _type
- fget() Callable[[Owner], Value]
The getter function.
- getter(fget: Callable[[Owner], Value]) typing_extensions.Self
Set the getter function of the property.
This function returns the descriptor, so it may be used as a decorator. If the function has a docstring, it will be used as the property docstring.
- Parameters:
fget – The new getter function.
- Returns:
this descriptor (i.e.
self). This allows use as a decorator.
- setter(fset: Callable[[Owner, Value], None]) typing_extensions.Self
Set the setter function of the property.
This function returns the descriptor, so it may be used as a decorator.
Once a setter has been added to a property, it will automatically become writeable from client code (over HTTP and via
DirectThingClient). To override this behaviour you may setreadonlyback toTrue.class MyThing(lt.Thing): def __init__(self, thing_server_interface): super().__init__(thing_server_interface=thing_server_interface) self._myprop: int = 0 @lt.property def myprop(self) -> int: "An example property that is an integer" return self._myprop @myprop.setter def _set_myprop(self, val: int) -> None: self._myprop = val myprop.readonly = True # Prevent client code from setting it
Note
The example code above is not quite what would be done for the built-in
@propertydecorator, because our setter does not have the same name as the getter. Using a different name avoids type checkers such asmypyraising an error that the getter has been redefined with a different type. The behaviour is identical whether the setter and getter have the same name or not. The only difference is that theThingwill have an additional method called_set_mypropin the example above.- Parameters:
fset – The new setter function.
- Returns:
this descriptor (i.e.
self). This allows use as a decorator.
Typing Notes
Python’s built-in
propertyis treated as a special case bymypyand others, and our descriptor is not treated in the same way. Naming the setter and getter the same is required bybuiltins.propertybecause the property must be overwritten when the setter is added, asbuiltins.propertyis not mutable.Our descriptor is mutable, so the setter may be added without having to overwrite the object. While it would be nice to use exactly the same conventions as
builtins.property, it currently causes type errors that must be silenced manually. We suggest using a different name for the setter as an alternative to adding# type: ignore[no-redef]to the setter function.It will cause problems elsewhere in the code if descriptors are assigned to more than one attribute, and this is checked in
BaseDescriptor.__set_name__. We therefore return the setter rather than the descriptor if the names don’t match. The type hint does not reflect this, as it would cause problems when the names do match (the descriptor would become aFunctionalProperty | Callableand thus typing errors would happen whenever it’s accessed).
- instance_get(obj: Owner) Value
Get the value of the property.
- Parameters:
obj – the
Thingon which the attribute is accessed.- Returns:
the value of the property.
- __set__(obj: Owner, value: Value) None
Set the value of the property.
- Parameters:
obj – the
Thingon which the attribute is accessed.value – the value of the property.
- Raises:
ReadOnlyPropertyError – if the property cannot be set.
- class labthings_fastapi.properties.PropertyInfo(descriptor: Descriptor, obj: Owner | None, cls: type[Owner] | None = None)
Bases:
labthings_fastapi.base_descriptor.FieldTypedBaseDescriptorInfo[BasePropertyT,Owner,Value],Generic[BasePropertyT,Owner,Value]Access to the metadata of a Property.
This class provides a way to access the metadata of a Property, without needing to retrieve the Descriptor object directly. It may be bound to a
Thinginstance, or may be accessed from the class.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.
- model() type[pydantic.BaseModel]
A
pydantic.BaseModeldescribing this property’s value.
- model_instance() pydantic.BaseModel
An instance of
self.modelpopulated with the current value.- Raises:
TypeError – if the return value can’t be wrapped in a model.
- validate(value: Any) Value
Use the validation logic in
self.model.This method should accept anything that
pydanticcan convert to the right type, and return a correctly-typed value. It also enforces any constraints that were set on the property.- Parameters:
value – The new value, in any form acceptable to the property’s model. Usually this means you can use either the correct type, or the value as loaded from JSON.
- Returns:
the new value, with the correct type.
- Raises:
ValidationError – if the supplied value can’t be loaded by the property’s model. This is the exception raised by
model_validateTypeError – if the property has a
modelthat’s inconsistent with its value type. This should never happen.
- class labthings_fastapi.properties.PropertyCollection(obj: Owner | None, cls: type[Owner] | None = None)
Bases:
labthings_fastapi.base_descriptor.DescriptorInfoCollection[Owner,PropertyInfo],Generic[Owner]Access to metadata on all the properties of a
Thinginstance or subclass.This object may be used as a mapping, to retrieve
PropertyInfoobjects for each Property of aThingby name. This allows easy access to metadata like their description and model.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
The class of DescriptorInfo objects contained in this collection.
This class attribute must be set in subclasses.
- class labthings_fastapi.properties.BaseSetting(constraints: collections.abc.Mapping[str, Any] | None = None)
Bases:
BaseProperty[Owner,Value],Generic[Owner,Value]A base class for settings.
This is a subclass of
BasePropertythat is used to define settings. It is not intended to be used directly, but viasettingand the two concrete implementations:DataSettingandFunctionalSetting.Initialise a BaseProperty.
- Parameters:
constraints – is passed as keyword arguments to
pydantic.Fieldto add validation constraints to the property. Seepydantic.Fieldfor details. The module-level constantCONSTRAINT_ARGSlists the supported constraint arguments.- Raises:
UnsupportedConstraintError – if unsupported constraint arguments are supplied. See
CONSTRAINT_ARGSfor the supported arguments.
- abstract set_without_emit(obj: Owner, value: Value) None
Set the setting’s value without emitting an event.
This is used to set the setting’s value without notifying observers. It is used during initialisation to set the value from disk before the server is fully started.
- Parameters:
obj – the
Thingto which we are attached.value – the new value of the setting.
- Raises:
NotImplementedError – this method should be implemented in subclasses.
- descriptor_info(owner: Owner | None = None) SettingInfo[Owner, Value]
Return an object that allows access to this descriptor’s metadata.
- Parameters:
owner – An instance to bind the descriptor info to. If
None, the returned object will be unbound and will only refer to the class.- Returns:
A
SettingInfoinstance describing this setting.
- class labthings_fastapi.properties.FunctionalSetting(fget: Callable[[Owner], Value], constraints: collections.abc.Mapping[str, Any] | None = None)
Bases:
FunctionalProperty[Owner,Value],BaseSetting[Owner,Value],Generic[Owner,Value]A
FunctionalPropertythat persists on disk.A setting can be accessed via the HTTP API and is persistent between sessions.
A
FunctionalSettingis aFunctionalPropertywith extra functionality for triggering aThingto save its settings.Note: If a setting is mutated rather than assigned to, this will not trigger saving. For example: if a Thing has a setting called
dictsettingholding the dictionary{"a": 1, "b": 2}thenself.dictsetting = {"a": 2, "b": 2}would trigger saving butself.dictsetting[a] = 2would not, as the setter fordictsettingis never called.The setting otherwise acts just like a
FunctionalProperty`, i.e. it uses a getter and a setter function.Set up a FunctionalProperty.
Create a descriptor for a property that uses a getter function.
This class also inherits from
builtins.propertyto help type checking tools understand that it functions like a property.- Parameters:
fget – the getter function, called when the property is read.
constraints – is passed as keyword arguments to
pydantic.Fieldto add validation constraints to the property. Seepydantic.Fieldfor details.
- Raises:
MissingTypeError – if the getter does not have a return type annotation.
- __set__(obj: Owner, value: Value) None
Set the setting’s value.
This will cause the settings to be saved to disk.
- Parameters:
obj – the
Thingto which we are attached.value – the new value of the setting.
- set_without_emit(obj: Owner, value: Value) None
Set the property’s value, but do not emit event to notify the server.
This function is not expected to be used externally. It is called during initial setup so that the setting can be set from disk before the server is fully started.
- Parameters:
obj – the
Thingto which we are attached.value – the new value of the setting.
- class labthings_fastapi.properties.SettingInfo(descriptor: Descriptor, obj: Owner | None, cls: type[Owner] | None = None)
Bases:
PropertyInfo[BaseSetting[Owner,Value],Owner,Value],Generic[Owner,Value]Access to the metadata of a setting.
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.
- class labthings_fastapi.properties.SettingCollection(obj: Owner | None, cls: type[Owner] | None = None)
Bases:
labthings_fastapi.base_descriptor.DescriptorInfoCollection[Owner,SettingInfo],Generic[Owner]Access to metadata on all the properties of a
Thinginstance or subclass.This object may be used as a mapping, to retrieve
PropertyInfoobjects for each Property of aThingby name. This allows easy access to metadata like their description and model.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
The class of DescriptorInfo objects contained in this collection.
This class attribute must be set in subclasses.
- model() type[pydantic.BaseModel]
A
pydantic.BaseModelrepresenting all the settings.This
pydantic.BaseModelis used to load and save the settings to a file. Note that it uses themodelof each setting, so every field in this model will be either aBaseModelor aRootModelinstance, unless it is missing.Wrapping plain types in a
RootModelmakes no difference to the JSON, but it means that constraints will be applied and it makes it easier to distinguish between missing fields and fields that are set toNone.
- model_instance() pydantic.BaseModel
An instance of
self.modelpopulated with the current setting values.