labthings_fastapi.properties ============================ .. py:module:: labthings_fastapi.properties .. autoapi-nested-parse:: Define properties of `.Thing` objects. :ref:`properties` are attributes of a `.Thing` that may be read or written to over HTTP, and they are described in :ref:`gen_docs`. 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: .. code-block:: python 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 ---------- .. autoapisummary:: labthings_fastapi.properties.CONSTRAINT_ARGS labthings_fastapi.properties.Value labthings_fastapi.properties.Owner labthings_fastapi.properties.BasePropertyT Exceptions ---------- .. autoapisummary:: labthings_fastapi.properties.OverspecifiedDefaultError labthings_fastapi.properties.MissingDefaultError Classes ------- .. autoapisummary:: labthings_fastapi.properties.BaseProperty labthings_fastapi.properties.FunctionalProperty labthings_fastapi.properties.PropertyInfo labthings_fastapi.properties.PropertyCollection labthings_fastapi.properties.BaseSetting labthings_fastapi.properties.FunctionalSetting labthings_fastapi.properties.SettingInfo labthings_fastapi.properties.SettingCollection Functions --------- .. autoapisummary:: labthings_fastapi.properties.default_factory_from_arguments Module Contents --------------- .. py:data:: CONSTRAINT_ARGS The set of supported constraint arguments for properties. .. py:exception:: OverspecifiedDefaultError Bases: :py:obj:`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. .. py:exception:: MissingDefaultError Bases: :py:obj:`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. .. py:data:: Value The value returned by a property. .. py:data:: Owner The `.Thing` instance on which a property is bound. .. py:data:: BasePropertyT An instance of (a subclass of) BaseProperty. .. py:function:: 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). :param default: the default value, or an ellipsis if not specified. :param default_factory: a function that returns the default value. :return: a function that returns the default value. :raises OverspecifiedDefaultError: if both ``default`` and ``default_factory`` are specified. :raises MissingDefaultError: if neither ``default`` nor ``default_factory`` are specified. .. py:class:: BaseProperty(constraints: collections.abc.Mapping[str, Any] | None = None) Bases: :py:obj:`labthings_fastapi.base_descriptor.FieldTypedBaseDescriptor`\ [\ :py:obj:`Owner`\ , :py:obj:`Value`\ ], :py:obj:`Generic`\ [\ :py:obj:`Owner`\ , :py:obj:`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 :ref:`wot_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. :param 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. .. py:attribute:: _model :type: type[pydantic.BaseModel] | None :value: None .. py:attribute:: readonly :type: bool :value: False .. py:attribute:: constraints .. py:method:: 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. :return: a Pydantic model for the property's type. .. py:method:: add_to_fastapi(app: fastapi.FastAPI, thing: Owner) -> None Add this action to a FastAPI app, bound to a particular Thing. :param app: The FastAPI application we are adding endpoints to. :param thing: The `.Thing` we are adding the endpoints for. :raises NotConnectedToServerError: if the `.Thing` does not have a ``path`` set. .. py:method:: property_affordance(thing: labthings_fastapi.thing.Thing, path: str | None = None) -> labthings_fastapi.thing_description._model.PropertyAffordance Represent the property in a Thing Description. :param thing: the `.Thing` to which we are attached. :param path: the URL of the `.Thing`. If not present, we will retrieve the ``path`` from ``thing``. :return: A description of the property in :ref:`wot_td` format. :raises NotConnectedToServerError: if the `.Thing` does not have a ``path`` set. .. py:method:: __set__(obj: Owner, value: Any) -> None :abstractmethod: Set the property (stub method). This is a stub ``__set__`` method to mark this as a data descriptor. :param obj: The Thing on which we are setting the value. :param value: The new value for the Thing. :raises NotImplementedError: as this must be overridden by concrete classes. .. py:method:: descriptor_info(owner: Owner | None = None) -> PropertyInfo[typing_extensions.Self, Owner, Value] Return an object that allows access to this descriptor's metadata. :param owner: An instance to bind the descriptor info to. If `None`\ , the returned object will be unbound and will only refer to the class. :return: A `PropertyInfo` instance describing this property. .. py:class:: FunctionalProperty(fget: Callable[[Owner], Value], constraints: collections.abc.Mapping[str, Any] | None = None) Bases: :py:obj:`BaseProperty`\ [\ :py:obj:`Owner`\ , :py:obj:`Value`\ ], :py:obj:`Generic`\ [\ :py:obj:`Owner`\ , :py:obj:`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. :param fget: the getter function, called when the property is read. :param 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. .. py:attribute:: _fget .. py:attribute:: _type .. py:attribute:: _fset :type: Callable[[Owner, Value], None] | None :value: None .. py:attribute:: readonly :type: bool :value: True .. py:method:: fget() -> Callable[[Owner], Value] The getter function. .. py:method:: fset() -> Callable[[Owner, Value], None] | None The setter function. .. py:method:: 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. :param fget: The new getter function. :return: this descriptor (i.e. ``self``). This allows use as a decorator. .. py:method:: 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``. .. code-block:: python 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. :param fset: The new setter function. :return: 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). .. py:method:: instance_get(obj: Owner) -> Value Get the value of the property. :param obj: the `.Thing` on which the attribute is accessed. :return: the value of the property. .. py:method:: __set__(obj: Owner, value: Value) -> None Set the value of the property. :param obj: the `.Thing` on which the attribute is accessed. :param value: the value of the property. :raises ReadOnlyPropertyError: if the property cannot be set. .. py:class:: PropertyInfo(descriptor: Descriptor, obj: Owner | None, cls: type[Owner] | None = None) Bases: :py:obj:`labthings_fastapi.base_descriptor.FieldTypedBaseDescriptorInfo`\ [\ :py:obj:`BasePropertyT`\ , :py:obj:`Owner`\ , :py:obj:`Value`\ ], :py:obj:`Generic`\ [\ :py:obj:`BasePropertyT`\ , :py:obj:`Owner`\ , :py:obj:`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``\ . :param descriptor: The descriptor that this object will describe. :param 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. :param cls: The class to which we are bound. Only required if ``obj`` is `None`\ . :raises ValueError: if both ``obj`` and ``cls`` are `None`\ . .. py:method:: model() -> type[pydantic.BaseModel] A `pydantic.BaseModel` describing this property's value. .. py:method:: 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. .. py:method:: 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. :param value: A `.BaseModel` instance to convert. :return: 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. .. py:class:: PropertyCollection(obj: Owner | None, cls: type[Owner] | None = None) Bases: :py:obj:`labthings_fastapi.base_descriptor.DescriptorInfoCollection`\ [\ :py:obj:`Owner`\ , :py:obj:`PropertyInfo`\ ], :py:obj:`Generic`\ [\ :py:obj:`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`\ . :param 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. :param cls: The class to which this info object refers. May be omitted if `obj` is supplied. .. py:attribute:: _descriptorinfo_class The class of DescriptorInfo objects contained in this collection. This class attribute must be set in subclasses. .. py:class:: BaseSetting(constraints: collections.abc.Mapping[str, Any] | None = None) Bases: :py:obj:`BaseProperty`\ [\ :py:obj:`Owner`\ , :py:obj:`Value`\ ], :py:obj:`Generic`\ [\ :py:obj:`Owner`\ , :py:obj:`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. :param 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. .. py:method:: set_without_emit(obj: Owner, value: Value) -> None :abstractmethod: 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. :param obj: the `.Thing` to which we are attached. :param value: the new value of the setting. :raises NotImplementedError: this method should be implemented in subclasses. .. py:method:: descriptor_info(owner: Owner | None = None) -> SettingInfo[Owner, Value] Return an object that allows access to this descriptor's metadata. :param owner: An instance to bind the descriptor info to. If `None`\ , the returned object will be unbound and will only refer to the class. :return: A `SettingInfo` instance describing this setting. .. py:class:: FunctionalSetting(fget: Callable[[Owner], Value], constraints: collections.abc.Mapping[str, Any] | None = None) Bases: :py:obj:`FunctionalProperty`\ [\ :py:obj:`Owner`\ , :py:obj:`Value`\ ], :py:obj:`BaseSetting`\ [\ :py:obj:`Owner`\ , :py:obj:`Value`\ ], :py:obj:`Generic`\ [\ :py:obj:`Owner`\ , :py:obj:`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. :param fget: the getter function, called when the property is read. :param 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. .. py:method:: __set__(obj: Owner, value: Value) -> None Set the setting's value. This will cause the settings to be saved to disk. :param obj: the `.Thing` to which we are attached. :param value: the new value of the setting. .. py:method:: 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. :param obj: the `.Thing` to which we are attached. :param value: the new value of the setting. .. py:class:: SettingInfo(descriptor: Descriptor, obj: Owner | None, cls: type[Owner] | None = None) Bases: :py:obj:`PropertyInfo`\ [\ :py:obj:`BaseSetting`\ [\ :py:obj:`Owner`\ , :py:obj:`Value`\ ]\ , :py:obj:`Owner`\ , :py:obj:`Value`\ ], :py:obj:`Generic`\ [\ :py:obj:`Owner`\ , :py:obj:`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``\ . :param descriptor: The descriptor that this object will describe. :param 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. :param cls: The class to which we are bound. Only required if ``obj`` is `None`\ . :raises ValueError: if both ``obj`` and ``cls`` are `None`\ . .. py:method:: set_without_emit(value: Value) -> None Set the value of the setting, but don't emit a notification. :param value: the new value for the setting. .. py:method:: set_without_emit_from_model(value: pydantic.BaseModel) -> None Set the value from a model instance, unwrapping RootModels as needed. :param value: the model to extract the value from. .. py:class:: SettingCollection(obj: Owner | None, cls: type[Owner] | None = None) Bases: :py:obj:`labthings_fastapi.base_descriptor.DescriptorInfoCollection`\ [\ :py:obj:`Owner`\ , :py:obj:`SettingInfo`\ ], :py:obj:`Generic`\ [\ :py:obj:`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`\ . :param 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. :param cls: The class to which this info object refers. May be omitted if `obj` is supplied. .. py:attribute:: _descriptorinfo_class The class of DescriptorInfo objects contained in this collection. This class attribute must be set in subclasses. .. py:method:: 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`. .. py:method:: model_instance() -> pydantic.BaseModel An instance of ``self.model`` populated with the current setting values.