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

CONSTRAINT_ARGS

The set of supported constraint arguments for properties.

Value

The value returned by a property.

Owner

The Thing instance on which a property is bound.

BasePropertyT

An instance of (a subclass of) BaseProperty.

Exceptions

OverspecifiedDefaultError

The default value has been specified more than once.

MissingDefaultError

The default value has not been specified.

Classes

BaseProperty

A descriptor that marks Properties on Things.

FunctionalProperty

A property that uses a getter and a setter.

PropertyInfo

Access to the metadata of a Property.

PropertyCollection

Access to metadata on all the properties of a Thing instance or subclass.

BaseSetting

A base class for settings.

FunctionalSetting

A FunctionalProperty that persists on disk.

SettingInfo

Access to the metadata of a setting.

SettingCollection

Access to metadata on all the properties of a Thing instance or subclass.

Functions

default_factory_from_arguments(→ Callable[[], Value])

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: ValueError

The default value has been specified more than once.

This error is raised when a DataProperty is instantiated with both a default value and a default_factory provided.

Initialize self. See help(type(self)) for accurate signature.

exception labthings_fastapi.properties.MissingDefaultError

Bases: ValueError

The default value has not been specified.

This error is raised when a DataProperty is instantiated without a default value or a default_factory function.

Initialize self. See help(type(self)) for accurate signature.

labthings_fastapi.properties.Value

The value returned by a property.

labthings_fastapi.properties.Owner

The Thing instance on which a property is bound.

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 default and default_factory arguments and will either return the default_factory if 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 DataProperty but it was needed in the property and setting functions in order to correctly type them (so that specifying both or neither of the default and default_factory arguments 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:
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 Thing should be treated as a Property (see Properties - essentially, it means the value should be available over HTTP).

BaseProperty should not be used directly, instead it is recommended to use property to declare properties on your Thing subclass.

Initialise a BaseProperty.

Parameters:

constraints – is passed as keyword arguments to pydantic.Field to add validation constraints to the property. See pydantic.Field for details. The module-level constant CONSTRAINT_ARGS lists the supported constraint arguments.

Raises:

UnsupportedConstraintError – if unsupported constraint arguments are supplied. See CONSTRAINT_ARGS for the supported arguments.

_model: type[pydantic.BaseModel] | None = None
readonly: bool = False
constraints
model() type[pydantic.BaseModel]

A Pydantic model for the property’s type.

pydantic models are used to serialise and deserialise values from and to JSON. If the property is defined with a type hint that is not a pydantic.BaseModel subclass, this property will ensure it is wrapped in a pydantic.RootModel so it can be used with FastAPI.

If BaseProperty.value_type is already a pydantic.BaseModel subclass, 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 Thing we are adding the endpoints for.

Raises:

NotConnectedToServerError – if the Thing does not have a path set.

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:
  • thing – the Thing to which we are attached.

  • path – the URL of the Thing. If not present, we will retrieve the path from thing.

Returns:

A description of the property in Thing Description format.

Raises:

NotConnectedToServerError – if the Thing does not have a path set.

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 PropertyInfo instance 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.property except 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.property to 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.Field to add validation constraints to the property. See pydantic.Field for details.

Raises:

MissingTypeError – if the getter does not have a return type annotation.

_fget
_type
_fset: Callable[[Owner, Value], None] | None = None
readonly: bool = True
fget() Callable[[Owner], Value]

The getter function.

fset() Callable[[Owner, Value], None] | None

The setter 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 set readonly back to True.

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 @property decorator, because our setter does not have the same name as the getter. Using a different name avoids type checkers such as mypy raising 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 the Thing will have an additional method called _set_myprop in 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 property is treated as a special case by mypy and others, and our descriptor is not treated in the same way. Naming the setter and getter the same is required by builtins.property because the property must be overwritten when the setter is added, as builtins.property is 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 a FunctionalProperty | Callable and thus typing errors would happen whenever it’s accessed).

instance_get(obj: Owner) Value

Get the value of the property.

Parameters:

obj – the Thing on 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 Thing on 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 Thing instance, or may be accessed from the class.

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.

model() type[pydantic.BaseModel]

A pydantic.BaseModel describing this property’s value.

model_instance() pydantic.BaseModel

An instance of self.model populated with the current value.

Raises:

TypeError – if the return value can’t be wrapped in a model.

model_to_value(value: pydantic.BaseModel) Value

Convert a model to a value for this property.

Even properties with plain types are sometimes converted to or from a pydantic.BaseModel to allow conversion to/from JSON. This is a convenience method that accepts a model (which should be an instance of self.model) and unwraps it when necessary to get the plain Python value.

Parameters:

value – A BaseModel instance to convert.

Returns:

the value, with RootModel unwrapped so it matches the descriptor’s type.

Raises:

TypeError – if the supplied value cannot be converted to the right type.

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 Thing instance or subclass.

This object may be used as a mapping, to retrieve PropertyInfo objects for each Property of a Thing by name. This allows easy access to metadata like their description and model.

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

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 BaseProperty that is used to define settings. It is not intended to be used directly, but via setting and the two concrete implementations: DataSetting and FunctionalSetting.

Initialise a BaseProperty.

Parameters:

constraints – is passed as keyword arguments to pydantic.Field to add validation constraints to the property. See pydantic.Field for details. The module-level constant CONSTRAINT_ARGS lists the supported constraint arguments.

Raises:

UnsupportedConstraintError – if unsupported constraint arguments are supplied. See CONSTRAINT_ARGS for 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 Thing to 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 SettingInfo instance 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 FunctionalProperty that persists on disk.

A setting can be accessed via the HTTP API and is persistent between sessions.

A FunctionalSetting is a FunctionalProperty with extra functionality for triggering a Thing to 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 dictsetting holding the dictionary {"a": 1, "b": 2} then self.dictsetting = {"a": 2, "b": 2} would trigger saving but self.dictsetting[a] = 2 would not, as the setter for dictsetting is 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.property to 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.Field to add validation constraints to the property. See pydantic.Field for 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 Thing to 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 Thing to 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 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.

set_without_emit(value: Value) None

Set the value of the setting, but don’t emit a notification.

Parameters:

value – the new value for the setting.

set_without_emit_from_model(value: pydantic.BaseModel) None

Set the value from a model instance, unwrapping RootModels as needed.

Parameters:

value – the model to extract the value from.

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 Thing instance or subclass.

This object may be used as a mapping, to retrieve PropertyInfo objects for each Property of a Thing by name. This allows easy access to metadata like their description and model.

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

The class of DescriptorInfo objects contained in this collection.

This class attribute must be set in subclasses.

model() type[pydantic.BaseModel]

A pydantic.BaseModel representing all the settings.

This pydantic.BaseModel is used to load and save the settings to a file. Note that it uses the model of each setting, so every field in this model will be either a BaseModel or a RootModel instance, unless it is missing.

Wrapping plain types in a RootModel makes 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 to None.

model_instance() pydantic.BaseModel

An instance of self.model populated with the current setting values.