From 493b0732f2590263470a1d096fa2be05eb9a62cb Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 1 Apr 2026 16:44:56 +0100 Subject: [PATCH 1/4] Create a top-level summary for the API docs, referenced as `lt.*` in the rest of the documentation. --- docs/source/actions.rst | 24 +- docs/source/concurrency.rst | 12 +- docs/source/conf.py | 26 +- docs/source/developer_notes/descriptors.rst | 8 +- docs/source/examples.rst | 4 +- docs/source/index.rst | 7 +- docs/source/properties.rst | 20 +- docs/source/quick_reference.rst | 455 ++++++++++++++++++ docs/source/removed_features.rst | 2 +- docs/source/structure.rst | 24 +- docs/source/thing_slots.rst | 28 +- docs/source/tutorial/running_labthings.rst | 2 +- docs/source/tutorial/writing_a_thing.rst | 4 +- docs/source/using_things.rst | 12 +- docs/source/wot_core_concepts.rst | 2 +- src/labthings_fastapi/__init__.py | 5 + src/labthings_fastapi/actions.py | 54 +-- src/labthings_fastapi/base_descriptor.py | 41 +- src/labthings_fastapi/client/__init__.py | 16 +- src/labthings_fastapi/endpoints.py | 36 +- src/labthings_fastapi/exceptions.py | 27 +- src/labthings_fastapi/invocation_contexts.py | 4 +- src/labthings_fastapi/logs.py | 4 +- src/labthings_fastapi/outputs/blob.py | 2 +- src/labthings_fastapi/outputs/mjpeg_stream.py | 18 +- src/labthings_fastapi/properties.py | 142 +++--- src/labthings_fastapi/server/__init__.py | 76 +-- src/labthings_fastapi/server/cli.py | 6 +- src/labthings_fastapi/server/config_model.py | 34 +- src/labthings_fastapi/testing.py | 14 +- src/labthings_fastapi/thing.py | 45 +- .../thing_description/__init__.py | 2 +- .../thing_server_interface.py | 14 +- src/labthings_fastapi/thing_slots.py | 96 ++-- src/labthings_fastapi/utilities/__init__.py | 12 +- .../utilities/introspection.py | 4 +- src/labthings_fastapi/websockets.py | 8 +- tests/test_blob_output.py | 2 +- tests/test_property.py | 2 +- tests/test_settings.py | 2 +- typing_tests/thing_properties.py | 2 +- 41 files changed, 881 insertions(+), 417 deletions(-) create mode 100644 docs/source/quick_reference.rst diff --git a/docs/source/actions.rst b/docs/source/actions.rst index 8e63a1d6..03d78c4b 100644 --- a/docs/source/actions.rst +++ b/docs/source/actions.rst @@ -3,9 +3,9 @@ Actions ======= -Actions are the way `.Thing` objects are instructed to do things. In Python -terms, any method of a `.Thing` that we want to be able to call over HTTP -should be decorated as an Action, using `.action`. +Actions are the way `~lt.Thing` objects are instructed to do things. In Python +terms, any method of a `~lt.Thing` that we want to be able to call over HTTP +should be decorated as an Action, using `lt.action`. This page gives an overview of how actions are implemented in LabThings-FastAPI. Our implementation should align with :ref:`wot_actions` as defined by the Web of Things standard. @@ -19,7 +19,7 @@ invokes an action will return almost immediately with a ``201`` code, and a JSON payload that describes the invocation as an `.InvocationModel`. This includes a link ``href`` that can be polled to check the status of the invocation. -The HTTP implementation of `.ThingClient` first makes a ``POST`` request to +The HTTP implementation of `~lt.ThingClient` first makes a ``POST`` request to invoke the action, then polls the invocation using the ``href`` supplied. Once the action has finished (i.e. its status is ``completed``, ``error``, or ``cancelled``), its output (the return value) is retrieved and used as the @@ -38,14 +38,14 @@ where Invocation-related HTTP endpoints are handled, including listing all the Running actions from other actions ---------------------------------- -If code running in a `.Thing` runs methods belonging either to that `.Thing` -or to another `.Thing` on the same server, no new thread is created: the +If code running in a `~lt.Thing` runs methods belonging either to that `~lt.Thing` +or to another `~lt.Thing` on the same server, no new thread is created: the called action runs in the same thread as the calling action, just like any other Python code. Action inputs and outputs ------------------------- -The code that implements an action is a method of a `.Thing`, meaning it is +The code that implements an action is a method of a `~lt.Thing`, meaning it is a function. The input parameters are the function's arguments, and the output parameter is the function's return value. Type hints on both arguments and return value are used to document the action in the OpenAPI description and @@ -58,7 +58,7 @@ Python construct giving access to the object on which the action is defined. Logging from actions -------------------- -Action code should use `.Thing.logger` to log messages. This will be configured +Action code should use `~lt.Thing.logger` to log messages. This will be configured to handle messages on a per-invocation basis and make them available when the action is queried over HTTP. @@ -101,7 +101,7 @@ If an action could run for a long time, it is useful to be able to cancel it cleanly. LabThings makes provision for this by allowing actions to be cancelled using a ``DELETE`` HTTP request. In order to allow an action to be cancelled, you must give LabThings opportunities to interrupt it. This is most often done -by replacing a `time.sleep()` statement with `.cancellable_sleep()` which +by replacing a `time.sleep()` statement with `lt.cancellable_sleep()` which is equivalent, but will raise an exception if the action is cancelled. For more advanced options, see `.invocation_contexts` for detail. @@ -116,15 +116,15 @@ such that the action code can use module-level symbols rather than needing to explicitly pass the logger and cancel hook as arguments to the action method. -Usually, you don't need to consider this mechanism: simply use `.Thing.logger` -or `.cancellable_sleep` as explained above. However, if you want to run actions +Usually, you don't need to consider this mechanism: simply use `~lt.Thing.logger` +or `~lt.cancellable_sleep` as explained above. However, if you want to run actions outside of the server (for example, for testing purposes) or if you want to call one action from another action, but not share the cancellation signal or log, functions are provided in `.invocation_contexts` to manage this. If you start a new thread from an action, code running in that thread will not have an invocation ID set in a context variable. A subclass of -`threading.Thread` is provided to do this, `.ThreadWithInvocationID`\ . +`threading.Thread` is provided to do this, `~lt.ThreadWithInvocationID`\ . This may be useful for test code, or if you wish to run actions in the background, with the option of cancelling them. diff --git a/docs/source/concurrency.rst b/docs/source/concurrency.rst index 2a25b64d..3d92265f 100644 --- a/docs/source/concurrency.rst +++ b/docs/source/concurrency.rst @@ -5,28 +5,28 @@ Concurrency in LabThings-FastAPI One of the major challenges when controlling hardware, particularly from web frameworks, is concurrency. Most web frameworks assume resources (database connections, object storage, etc.) may be instantiated multiple times, and often initialise or destroy objects as required. In contrast, hardware can usually only be controlled from one process, and usually is initialised and shut down only once. -LabThings-FastAPI instantiates each :class:`.Thing` only once, and runs all code in a thread. More specifically, each time an action is invoked via HTTP, a new thread is created to run the action. Similarly, each time a property is read or written, a new thread is created to run the property method. This means that :class:`.Thing` code should protect important variables or resources using locks from the `threading` module, and need not worry about writing asynchronous code. +LabThings-FastAPI instantiates each :class:`~lt.Thing` only once, and runs all code in a thread. More specifically, each time an action is invoked via HTTP, a new thread is created to run the action. Similarly, each time a property is read or written, a new thread is created to run the property method. This means that :class:`~lt.Thing` code should protect important variables or resources using locks from the `threading` module, and need not worry about writing asynchronous code. -In the case of properties, the HTTP response is only returned once the `.Thing` code is complete. Actions currently return a response immediately, and must be polled to determine when they have completed. This behaviour may change in the future, most likely with the introduction of a timeout to allow the client to choose between waiting for a response or polling. +In the case of properties, the HTTP response is only returned once the `~lt.Thing` code is complete. Actions currently return a response immediately, and must be polled to determine when they have completed. This behaviour may change in the future, most likely with the introduction of a timeout to allow the client to choose between waiting for a response or polling. Many of the functions that handle HTTP requests are asynchronous, running in an :mod:`anyio` event loop. This enables many HTTP connections to be handled at once with good efficiency. The `anyio documentation`_ describes the functions that link between async and threaded code. When the LabThings server is started, we create an :class:`anyio.from_thread.BlockingPortal`, which allows threaded code to run code asynchronously in the event loop. -An action can run async code using its server interface. See `.ThingServerInterface.start_async_task_soon` for details. +An action can run async code using its server interface. See `~lt.ThingServerInterface.start_async_task_soon` for details. -There are relatively few occasions when `.Thing` code will need to consider this explicitly: more usually the blocking portal will be obtained by a LabThings function, for example the `.MJPEGStream` class. +There are relatively few occasions when `~lt.Thing` code will need to consider this explicitly: more usually the blocking portal will be obtained by a LabThings function, for example the `.MJPEGStream` class. .. _`anyio documentation`: https://anyio.readthedocs.io/en/stable/threads.html Calling Things from other Things -------------------------------- -When one `Thing` calls the actions or properties of another `.Thing`, either directly or via a `.DirectThingClient`, no new threads are spawned: the action or property is run in the same thread as the caller. This mirrors the behaviour of the `.ThingClient`, which blocks until the action or property is complete. See :doc:`using_things` for more details on how to call actions and properties of other Things. +When one `Thing` calls the actions or properties of another `~lt.Thing`, either directly or via a `.DirectThingClient`, no new threads are spawned: the action or property is run in the same thread as the caller. This mirrors the behaviour of the `~lt.ThingClient`, which blocks until the action or property is complete. See :doc:`using_things` for more details on how to call actions and properties of other Things. Invocations and concurrency --------------------------- Each time an action is run ("invoked" in :ref:`wot_cc`), we create a new thread to run it. This thread has a context variable set, such that ``lt.cancellable_sleep`` and ``lt.get_invocation_logger`` are aware of which invocation is currently running. If an action spawns a new thread (e.g. using `threading.Thread`\ ), this new thread will not have an invocation ID, and consequently the two invocation-specific functions mentioned will not work. -Usually, the best solution to this problem is to generate a new invocation ID for the thread. This means only the original action thread will receive cancellation events, and only the original action thread will log to the invocation logger. If the action is cancelled, you must cancel the background thread. This is the behaviour of `.ThreadWithInvocationID`\ . +Usually, the best solution to this problem is to generate a new invocation ID for the thread. This means only the original action thread will receive cancellation events, and only the original action thread will log to the invocation logger. If the action is cancelled, you must cancel the background thread. This is the behaviour of `~lt.ThreadWithInvocationID`\ . It is also possible to copy the current invocation ID to a new thread. This is often a bad idea, as it's ill-defined whether the exception will arise in the original thread or the new one if the invocation is cancelled. Logs from the two threads will also be interleaved. If it's desirable to log from the background thread, the invocation logger may safely be passed as an argument, rather than accessed via ``lt.get_invocation_logger``\ . diff --git a/docs/source/conf.py b/docs/source/conf.py index ed4bad66..348203c9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,5 +1,6 @@ +import inspect import logging -import labthings_fastapi +import labthings_fastapi as lt import importlib.metadata # Configuration file for the Sphinx documentation builder. @@ -20,6 +21,7 @@ extensions = [ "sphinx.ext.intersphinx", + "sphinx.ext.autodoc", # "sphinx.ext.napoleon", # "autodoc2", "autoapi.extension", @@ -66,10 +68,7 @@ skipper_log.addHandler(logging.FileHandler("./skipper.log", mode="w")) skipper_log.setLevel(logging.DEBUG) -convenience_modules = { - "labthings_fastapi": labthings_fastapi.__all__, -} -canonical_fq_names = [ +canonical_fq_names = { "labthings_fastapi.descriptors.action.ActionDescriptor", "labthings_fastapi.outputs.blob.BlobDataManager", "labthings_fastapi.invocations.InvocationModel", @@ -79,7 +78,16 @@ "labthings_fastapi.actions.ActionManager", "labthings_fastapi.descriptors.endpoint.EndpointDescriptor", "labthings_fastapi.utilities.introspection.EmptyObject", -] +} + +# Everything in `labthings_fastapi` is documented elsewhere, so we +# add all of those fq names to the list. +top_level_objects = [getattr(lt, name) for name in lt.__all__] +canonical_fq_names.update( + f"{obj.__module__}.{obj.__qualname__}" + for obj in top_level_objects + if not inspect.ismodule(obj) +) def unqual(name): @@ -90,8 +98,6 @@ def unqual(name): canonical_names = {unqual(n): n for n in canonical_fq_names} -skipper_log.info("Convenience modules: %s.", convenience_modules) - def skip_public_api(app, what, name: str, obj, skip, options): """Skip documenting members that are re-exported from the public API.""" @@ -100,10 +106,6 @@ def skip_public_api(app, what, name: str, obj, skip, options): if unqual in canonical_names and name != canonical_names[unqual]: skip = True return skip - for conv, all in convenience_modules.items(): - if unqual in all and name != f"{conv}.{unqual}": - skipper_log.warning(f"skipping {name}") - skip = True return skip diff --git a/docs/source/developer_notes/descriptors.rst b/docs/source/developer_notes/descriptors.rst index 952f6c7d..0a4b5326 100644 --- a/docs/source/developer_notes/descriptors.rst +++ b/docs/source/developer_notes/descriptors.rst @@ -3,11 +3,11 @@ Descriptors =========== -Descriptors are a way to intercept attribute access on an object, and they are used extensively by LabThings to add functionality to `.Thing` instances, while continuing to look like normal Python objects. +Descriptors are a way to intercept attribute access on an object, and they are used extensively by LabThings to add functionality to `~lt.Thing` instances, while continuing to look like normal Python objects. By default, attributes of an object are just variables - so an object called ``foo`` might have an attribute called ``bar``, and you may read its value with ``foo.bar``, write its value with ``foo.bar = "baz"``, and delete the attribute with ``del foo.bar``. If ``foo`` is a descriptor, Python will call the ``__get__`` method of that descriptor when it's read and the ``__set__`` method when it's written to. You have quite probably used a descriptor already, because the built-in `~builtins.property` creates a descriptor object: that's what runs your getter method when the property is accessed. The descriptor protocol is described with plenty of examples in the `Descriptor Guide`_ in the Python documentation. -In LabThings-FastAPI, descriptors are used to implement :ref:`actions` and :ref:`properties` on `.Thing` subclasses. The intention is that these will function like standard Python methods and properties, but will also be available over HTTP, along with :ref:`gen_docs`. +In LabThings-FastAPI, descriptors are used to implement :ref:`actions` and :ref:`properties` on `~lt.Thing` subclasses. The intention is that these will function like standard Python methods and properties, but will also be available over HTTP, along with :ref:`gen_docs`. .. _field_typing: @@ -22,7 +22,7 @@ Field typing my_property: int = lt.property(default=0) """An integer property.""" -This makes it clear to anyone using ``MyThing`` that ``my_property`` is an integer, and should be picked up by most type checking/autocompletion tools. However, because the annotation is attached to the *class* and not passed to the underlying `.DataProperty` descriptor, we need to use the descriptor protocol to figure it out. +This makes it clear to anyone using ``MyThing`` that ``my_property`` is an integer, and should be picked up by most type checking/autocompletion tools. However, because the annotation is attached to the *class* and not passed to the underlying `~lt.DataProperty` descriptor, we need to use the descriptor protocol to figure it out. Field typing in LabThings is implemented by `.FieldTypedBaseDescriptor` and there are docstrings on all of the relevant "magic" methods explaining what each one does. Below, there is a brief overview of how these fit together. @@ -39,7 +39,7 @@ Descriptor implementation There are a few useful notes that relate to many of the descriptors in LabThings-FastAPI: * Descriptor objects **may have more than one owner**. As a rule, a descriptor object - (e.g. an instance of `.DataProperty`) is assigned to an attribute of one `.Thing` subclass. There may, however, be multiple *instances* of that class, so it is not safe to assume that the descriptor object corresponds to only one `.Thing`. This is why the `.Thing` is passed to the ``__get__`` method: we should ensure that any values being remembered are keyed to the owning `.Thing` and are not simply stored in the descriptor. Usually, this is done using `.WeakKeyDictionary` objects, which allow us to look up values based on the `.Thing`, without interfering with garbage collection. + (e.g. an instance of `~lt.DataProperty`) is assigned to an attribute of one `~lt.Thing` subclass. There may, however, be multiple *instances* of that class, so it is not safe to assume that the descriptor object corresponds to only one `~lt.Thing`. This is why the `~lt.Thing` is passed to the ``__get__`` method: we should ensure that any values being remembered are keyed to the owning `~lt.Thing` and are not simply stored in the descriptor. Usually, this is done using `.WeakKeyDictionary` objects, which allow us to look up values based on the `~lt.Thing`, without interfering with garbage collection. The example below shows how this can go wrong. diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 77f3c611..a7a9b097 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -1,9 +1,9 @@ Examples ======== -For a simple example `.Thing` and instructions on how to serve it, see the :ref:`tutorial`\ . +For a simple example `~lt.Thing` and instructions on how to serve it, see the :ref:`tutorial`\ . -For more complex examples, there is a useful collection of `.Thing` subclasses implemented as part of the `OpenFlexure Microscope`_ in the `things`_ submodule. This includes control of a camera and a translation stage, as well as various software `.Thing`\ s that integrate the two. +For more complex examples, there is a useful collection of `~lt.Thing` subclasses implemented as part of the `OpenFlexure Microscope`_ in the `things`_ submodule. This includes control of a camera and a translation stage, as well as various software `~lt.Thing`\ s that integrate the two. .. _`OpenFlexure Microscope`: https://gitlab.com/openflexure/openflexure-microscope-server/ .. _`things`: https://gitlab.com/openflexure/openflexure-microscope-server/-/tree/v3/src/openflexure_microscope_server/things/ \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 3a13e886..2c6f4663 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -21,15 +21,16 @@ Documentation for LabThings-FastAPI wot_core_concepts.rst removed_features.rst + quick_reference.rst autoapi/index developer_notes/index.rst `labthings-fastapi` is a Python library to simplify the process of making laboratory instruments available via a HTTP. It aims to create an API that is usable from any modern programming language, with API documentation in both :ref:`openapi` and :ref:`gen_td` formats. It is the underlying framework for v3 of the `OpenFlexure Microscope software `_. Key features and design aims are: -* The functionality of a unit of hardware or software is described using `.Thing` subclasses. -* Methods and properties of `.Thing` subclasses may be added to the HTTP API and associated documentation using decorators. +* The functionality of a unit of hardware or software is described using `~lt.Thing` subclasses. +* Methods and properties of `~lt.Thing` subclasses may be added to the HTTP API and associated documentation using decorators. * Datatypes of action input/outputs and properties are defined with Python type hints. -* Actions are decorated methods of a `.Thing` class. There is no need for separate schemas or endpoint definitions. +* Actions are decorated methods of a `~lt.Thing` class. There is no need for separate schemas or endpoint definitions. * Properties are defined either as typed attributes (similar to `pydantic` or `dataclasses`) or with a `property`\ -like decorator. * Lifecycle and concurrency are appropriate for hardware: `Thing` code is always run in a thread, and each `Thing` is instantiated, started up, and shut down only once. * Vocabulary and concepts are aligned with the `W3C Web of Things `_ standard (see :ref:`wot_cc`) diff --git a/docs/source/properties.rst b/docs/source/properties.rst index 105a0fe6..cf44ceb7 100644 --- a/docs/source/properties.rst +++ b/docs/source/properties.rst @@ -7,17 +7,17 @@ Properties Properties are values that can be read from and written to a Thing. They are used to represent the state of the Thing, such as its current temperature, brightness, or status. :ref:`wot_properties` are a key concept in the Web of Things standard. -LabThings implements properties in a very similar way to the built-in Python `~builtins.property`. The key difference is that defining an attribute as a `.property` means that the property will be listed in the :ref:`gen_td` and exposed over HTTP. This is important for two reasons: +LabThings implements properties in a very similar way to the built-in Python `~builtins.property`. The key difference is that defining an attribute as a `~lt.property` means that the property will be listed in the :ref:`gen_td` and exposed over HTTP. This is important for two reasons: -* Only properties declared using `.property` (usually imported as ``lt.property``) can be accessed over HTTP. Regular attributes or properties using `builtins.property` are only available to your `.Thing` internally, except in some special cases. -* Communication between `.Thing`\ s within a LabThings server should be done using a `.DirectThingClient` class. The purpose of `.DirectThingClient` is to provide the same interface as a `.ThingClient` over HTTP, so it will also only expose functionality described in the Thing Description. +* Only properties declared using `~lt.property` (usually imported as ``lt.property``) can be accessed over HTTP. Regular attributes or properties using `builtins.property` are only available to your `~lt.Thing` internally, except in some special cases. +* Communication between `~lt.Thing`\ s within a LabThings server should be done using a `.DirectThingClient` class. The purpose of `.DirectThingClient` is to provide the same interface as a `~lt.ThingClient` over HTTP, so it will also only expose functionality described in the Thing Description. -You can add properties to a `.Thing` by using `.property` (usually imported as ``lt.property``). +You can add properties to a `~lt.Thing` by using `~lt.property` (usually imported as ``lt.property``). Data properties ------------------------- -Data properties behave like variables: they simply store a value that is used by other code on the `.Thing`. They are defined similarly to fields in `dataclasses` or `pydantic` models: +Data properties behave like variables: they simply store a value that is used by other code on the `~lt.Thing`. They are defined similarly to fields in `dataclasses` or `pydantic` models: .. code-block:: python @@ -52,7 +52,7 @@ If your property's default value is a mutable datatype, like a list or dictionar class MyThing(lt.Thing): my_list: list[int] = lt.property(default_factory=list) -The example above will have its default value set to the empty list, as that's what is returned when ``list()`` is called. It's often convenient to use a "lambda function" as a default factory, for example `lambda: [1,2,3]` is a function that returns the list `[1,2,3]`\ . This is better than specifying a default value, because it returns a fresh copy of the object every time - using a list as a default value can lead to multiple `.Thing` instances changing in sync unexpectedly, which gets very confusing. +The example above will have its default value set to the empty list, as that's what is returned when ``list()`` is called. It's often convenient to use a "lambda function" as a default factory, for example `lambda: [1,2,3]` is a function that returns the list `[1,2,3]`\ . This is better than specifying a default value, because it returns a fresh copy of the object every time - using a list as a default value can lead to multiple `~lt.Thing` instances changing in sync unexpectedly, which gets very confusing. Data properties may be *observed*, which means notifications will be sent when the property is written to (see below). @@ -200,7 +200,7 @@ We can modify the previous example to show how to add constraints to both data a In the example above, the ``temperature`` property is a data property with constraints that limit its value to between -40.0 and 125.0 degrees Celsius. The ``humidity`` property is a functional property with constraints that limit its value to between 0.0 and 100.0 percent. The ``sensor_name`` property is a data property with a regex pattern constraint that only allows alphanumeric characters and underscores. -Note that the constraints for functional properties are set by assigning a dictionary to the property's ``constraints`` attribute. This dictionary should contain the same keys and values as the arguments to `pydantic.Field` definitions. The `.property` decorator does not currently accept arguments, so constraints may only be set this way for functional properties and settings. +Note that the constraints for functional properties are set by assigning a dictionary to the property's ``constraints`` attribute. This dictionary should contain the same keys and values as the arguments to `pydantic.Field` definitions. The `~lt.property` decorator does not currently accept arguments, so constraints may only be set this way for functional properties and settings. .. note:: @@ -210,7 +210,7 @@ Note that the constraints for functional properties are set by assigning a dicti Property metadata ----------------- -Properties in LabThings are intended to work very much like native Python properties. This means that getting and setting the attributes of a `.Thing` get and set the value of the property. Other operations, like reading the default value or resetting to default, need a different interface. For this, we use `.Thing.properties` which is a mapping of names to `.PropertyInfo` objects. These expose the extra functionality of properties in a convenient way. For example, I can reset a property by calling ``self.properties["myprop"].reset()`` or get its default by reading ``self.properties["myprop"].default``\ . See the `.PropertyInfo` API documentation for a full list of available properties and methods. +Properties in LabThings are intended to work very much like native Python properties. This means that getting and setting the attributes of a `~lt.Thing` get and set the value of the property. Other operations, like reading the default value or resetting to default, need a different interface. For this, we use `~lt.Thing.properties` which is a mapping of names to `.PropertyInfo` objects. These expose the extra functionality of properties in a convenient way. For example, I can reset a property by calling ``self.properties["myprop"].reset()`` or get its default by reading ``self.properties["myprop"].default``\ . See the `.PropertyInfo` API documentation for a full list of available properties and methods. HTTP interface -------------- @@ -229,11 +229,11 @@ Observable properties Properties can be made observable, which means that clients can subscribe to changes in the property's value. This is useful for properties that change frequently, such as sensor readings or instrument settings. In order for a property to be observable, LabThings must know whenever it changes. Currently, this means only data properties can be observed, as functional properties do not have a simple value that can be tracked. -Properties are currently only observable via websockets: in the future, it may be possible to observe them from other `.Thing` instances or from other parts of the code. +Properties are currently only observable via websockets: in the future, it may be possible to observe them from other `~lt.Thing` instances or from other parts of the code. .. _settings: Settings ------------ -Settings are properties with an additional feature: they are saved to disk. This means that settings will be automatically restored after the server is restarted. The function `.setting` can be used to declare a `.DataSetting` or decorate a function to make a `.FunctionalSetting` in the same way that `.property` can. It is usually imported as ``lt.setting``\ . +Settings are properties with an additional feature: they are saved to disk. This means that settings will be automatically restored after the server is restarted. The function `~lt.setting` can be used to declare a `~lt.DataSetting` or decorate a function to make a `~lt.FunctionalSetting` in the same way that `~lt.property` can. It is usually imported as ``lt.setting``\ . diff --git a/docs/source/quick_reference.rst b/docs/source/quick_reference.rst new file mode 100644 index 00000000..14221423 --- /dev/null +++ b/docs/source/quick_reference.rst @@ -0,0 +1,455 @@ +.. _quickref: + +Quick Reference API Documentation +================================= + +This page summarises the parts of the LabThings API that should be most frequently used by people writing `lt.Thing` subclasses. It doesn't list options exhaustively: the full :doc:`API documentation ` does that if extra detail is needed. + +.. py:module:: lt + +.. py:class:: Thing(thing_server_interface: ThingServerInterface) + + The basic unit of functionality in LabThings is the `Thing`. Each piece of hardware (or software) controlled by LabThings is represented by an instance of a `Thing` subclass, and so adding a new instrument or software unit generally involves subclassing. Each `Thing` on a server has a name, a URL, and a Thing Description describing its capabilities. + + It is likely that `Thing` subclasses will override `__init__`, `__enter__`, and `__exit__`. See the documentation on those methods for important subclassing notes. + + The capabilities of a `Thing` are described using attributes. Actions are methods decorated with `action`, and properties are declared using `property` or `setting`. `Thing`\ s may communicate with each other using `thing_slot`\ s. + + `Thing`\ s should only be created by a `ThingServer`\ . To create a `Thing` without a server, see `.testing.create_thing_without_server` for a test harness that supplies a dummy `ThingServerInterface`. + + This page offers only the most commonly-used methods: full documentation is available at `labthings_fastapi.thing.Thing`\ . + + .. py:method:: __init__(thing_server_interface: ThingServerInterface) + + `__init__` may be overridden in order to accept arguments when your object is created. If you override `__init__` you **must** call ``super().__init__(thing_server_interface)`` to ensure the `Thing` is properly connected to a server. + + `__init__` *should not* acquire any resources (like communications ports), as they may not be closed cleanly. Please acquire any necessary resources in `__enter__` instead. + + :param thing_server_interface: The interface to the server that + is hosting this Thing. It will be supplied when the `~lt.Thing` is + instantiated by the `~lt.ThingServer` or by + `.testing.create_thing_without_server` which generates a mock interface. + + + .. autoattribute:: labthings_fastapi.thing.Thing.title + :no-index: + + .. py:attribute:: _thing_server_interface + :type: ThingServerInterface + + Provide access to features of the server that this `~lt.Thing` is attached to. + + .. autoproperty:: labthings_fastapi.thing.Thing.name + :no-index: + + .. py:property:: logger + :type: logging.Logger + + A logger, named after this Thing. Use this logger if you wish to log messages from `action` or `property` code. + + .. py:property:: properties + :type: labthings_fastapi.properties.PropertyCollection + + A mapping of names to `PropertyInfo` objects. This allows easy access to metadata, for example: + + .. code-block:: python + + self.properties["myprop"].default + + .. py:property:: settings + :type: labthings_fastapi.properties.SettingCollection + + A mapping of names to `.SettingInfo` objects, similar to `properties` but providing setting-specific features. + + .. py:property:: actions + :type: labthings_fastapi.actions.ActionCollection + + A mapping of names to `.ActionInfo` objects that allows + convenient access to metadata of each action. + + + .. py:property:: thing_state + :type: collections.abc.Mapping + + + This should return a dictionary of metadata, which will be returned to any code requesting it through `ThingServerInterface.get_thing_states`\ . + + + .. automethod:: labthings_fastapi.thing.Thing.get_current_invocation_logs + :no-index: + +.. py:function:: property(getter: Callable[[Owner], Value]) -> FunctionalProperty[Owner, Value] + property(*, default: Value, readonly: bool = False, **constraints: Any) -> Value + property(*, default_factory: Callable[[], Value], readonly: bool = False, **constraints: Any) -> Value + + This function may be used to define :ref:`properties` either by decorating a function, or marking an attribute. Full documentation is available at `labthings_fastapi.properties.property` and a more in-depth discussion is available at :ref:`properties`\ . This page focuses on the most frequently used examples. + + To mark a class attribute with `property` you should define the attribute as shown below. Note that the type hint is required for LabThings to work properly. + + .. code-block:: python + + class MyThing(lt.Thing): + intprop: int = lt.property(default=0) + """A simple read-write property""" + + readonly: int = lt.property(default=42, readonly=True) + """This property may not be written to over HTTP""" + + listprop: list[int] = lt.property(default_factory=lambda: [1,2,3]) + """Mutable default values should be wrapped in a "factory function"."""" + + positive: int = lt.property(default=1, gt=0) + """Constraints may be used in the same way as for `pydantic.Field`""" + + All the examples above are "data properties". `property` can also define "functional properties" when used as a decorator: + + .. code-block:: python + + class MyThing(lt.Thing): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._number = 0 + + @lt.property + def the_answer(self) -> int: + """A read-only property.""" + return 42 + + @lt.property + def number(self) -> int: + """A property that's got extra attributes set.""" + + @number.setter + def _set_number(self, value: int) -> None: + self._number = value + + number.readonly = True # This prevents it being written over HTTP + number.constraints = {"ge": 0} # This adds constraints to the schema + number.default = 0 # This adds a default value to the documentation + + For a full listing of attributes that may be modified, see `DataProperty`\ . + + +.. py:function:: setting(getter: Callable[[Owner], Value]) -> FunctionalSetting[Owner, Value] + setting(*, default: Value, readonly: bool = False, **constraints: Any) -> Value + setting(*, default_factory: Callable[[], Value], readonly: bool = False, **constraints: Any) -> Value + + A setting is a property that is saved to disk. It is defined in the same way as `property` but will be synchronised with the `Thing`\ 's settings file. Full documentation is available at `labthings_fastapi.properties.setting` + + +.. py:decorator:: action + action(**kwargs: Any) + + Mark a method of a `~lt.Thing` as a LabThings Action. + + Methods decorated with `action` will be available to call + over HTTP as actions. See :ref:`actions` for an introduction to the concept + of actions. + + This decorator may be used with or without arguments. + + :param \**kwargs: Keyword arguments are passed to the constructor + of `.ActionDescriptor`. + + + + + +.. py:function:: thing_slot(default: str | collections.abc.Iterable[str] | None | types.EllipsisType = ...) -> Any + + Declare a connection to another `~lt.Thing` in the same server. + + ``thing_slot`` marks a class attribute as a connection to another + `Thing` on the same server. This will be automatically supplied when the + server is started, based on the type hint and default value. + + In keeping with `property` and `setting`, the type of the attribute should + be the type of the connected `~lt.Thing`\ . A `~collections.abc.Mapping` should be used + if the slot supports multiple `Thing`\ s. For example: + + .. code-block:: python + + class ThingA(lt.Thing): ... + + + class ThingB(lt.Thing): + "A class that relies on ThingA." + + thing_a: ThingA = lt.thing_slot() + multiple_things_a: Mapping[str, ThingA] = lt.thing_slot() + + For more details, see the full API docs for `~labthings_fastapi.thing_slots.thing_slot`\ . + + :param default: The name(s) of the Thing(s) that will be connected by default. + If the default is omitted or set to ``...`` the server will attempt to find + a matching `~lt.Thing` instance (or instances). A default value of `None` is + allowed if the connection is type hinted as optional. + :return: A `.ThingSlot` descriptor. + + +.. py:decorator:: endpoint(method: HTTPMethod, path: Optional[str] = None, **kwargs: Any) + + Mark a function as a FastAPI endpoint without making it an action. + + This decorator will cause a method of a `~lt.Thing` to be directly added to + the HTTP API, bypassing the machinery underlying Action and Property + affordances. Such endpoints will not be documented in the :ref:`wot_td` but + may be used as the target of links. For example, this could allow a file + to be downloaded from the `~lt.Thing` at a known URL, or serve a video stream + that wouldn't be supported as a `.Blob`\ . + + The majority of `~lt.Thing` implementations won't need this decorator, but + it is here to enable flexibility when it's needed. + + This decorator always takes arguments; in particular, ``method`` is + required. It should be used as: + + .. code-block:: python + + class DownloadThing(Thing): + @endpoint("get") + def plain_text_response(self) -> str: + return "example string" + + This decorator is intended to work very similarly to the `fastapi` decorators + ``@app.get``, ``@app.post``, etc., with two changes: + + 1. The path is relative to the host `~lt.Thing` and will default to the name + of the method. + 2. The method will be called with the host `~lt.Thing` as its first argument, + i.e. it will be bound to the class as usua. + + :param method: The HTTP verb this endpoint responds to. + :param path: The path, relative to the host `~lt.Thing` base URL. + :param \**kwargs: Additional keyword arguments are passed to the + `fastapi.FastAPI.get` decorator if ``method`` is ``get``, or to + the equivalent decorator for other HTTP verbs. + + :return: When used as intended, the result is an `.EndpointDescriptor`. + + +.. py:class:: ThingServer(things: config_model.ThingsConfig, settings_folder: Optional[str] = None, application_config: Optional[collections.abc.Mapping[str, Any]] = None, debug: bool = False) + + The `ThingServer` sets up a `fastapi.FastAPI` application and uses it + to expose the capabilities of `Thing` instances over HTTP. + + Full documentation of how the class works is available at `labthings_fastpi.server.ThingServer`\ . Most of the attributes of `ThingServer` should not be accessed directly by `Thing` subclasses - instead they should use the `ThingServerInterface` for a cleaner way to access the server. + + :param things: A mapping of Thing names to `~lt.Thing` subclasses, or + `ThingConfig` objects specifying the subclass, its initialisation + arguments, and any connections to other `~lt.Thing`\ s. + :param settings_folder: the location on disk where `~lt.Thing` + settings will be saved. + :param application_config: A mapping containing custom configuration for the + application. This is not processed by LabThings. Each `~lt.Thing` can access + application. This is not processed by LabThings. Each `~lt.Thing` can access + this via the Thing-Server interface. + :param debug: If ``True``, set the log level for `~lt.Thing` instances to + DEBUG. + + + .. automethod:: labthings_fastapi.server.ThingServer.from_config + :no-index: + + + +.. py:class:: ThingServerInterface(server: ThingServer, name: str) + + An interface for Things to interact with their server. This is available as `Thing._thing_server_interface` and should not normally be created except by the `ThingServer`\ . + + .. automethod:: labthings_fastapi.thing_server_interface.ThingServerInterface.start_async_task_soon + :no-index: + + .. automethod:: labthings_fastapi.thing_server_interface.ThingServerInterface.call_async_task + :no-index: + + .. autoproperty:: labthings_fastapi.thing_server_interface.ThingServerInterface.settings_folder + :no-index: + + .. autoproperty:: labthings_fastapi.thing_server_interface.ThingServerInterface.settings_file_path + :no-index: + + .. autoproperty:: labthings_fastapi.thing_server_interface.ThingServerInterface.name + :no-index: + + .. autoproperty:: labthings_fastapi.thing_server_interface.ThingServerInterface.application_config + :no-index: + + .. automethod:: labthings_fastapi.thing_server_interface.ThingServerInterface.get_thing_states + :no-index: + + +.. py:class:: ThingConfig(/, **data: Any) + + Bases: :py:obj:`pydantic.BaseModel` + + + The information needed to add a `~lt.Thing` to a `~lt.ThingServer`\ . This is an alias of `labthings_fastapi.server.config_model.ThingConfig` + + .. autoattribute:: labthings_fastapi.server.config_model.ThingConfig.cls + :no-index: + + .. autoattribute:: labthings_fastapi.server.config_model.ThingConfig.args + :no-index: + + .. autoattribute:: labthings_fastapi.server.config_model.ThingConfig.kwargs + :no-index: + + .. autoattribute:: labthings_fastapi.server.config_model.ThingConfig.thing_slots + :no-index: + + +.. py:class:: ThingServerConfig(/, **data: Any) + + Bases: :py:obj:`pydantic.BaseModel` + + + The configuration parameters for a `~lt.ThingServer`\ . + + + .. autoattribute:: labthings_fastapi.server.config_model.ThingServerConfig.things + :no-index: + + .. autoattribute:: labthings_fastapi.server.config_model.ThingServerConfig.settings_folder + :no-index: + + .. autoattribute:: labthings_fastapi.server.config_model.ThingServerConfig.application_config + :no-index: + + +.. py:class:: ThingClient + + A client for a LabThings-FastAPI Thing, alias of `labthings_fastapi.client.ThingClient` + + .. note:: + ThingClient must be subclassed to add actions/properties, + so this class will be minimally useful on its own. + + The best way to get a client for a particular Thing is + currently `ThingClient.from_url`, which dynamically + creates a subclass with the right attributes. + + .. automethod:: labthings_fastapi.client.ThingClient.from_url + :no-index: + + +.. py:function:: cancellable_sleep(interval: float) -> None + + Sleep for a specified time, allowing cancellation. + + This function should be called from action functions instead of + `time.sleep` to allow them to be cancelled. Usually, this + function is equivalent to `time.sleep` (it waits the specified + number of seconds). If the action is cancelled during the sleep, + it will raise an `.InvocationCancelledError` to signal that the + action should finish. + + .. warning:: + + This function uses `.Event.wait` internally, which suffers + from timing errors on some platforms: it may have error of + around 10-20ms. If that's a problem, consider using + `time.sleep` instead. ``lt.raise_if_cancelled()`` may then + be used to allow cancellation. + + If this function is called from outside of an action thread, it + will revert to `time.sleep`\ . + + :param interval: The length of time to wait for, in seconds. + + +.. py:function:: raise_if_cancelled() -> None + + Raise an exception if the current invocation has been cancelled. + + This function checks for cancellation events and, if the current + action invocation has been cancelled, it will raise an + `.InvocationCancelledError` to signal the thread to terminate. + It is equivalent to `~lt.cancellable_sleep` but without waiting any + time. + + If called outside of an invocation context, this function does + nothing, and will not raise an error. + + +.. py:class:: ThreadWithInvocationID(target: Callable, args: collections.abc.Sequence[Any] | None = None, kwargs: collections.abc.Mapping[str, Any] | None = None, *super_args: Any, **super_kwargs: Any) + + Bases: :py:obj:`threading.Thread` + + + A thread that sets a new invocation ID. + + This is a subclass of `threading.Thread` and works very much the + same way. It implements its functionality by overriding the ``run`` + method, so this should not be overridden again - you should instead + specify the code to run using the ``target`` argument. + + This function enables an action to be run in a thread, which gets its + own invocation ID and cancel hook. This means logs will not be interleaved + with the calling action, and the thread may be cancelled just like an + action started over HTTP, by calling its ``cancel`` method. + + The thread also remembers the return value of the target function + in the property ``result`` and stores any exception raised in the + ``exception`` property. + + A final LabThings-specific feature is cancellation propagation. If + the thread is started from an action that may be cancelled, it may + be joined with ``join_and_propagate_cancel``\ . This is intended + to be equivalent to calling ``join`` but with the added feature that, + if the parent thread is cancelled while waiting for the child thread + to join, the child thread will also be cancelled. + + :param target: the function to call in the thread. + :param args: positional arguments to ``target``\ . + :param kwargs: keyword arguments to ``target``\ . + :param \*super_args: arguments passed to `threading.Thread`\ . + :param \*\*super_kwargs: keyword arguments passed to `threading.Thread`\ . + + + .. py:property:: result + :type: Any + + + The return value of the target function. + + + + .. py:property:: exception + :type: BaseException | None + + + The exception raised by the target function, or None. + + + + .. py:method:: cancel() -> None + + Set the cancel event to tell the code to terminate. + + + + .. py:method:: join_and_propagate_cancel(poll_interval: float = 0.2) -> None + + Wait for the thread to finish, and propagate cancellation. + + This function wraps `threading.Thread.join` but periodically checks if + the calling thread has been cancelled. If it has, it will cancel the + thread, before attempting to ``join`` it again. + + Note that, if the invocation that calls this function is cancelled + while the function is running, the exception will propagate, i.e. + you should handle `.InvocationCancelledError` unless you wish + your invocation to terminate if it is cancelled. + + :param poll_interval: How often to check for cancellation of the + calling thread, in seconds. + :raises InvocationCancelledError: if this invocation is cancelled + while waiting for the thread to join. + + + + .. py:method:: run() -> None + + Run the target function, with invocation ID set in the context variable. + diff --git a/docs/source/removed_features.rst b/docs/source/removed_features.rst index e6af2574..40900ee0 100644 --- a/docs/source/removed_features.rst +++ b/docs/source/removed_features.rst @@ -5,4 +5,4 @@ Removed Features Dependencies ------------ -The use of dependencies for inter-`.Thing` communication was removed in version 0.1. See :ref:`thing_slots` and `.ThingServerInterface` for a more intuitive way to access that functionality. \ No newline at end of file +The use of dependencies for inter-`~lt.Thing` communication was removed in version 0.1. See :ref:`thing_slots` and `~lt.ThingServerInterface` for a more intuitive way to access that functionality. \ No newline at end of file diff --git a/docs/source/structure.rst b/docs/source/structure.rst index 47118c1b..08f43fbb 100644 --- a/docs/source/structure.rst +++ b/docs/source/structure.rst @@ -10,38 +10,38 @@ Server ------ LabThings is a server-based framework. -The `.ThingServer` creates and manages the `.Thing` instances that represent individual hardware or software units. The functionality of those `.Thing`\ s is accessed via HTTP requests, which can be made from a web browser, the command line, or any programming language with an HTTP library. +The `~lt.ThingServer` creates and manages the `~lt.Thing` instances that represent individual hardware or software units. The functionality of those `~lt.Thing`\ s is accessed via HTTP requests, which can be made from a web browser, the command line, or any programming language with an HTTP library. -LabThings-FastAPI is built on top of `fastapi`\ , which is a fast, modern HTTP framework. LabThings provides functionality to manage `.Thing`\ s and their actions, including: +LabThings-FastAPI is built on top of `fastapi`\ , which is a fast, modern HTTP framework. LabThings provides functionality to manage `~lt.Thing`\ s and their actions, including: -* Initialising, starting up, and shutting down the `.Thing` instances, so that hardware is correctly started up and shut down. +* Initialising, starting up, and shutting down the `~lt.Thing` instances, so that hardware is correctly started up and shut down. * Managing actions, including making logs and output values available over HTTP. * Managing `.Blob` input and output (i.e. binary objects that are best not serialised to JSON). * Generating a :ref:`gen_td` in addition to the :ref:`openapi` documentation produced by `fastapi`\ . -* Making connections between `.Thing` instances as required. +* Making connections between `~lt.Thing` instances as required. .. _things: Things ----------- -Each unit of hardware (or software) that should be exposed by the server is implemented as a subclass of `.Thing`\ . A `.Thing` subclass represents a particular type of instrument (whether hardware or software), and its functionality is described using actions and properties, described below. `.Thing`\ s don't have to correspond to separate pieces of hardware: it's possible (and indeed recommended) to use `.Thing` subclasses for software components, plug-ins, swappable modules, or anything else that needs to add functionality to the server. `.Thing`\ s may access each other's attributes, so you can write a `.Thing` that implements a particular measurement protocol or task, using hardware that's accessed through other `.Thing` instances on the server. Each `.Thing` is documented by a :ref:`gen_td` which outlines its features in a higher-level way than :ref:`openapi`\ . +Each unit of hardware (or software) that should be exposed by the server is implemented as a subclass of `~lt.Thing`\ . A `~lt.Thing` subclass represents a particular type of instrument (whether hardware or software), and its functionality is described using actions and properties, described below. `~lt.Thing`\ s don't have to correspond to separate pieces of hardware: it's possible (and indeed recommended) to use `~lt.Thing` subclasses for software components, plug-ins, swappable modules, or anything else that needs to add functionality to the server. `~lt.Thing`\ s may access each other's attributes, so you can write a `~lt.Thing` that implements a particular measurement protocol or task, using hardware that's accessed through other `~lt.Thing` instances on the server. Each `~lt.Thing` is documented by a :ref:`gen_td` which outlines its features in a higher-level way than :ref:`openapi`\ . -The attributes of a `.Thing` are made available over HTTP by decorating or marking them with the following functions: +The attributes of a `~lt.Thing` are made available over HTTP by decorating or marking them with the following functions: -* `.property` may be used as a decorator analogous to Python's built-in ``@property``\ . It can also be used to mark class attributes as variables that should be available over HTTP. -* `.setting` works similarly to `.property` but it is persisted to disk when the server stops, so the value is remembered. -* `.action` is a decorator that makes methods available over HTTP. -* `.thing_slot` tells LabThings to supply an instance of another `.Thing` at runtime, so your `.Thing` can make use of it. +* `~lt.property` may be used as a decorator analogous to Python's built-in ``@property``\ . It can also be used to mark class attributes as variables that should be available over HTTP. +* `~lt.setting` works similarly to `~lt.property` but it is persisted to disk when the server stops, so the value is remembered. +* `lt.action` is a decorator that makes methods available over HTTP. +* `~lt.thing_slot` tells LabThings to supply an instance of another `~lt.Thing` at runtime, so your `~lt.Thing` can make use of it. Client Code ----------- Client code can be written in any language that supports an HTTP request. However, LabThings FastAPI provides additional functionality that makes writing client code in Python easier. See :ref:`using_things` for more detail. -`.ThingClient` is a class that wraps up the required HTTP requests into a simpler interface. It can retrieve the :ref:`gen_td` over HTTP and use it to generate a new object with methods matching each `.action` and properties matching each `.property`. +`~lt.ThingClient` is a class that wraps up the required HTTP requests into a simpler interface. It can retrieve the :ref:`gen_td` over HTTP and use it to generate a new object with methods matching each `lt.action` and properties matching each `~lt.property`. -While the current dynamic implementation of `.ThingClient` can be inspected with functions like `help` at runtime, it does not work well with static tools like `mypy` or `pyright`\ . In the future, LabThings should be able to generate static client code that works better with autocompletion and type checking. +While the current dynamic implementation of `~lt.ThingClient` can be inspected with functions like `help` at runtime, it does not work well with static tools like `mypy` or `pyright`\ . In the future, LabThings should be able to generate static client code that works better with autocompletion and type checking. Data types ----------- diff --git a/docs/source/thing_slots.rst b/docs/source/thing_slots.rst index 8e1aaa94..32034d2f 100644 --- a/docs/source/thing_slots.rst +++ b/docs/source/thing_slots.rst @@ -5,17 +5,17 @@ Thing Slots It is often desirable for two Things in the same server to be able to communicate. In order to do this in a nicely typed way that is easy to test and inspect, -LabThings-FastAPI provides `.thing_slot`\ . This allows a `.Thing` -to declare that it depends on another `.Thing` being present, and provides a way for +LabThings-FastAPI provides `~lt.thing_slot`\ . This allows a `~lt.Thing` +to declare that it depends on another `~lt.Thing` being present, and provides a way for the server to automatically connect the two when the server is set up. -Thing connections are set up **after** all the `.Thing` instances are initialised. +Thing connections are set up **after** all the `~lt.Thing` instances are initialised. This means you should not rely on them during initialisation: if you attempt to access a connection before it is available, it will raise an exception. The advantage of making connections after initialisation is that we don't need to -worry about the order in which `.Thing`\ s are created. +worry about the order in which `~lt.Thing`\ s are created. -The following example shows the use of a `.thing_slot`: +The following example shows the use of a `~lt.thing_slot`: .. code-block:: python @@ -46,27 +46,27 @@ The following example shows the use of a `.thing_slot`: server = lt.ThingServer(things) -In this example, ``ThingB.thing_a`` is the simplest form of `.thing_slot`: it -is type hinted as a `.Thing` subclass, and by default the server will look for the +In this example, ``ThingB.thing_a`` is the simplest form of `~lt.thing_slot`: it +is type hinted as a `~lt.Thing` subclass, and by default the server will look for the instance of that class and supply it when the server starts. If there is no -matching `.Thing` or if more than one instance is present, the server will fail +matching `~lt.Thing` or if more than one instance is present, the server will fail to start with a `.ThingSlotError`\ . It is also possible to use an optional type hint (``ThingA | None``), which -means there will be no error if a matching `.Thing` instance is not found, and -the slot will evaluate to `None`\ . Finally, a `.thing_slot` may be +means there will be no error if a matching `~lt.Thing` instance is not found, and +the slot will evaluate to `None`\ . Finally, a `~lt.thing_slot` may be type hinted as ``Mapping[str, ThingA]`` which permits zero or more instances to be connected. The mapping keys are the names of the things. Configuring Thing Slots ----------------------- -A `.thing_slot` may be given a default value. If this is a string, the server -will look up the `.Thing` by name. If the default is `None` the slot will +A `~lt.thing_slot` may be given a default value. If this is a string, the server +will look up the `~lt.Thing` by name. If the default is `None` the slot will evaluate to `None` unless explicitly configured. Slots may also be specified in the server's configuration: -`.ThingConfig` takes an argument that allows connections to be made +`~lt.ThingConfig` takes an argument that allows connections to be made by name (or set to `None`). The same field is present in a config file. Each entry in the ``things`` list may have a ``thing_slots`` property that sets up the connections. To repeat the example above with a configuration @@ -84,5 +84,5 @@ file: } } -More detail can be found in the description of `.thing_slot` or the +More detail can be found in the description of `~lt.thing_slot` or the :mod:`.thing_slots` module documentation. diff --git a/docs/source/tutorial/running_labthings.rst b/docs/source/tutorial/running_labthings.rst index a3f8baad..fc98efa1 100644 --- a/docs/source/tutorial/running_labthings.rst +++ b/docs/source/tutorial/running_labthings.rst @@ -43,4 +43,4 @@ You can then start the server using the command: Starting the server from Python ------------------------------- -It is also possible to start a LabThings server from within a Python script. This is the only way to serve `.Thing` classes that are not importable (e.g. if you're running example code that's not structured as a proper package). Many of the examples will start a server in this way - for example :ref:`tutorial_thing`\ . +It is also possible to start a LabThings server from within a Python script. This is the only way to serve `~lt.Thing` classes that are not importable (e.g. if you're running example code that's not structured as a proper package). Many of the examples will start a server in this way - for example :ref:`tutorial_thing`\ . diff --git a/docs/source/tutorial/writing_a_thing.rst b/docs/source/tutorial/writing_a_thing.rst index cda1e5f8..1df0595a 100644 --- a/docs/source/tutorial/writing_a_thing.rst +++ b/docs/source/tutorial/writing_a_thing.rst @@ -3,11 +3,11 @@ Writing a Thing ========================= -In this section, we will write a simple example `.Thing` that provides some functionality on the server. +In this section, we will write a simple example `~lt.Thing` that provides some functionality on the server. .. note:: - Usually, you will write your own `.Thing` in a separate Python module and run it using a configuration file as described in :ref:`tutorial_running`. However, for this tutorial, we will write the `.Thing` in a single file, and use a ``__name__ == "__main__"`` block to run it directly. This is not recommended for production code, but it is convenient for a tutorial. + Usually, you will write your own `~lt.Thing` in a separate Python module and run it using a configuration file as described in :ref:`tutorial_running`. However, for this tutorial, we will write the `~lt.Thing` in a single file, and use a ``__name__ == "__main__"`` block to run it directly. This is not recommended for production code, but it is convenient for a tutorial. Our first Thing will pretend to be a light: we can set its brightness and turn it on and off. A first, most basic implementation might look like: diff --git a/docs/source/using_things.rst b/docs/source/using_things.rst index e60010ac..69d69c89 100644 --- a/docs/source/using_things.rst +++ b/docs/source/using_things.rst @@ -3,11 +3,11 @@ Using Things ============ -The interface to a `Thing` is defined by its actions, properties and events [#events]_. These can all be accessed remotely via HTTP from any language, but a more convenient interface in Python is a `.ThingClient` subclass. This provides a simple, pythonic interface to the `.Thing`, allowing you to call actions and access properties as if they were methods and attributes of a Python object. +The interface to a `Thing` is defined by its actions, properties and events [#events]_. These can all be accessed remotely via HTTP from any language, but a more convenient interface in Python is a `~lt.ThingClient` subclass. This provides a simple, pythonic interface to the `~lt.Thing`, allowing you to call actions and access properties as if they were methods and attributes of a Python object. -`.ThingClient` subclasses can be generated dynamically from a URL using :meth:`.ThingClient.from_url`. This creates an object with the right methods, properties and docstrings, though type hints are often missing. The client can be "introspected" to explore its methods and properties using tools that work at run-time (e.g. autocompletion in a Jupyter notebook), but "static" analysis tools will not yet work. +`~lt.ThingClient` subclasses can be generated dynamically from a URL using :meth:`lt.ThingClient.from_url`. This creates an object with the right methods, properties and docstrings, though type hints are often missing. The client can be "introspected" to explore its methods and properties using tools that work at run-time (e.g. autocompletion in a Jupyter notebook), but "static" analysis tools will not yet work. -Both the input and return types of the functions of a `.ThingClient` are intended to match those of the `.Thing` it's connected to, however there are currently some differences. In particular, `pydantic` models are usually converted to dictionaries. This is something that is on the long-term roadmap to improve, possibly with :ref:`code generation `\ . +Both the input and return types of the functions of a `~lt.ThingClient` are intended to match those of the `~lt.Thing` it's connected to, however there are currently some differences. In particular, `pydantic` models are usually converted to dictionaries. This is something that is on the long-term roadmap to improve, possibly with :ref:`code generation `\ . .. [#events] Events are not yet implemented. @@ -16,7 +16,7 @@ Both the input and return types of the functions of a `.ThingClient` are intende Using Things from other Things ------------------------------ -Code within a Thing may access other Things on the same server using :ref:`thing_slots`. These are attributes of the `.Thing` that will be supplied by the server when it is set up. When you access a `.thing_slot` it will return the other `.Thing` instance, and it may be used like any other Python object. `.thing_slot`\ s may be optional, or may be configured to return multiple `.Thing` instances: see the :ref:`thing_slots` documentation for more details. +Code within a Thing may access other Things on the same server using :ref:`thing_slots`. These are attributes of the `~lt.Thing` that will be supplied by the server when it is set up. When you access a `~lt.thing_slot` it will return the other `~lt.Thing` instance, and it may be used like any other Python object. `~lt.thing_slot`\ s may be optional, or may be configured to return multiple `~lt.Thing` instances: see the :ref:`thing_slots` documentation for more details. Using Things from other languages ---------------------------------- @@ -28,7 +28,7 @@ _`render the interactive documentation`: https://fastapi.tiangolo.com/#interacti Dynamic class generation ------------------------- -The object returned by `.ThingClient.from_url` is an instance of a dynamically-created subclass of `.ThingClient`. Dynamically creating the class is needed because we don't know what the methods and properties should be until we have downloaded the Thing Description. However, this means most code autocompletion tools, type checkers, and linters will not work well with these classes. In the future, LabThings-FastAPI will generate custom client subclasses that can be shared in client modules, which should fix these problems (see below). +The object returned by `lt.ThingClient.from_url` is an instance of a dynamically-created subclass of `~lt.ThingClient`. Dynamically creating the class is needed because we don't know what the methods and properties should be until we have downloaded the Thing Description. However, this means most code autocompletion tools, type checkers, and linters will not work well with these classes. In the future, LabThings-FastAPI will generate custom client subclasses that can be shared in client modules, which should fix these problems (see below). .. _client_codegen: @@ -37,7 +37,7 @@ Planned future development: static code generation In the future, `labthings_fastapi` will generate custom client subclasses. These will have the methods and properties defined in a Python module, including type annotations. This will allow static analysis (e.g. with MyPy) and IDE autocompletion to work. Most packages that provide a `Thing` subclass will want to release a client package that is generated automatically in this way. The intention is to make it possible to add custom Python code to this client, for example to handle specialised return types more gracefully or add convenience methods. Generated client code does mean there will be more packages to install on the client in order to use a particular Thing. However, the significant benefits of having a properly defined interface should make this worthwhile. -Return types are also currently not consistent between client and server code: currently, the HTTP implementation of `.ThingClient` deserialises the JSON response and returns it directly, meaning that `pydantic.BaseModel` subclasses become dictionaries. This behaviour should change in the future to be consistent between client and server. Most likely, this will mean Pydantic models are used in both cases. +Return types are also currently not consistent between client and server code: currently, the HTTP implementation of `~lt.ThingClient` deserialises the JSON response and returns it directly, meaning that `pydantic.BaseModel` subclasses become dictionaries. This behaviour should change in the future to be consistent between client and server. Most likely, this will mean Pydantic models are used in both cases. diff --git a/docs/source/wot_core_concepts.rst b/docs/source/wot_core_concepts.rst index 09b70c87..9b5826e2 100644 --- a/docs/source/wot_core_concepts.rst +++ b/docs/source/wot_core_concepts.rst @@ -12,7 +12,7 @@ Thing A Thing represents a piece of hardware or software. It could be a whole instrument (e.g. a microscope), a component within an instrument (e.g. a translation stage or camera), or a piece of software (e.g. code to tile together large area scans). Things in `labthings-fastapi` are Python classes that define Properties, Actions, and Events (see below). A Thing (sometimes called a "Web Thing") is defined by W3C as "an abstraction of a physical or a virtual entity whose metadata and interfaces are described by a WoT Thing description." -`labthings-fastapi` automatically generates a `Thing Description`_ to describe each `.Thing`. Each function offered by the `.Thing` is either a Property, Action, or Event. These are termed "interaction affordances" in WoT_ terminology. +`labthings-fastapi` automatically generates a `Thing Description`_ to describe each `~lt.Thing`. Each function offered by the `~lt.Thing` is either a Property, Action, or Event. These are termed "interaction affordances" in WoT_ terminology. .. _wot_properties: diff --git a/src/labthings_fastapi/__init__.py b/src/labthings_fastapi/__init__.py index 37aec129..e05fb118 100644 --- a/src/labthings_fastapi/__init__.py +++ b/src/labthings_fastapi/__init__.py @@ -13,10 +13,15 @@ import labthings_fastapi as lt + +The most important symbols are described in `lt` with links to the full API +documentation as appropriate. + The example code elsewhere in the documentation generally follows this convention. Symbols in the top-level module mostly exist elsewhere in the package, but should be imported from here as a preference, to ensure code does not break if modules are rearranged. + """ from .thing import Thing diff --git a/src/labthings_fastapi/actions.py b/src/labthings_fastapi/actions.py index b5838a19..fc27af6f 100644 --- a/src/labthings_fastapi/actions.py +++ b/src/labthings_fastapi/actions.py @@ -1,6 +1,6 @@ """Actions module. -:ref:`actions` are represented by methods, decorated with the `.action` +:ref:`actions` are represented by methods, decorated with the `lt.action` decorator. See the :ref:`actions` documentation for a top-level overview of actions in @@ -9,7 +9,7 @@ Developer notes --------------- -Currently much of the code related to Actions is in `.action` and the +Currently much of the code related to Actions is in `lt.action` and the underlying `.ActionDescriptor`. This is likely to be refactored in the near future. """ @@ -84,7 +84,7 @@ class Invocation(Thread): `.Invocation` threads add several bits of functionality compared to the base `threading.Thread`. - * They are instantiated with an `.ActionDescriptor` and a `.Thing` + * They are instantiated with an `.ActionDescriptor` and a `~lt.Thing` rather than a target function (see ``__init__``). * Each invocation is assigned a unique ``ID`` to allow it to be polled over HTTP. @@ -105,7 +105,7 @@ def __init__( :param action: provides the function that we run, as well as metadata and type information. The descriptor is not bound to an object, so we - supply the `.Thing` it's bound to when the function is run. + supply the `~lt.Thing` it's bound to when the function is run. :param thing: is the object on which we are running the ``action``, i.e. it is supplied to the function wrapped by ``action`` as the ``self`` argument. @@ -187,7 +187,7 @@ def action(self) -> ActionDescriptor: @property def thing(self) -> Thing: - """The `.Thing` to which the action is bound, i.e. this is ``self``. + """The `~lt.Thing` to which the action is bound, i.e. this is ``self``. :raises RuntimeError: if the Thing no longer exists. """ @@ -250,7 +250,7 @@ def run(self) -> None: The code to be run is the function wrapped in the `.ActionDescriptor` that is passed in as ``action``. Its arguments are the associated - `.Thing` (the first argument, i.e. ``self``), the ``input`` model + `~lt.Thing` (the first argument, i.e. ``self``), the ``input`` model (split into keyword arguments for each field), and any ``dependencies`` (also as keyword arguments). @@ -360,7 +360,7 @@ def invoke_action( :param action: provides the function that we run, as well as metadata and type information. The descriptor is not bound to an object, so we - supply the `.Thing` it's bound to when the function is run. + supply the `~lt.Thing` it's bound to when the function is run. :param thing: is the object on which we are running the ``action``, i.e. it is supplied to the function wrapped by ``action`` as the ``self`` argument. @@ -407,8 +407,8 @@ def list_invocations( :param action: filters out only the invocations of a particular `.ActionDescriptor`. Note that if there are two Things of the same subclass, filtering by action will return invocations - on either `.Thing`. - :param thing: returns only invocations of actions on a particular `.Thing`. + on either `~lt.Thing`. + :param thing: returns only invocations of actions on a particular `~lt.Thing`. This will often be combined with filtering by ``action`` to give the list of invocations returned by a GET request on an action endpoint. :param request: is used to pass a `fastapi.Request` object to the @@ -654,9 +654,9 @@ class ActionDescriptor( .. note:: Descriptors are instantiated once per class. This means that we cannot assume there is only one action corresponding to this descriptor: there - may be multiple `.Thing` instances with the same descriptor. That is - why the host `.Thing` must be passed to many functions as an argument, - and why observers, for example, must be keyed by the `.Thing` rather + may be multiple `~lt.Thing` instances with the same descriptor. That is + why the host `~lt.Thing` must be passed to many functions as an argument, + and why observers, for example, must be keyed by the `~lt.Thing` rather than kept as a property of ``self``. """ @@ -668,7 +668,7 @@ def __init__( ) -> None: """Create a new action descriptor. - The action descriptor wraps a method of a `.Thing`. It may still be + The action descriptor wraps a method of a `~lt.Thing`. It may still be called from Python in the same way, but it will also be added to the HTTP API and automatic documentation. @@ -732,7 +732,7 @@ def instance_get(self, obj: OwnerT) -> Callable[ActionParams, ActionReturn]: in future. In its present form, this is equivalent to a regular Python method, i.e. all we do is supply the first argument, `self`. - :param obj: the `.Thing` to which we are attached. This will be + :param obj: the `~lt.Thing` to which we are attached. This will be the first argument supplied to the function wrapped by this descriptor. :return: the action function, bound to ``obj``. @@ -742,11 +742,11 @@ def instance_get(self, obj: OwnerT) -> Callable[ActionParams, ActionReturn]: def _observers_set(self, obj: Thing) -> WeakSet: """Return a set used to notify changes. - Note that we need to supply the `.Thing` we are looking at, as in + Note that we need to supply the `~lt.Thing` we are looking at, as in general there may be more than one object of the same type, and descriptor instances are shared between all instances of their class. - :param obj: The `.Thing` on which the action is being observed. + :param obj: The `~lt.Thing` on which the action is being observed. :return: a weak set of callables to notify on changes to the action. This is used by websocket endpoints. @@ -765,7 +765,7 @@ def emit_changed_event(self, obj: Thing, status: str) -> None: portal. Async code must not use the blocking portal as it can deadlock the event loop. - :param obj: The `.Thing` on which the action is being observed. + :param obj: The `~lt.Thing` on which the action is being observed. :param status: The status of the action, to be sent to observers. """ obj._thing_server_interface.start_async_task_soon( @@ -781,10 +781,10 @@ async def emit_changed_event_async(self, obj: Thing, value: Any) -> None: It will send messages to each observer to notify them that something has changed. - :param obj: The `.Thing` on which the action is defined. + :param obj: The `~lt.Thing` on which the action is defined. `.ActionDescriptor` objects are unique to the class, but there may - be more than one `.Thing` attached to a server with the same class. - We use ``obj`` to look up the observers of the current `.Thing`. + be more than one `~lt.Thing` attached to a server with the same class. + We use ``obj`` to look up the observers of the current `~lt.Thing`. :param value: The action status to communicate to the observers. """ action_name = self.name @@ -804,12 +804,12 @@ def add_to_fastapi(self, app: FastAPI, thing: Thing) -> None: application. :param app: The `fastapi.FastAPI` app to add the endpoint to. - :param thing: The `.Thing` to which the action is attached. Bear in - mind that the descriptor may be used by more than one `.Thing`, + :param thing: The `~lt.Thing` to which the action is attached. Bear in + mind that the descriptor may be used by more than one `~lt.Thing`, so this can't be a property of the descriptor. :raises NotConnectedToServerError: if the function is run before the - ``thing`` has a ``path`` property. This is assigned when the `.Thing` + ``thing`` has a ``path`` property. This is assigned when the `~lt.Thing` is added to a server. """ @@ -904,15 +904,15 @@ def action_affordance( This function describes the Action in :ref:`wot_td` format. - :param thing: The `.Thing` to which the action is attached. + :param thing: The `~lt.Thing` to which the action is attached. :param path: The prefix applied to all endpoints associated with the - `.Thing`. This is the URL for the Thing Description. If it is + `~lt.Thing`. This is the URL for the Thing Description. If it is omitted, we use the ``path`` property of the ``thing``. :return: An `.ActionAffordance` describing this action. :raises NotConnectedToServerError: if the function is run before the - ``thing`` has a ``path`` property. This is assigned when the `.Thing` + ``thing`` has a ``path`` property. This is assigned when the `~lt.Thing` is added to a server. """ path = path or thing.path @@ -971,7 +971,7 @@ def action( ActionDescriptor[ActionParams, ActionReturn, OwnerT], ] ): - r"""Mark a method of a `.Thing` as a LabThings Action. + r"""Mark a method of a `~lt.Thing` as a LabThings Action. Methods decorated with :deco:`action` will be available to call over HTTP as actions. See :ref:`actions` for an introduction to the concept diff --git a/src/labthings_fastapi/base_descriptor.py b/src/labthings_fastapi/base_descriptor.py index 43624986..f3dd4f72 100644 --- a/src/labthings_fastapi/base_descriptor.py +++ b/src/labthings_fastapi/base_descriptor.py @@ -20,7 +20,7 @@ `.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`\ . +`~lt.Thing`\ . """ from __future__ import annotations @@ -223,7 +223,7 @@ def is_bound(self) -> bool: return self._bound_to_obj is not None def owning_object_or_error(self) -> Owner: - """Return the `.Thing` instance to which we are bound, or raise an error. + """Return the `~lt.Thing` instance to which we are bound, or raise an error. This is mostly a convenience function that saves type-checking boilerplate. @@ -243,12 +243,13 @@ class BaseDescriptorInfo( r"""A class that describes a `BaseDescriptor`\ . This class is used internally by LabThings to describe :ref:`properties`\ , - :ref:`actions`\ , and other attributes of a `.Thing`\ . It's not usually + :ref:`actions`\ , and other attributes of a `~lt.Thing`\ . It's not usually encountered directly by someone using LabThings, except as a base class for - `.Action`\ , `.Property` and others. + `~.actions.Action`\ , `~.properties.BaseProperty` and others. - LabThings uses descriptors to represent the :ref:`wot_affordances` of a `.Thing`\ . - However, passing descriptors around isn't very elegant for two reasons: + LabThings uses descriptors to represent the :ref:`wot_affordances` of a + `~lt.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 @@ -256,8 +257,8 @@ class BaseDescriptorInfo( 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 `.Thing` instance, in which case it is - said to be "bound". This means there's no need to separately pass the `.Thing` + optionally hold a reference to a `~lt.Thing` instance, in which case it is + said to be "bound". This means there's no need to separately pass the `~lt.Thing` along with the descriptor, which should help keep things simple in several places in the code. """ @@ -324,7 +325,7 @@ def get(self) -> 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 `.Thing` instance. + if called via a class rather than a `~lt.Thing` instance. :return: the value of the descriptor. :raises NotBoundToInstanceError: if called on an unbound object. @@ -421,7 +422,7 @@ def __set_name__(self, owner: type[Owner], name: str) -> None: an argument. See `.get_class_attribute_docstrings` for more details. - :param owner: the `.Thing` subclass to which we are being attached. + :param owner: the `~lt.Thing` subclass to which we are being attached. :param name: the name to which we have been assigned. :raises DescriptorAddedToClassTwiceError: if the descriptor has been @@ -547,8 +548,8 @@ def __get__(self, obj: Owner | None, type: type | None = None) -> Value | Self: boilerplate in every subclass, we will call ``__instance_get__`` to get the value. - :param obj: the `.Thing` instance to which we are attached. - :param type: the `.Thing` subclass on which we are defined. + :param obj: the `~lt.Thing` instance to which we are attached. + :param type: the `~lt.Thing` subclass on which we are defined. :return: the value of the descriptor returned from ``__instance_get__`` when accessed on an instance, or the descriptor object if accessed on a class. @@ -571,7 +572,7 @@ def instance_get(self, obj: Owner) -> Value: the case where we are called as an instance attribute. This simplifies type annotations and removes the need for overload definitions in every subclass. - :param obj: is the `.Thing` instance on which this descriptor is being + :param obj: is the `~lt.Thing` instance on which this descriptor is being accessed. :return: the value of the descriptor (i.e. property value, or bound method). @@ -596,7 +597,7 @@ def _descriptor_info( object, and if not it is unbound, i.e. knows only about the class. :param info_class: the `.BaseDescriptorInfo` subclass to return. - :param obj: The `.Thing` instance to which the return value is bound. + :param obj: The `~lt.Thing` instance to which the return value is bound. :return: An object that may be used to refer to this descriptor. :raises RuntimeError: if garbage collection occurs unexpectedly. This should not happen and would indicate a LabThings bug. @@ -622,7 +623,7 @@ def descriptor_info( If ``owner`` is supplied, the returned object is bound to a particular object, and if not it is unbound, i.e. knows only about the class. - :param owner: The `.Thing` instance to which the return value is bound. + :param owner: The `~lt.Thing` instance to which the return value is bound. :return: An object that may be used to refer to this descriptor. """ return self._descriptor_info(BaseDescriptorInfo, owner) @@ -647,7 +648,7 @@ def set(self, value: Value) -> None: """Set the value of the descriptor. This method may only be called if the DescriptorInfo object is bound to a - `.Thing` instance. It will raise an error if called on a class. + `~lt.Thing` instance. It will raise an error if called on a class. :param value: the new value. @@ -723,7 +724,7 @@ class MyThing(Thing): check for the existence of a type hint during ``__set_name__`` and will evaluate it fully during ``value_type``\ . - :param owner: the `.Thing` subclass to which we are being attached. + :param owner: the `~lt.Thing` subclass to which we are being attached. :param name: the name to which we have been assigned. :raises InconsistentTypeError: if the type is specified twice and @@ -847,7 +848,7 @@ def descriptor_info( If ``owner`` is supplied, the returned object is bound to a particular object, and if not it is unbound, i.e. knows only about the class. - :param owner: The `.Thing` instance to which the return value is bound. + :param owner: The `~lt.Thing` instance to which the return value is bound. :return: An object that may be used to refer to this descriptor. """ return self._descriptor_info(FieldTypedBaseDescriptorInfo, owner) @@ -886,7 +887,7 @@ class DescriptorInfoCollection( This class is subclassed by each of the LabThings descriptors (Properties, Actions, etc.) and generated by a corresponding - `.OptionallyBoundDescriptor` on `.Thing` for convenience. + `.OptionallyBoundDescriptor` on `~lt.Thing` for convenience. """ def __init__( @@ -962,7 +963,7 @@ class OptionallyBoundDescriptor(Generic[Owner, OptionallyBoundInfoT]): with either the object, or its class, depending on how it is accessed. This is useful for returning collections of `.BaseDescriptorInfo` objects - from a `.Thing` subclass. + from a `~lt.Thing` subclass. """ def __init__(self, cls: type[OptionallyBoundInfoT]) -> None: diff --git a/src/labthings_fastapi/client/__init__.py b/src/labthings_fastapi/client/__init__.py index 24b4c59a..bc0ed6d4 100644 --- a/src/labthings_fastapi/client/__init__.py +++ b/src/labthings_fastapi/client/__init__.py @@ -1,4 +1,4 @@ -"""Code to access `.Thing` features over HTTP. +"""Code to access `~lt.Thing` features over HTTP. This module defines a base class for controlling LabThings-FastAPI over HTTP. It is based on `httpx`, and attempts to create a simple wrapper such that @@ -265,7 +265,7 @@ def from_url(cls, thing_url: str, client: Optional[httpx.Client] = None) -> Self set HTTP options, or if you want to work with a local server object for testing purposes (see `fastapi.TestClient`). - :return: a `.ThingClient` subclass with properties and methods that + :return: a `~lt.ThingClient` subclass with properties and methods that match the retrieved Thing Description (see :ref:`wot_thing`). """ td_client = client or httpx @@ -278,13 +278,13 @@ def from_url(cls, thing_url: str, client: Optional[httpx.Client] = None) -> Self def subclass_from_td(cls, thing_description: dict) -> type[Self]: """Create a ThingClient subclass from a Thing Description. - Dynamically subclass `.ThingClient` to add properties and + Dynamically subclass `~lt.ThingClient` to add properties and methods for each property and action in the Thing Description. :param thing_description: A :ref:`wot_td` as a dictionary, which will be used to construct the class. - :return: a `.ThingClient` subclass with the right properties and + :return: a `~lt.ThingClient` subclass with the right properties and methods. """ my_thing_description = thing_description @@ -305,7 +305,7 @@ class Client(cls): # type: ignore[valid-type, misc] class PropertyClientDescriptor: - """A base class for properties on `.ThingClient` objects.""" + """A base class for properties on `~lt.ThingClient` objects.""" name: str type: type | BaseModel @@ -324,7 +324,7 @@ def property_descriptor( The returned `.PropertyClientDescriptor` will have ``__get__`` and (optionally) ``__set__`` methods that are typed according to the - supplied ``model``. The descriptor should be added to a `.ThingClient` + supplied ``model``. The descriptor should be added to a `~lt.ThingClient` subclass and used to access the relevant property via `.ThingClient.get_property` and `.ThingClient.set_property`. @@ -398,7 +398,7 @@ def add_action(cls: type[ThingClient], action_name: str, action: dict) -> None: Currently, this will have a return type hint but no argument names or type hints. - :param cls: the `.ThingClient` subclass to which we are adding the + :param cls: the `~lt.ThingClient` subclass to which we are adding the action. :param action_name: is both the name we assign the method to, and the name of the action in the Thing Description. @@ -424,7 +424,7 @@ def add_property(cls: type[ThingClient], property_name: str, property: dict) -> by the ``property`` dictionary. - :param cls: the `.ThingClient` subclass to which we are adding the + :param cls: the `~lt.ThingClient` subclass to which we are adding the property. :param property_name: is both the name we assign the descriptor to, and the name of the property in the Thing Description. diff --git a/src/labthings_fastapi/endpoints.py b/src/labthings_fastapi/endpoints.py index 468b74a4..60db981a 100644 --- a/src/labthings_fastapi/endpoints.py +++ b/src/labthings_fastapi/endpoints.py @@ -1,7 +1,7 @@ """Add a FastAPI endpoint without making it an action. The `.EndpointDescriptor` wraps a function and marks it to be added to the -HTTP API at the same time as the properties and actions of the host `.Thing`. +HTTP API at the same time as the properties and actions of the host `~lt.Thing`. This is intended to allow flexibility to implement endpoints that cannot be described in a Thing Description as actions or properties. @@ -52,11 +52,11 @@ def __init__( See `.endpoint`, which is the usual way of instantiating this class. - :param func: is the method (defined on a `.Thing`) wrapped by this + :param func: is the method (defined on a `~lt.Thing`) wrapped by this descriptor. :param http_method: the HTTP verb we are responding to. This selects the FastAPI decorator: ``"get"`` corresponds to ``@app.get``. - :param path: the URL, relative to the host `.Thing`, for the endpoint. + :param path: the URL, relative to the host `~lt.Thing`, for the endpoint. :param \**kwargs: additional keyword arguments are passed to the FastAPI decorator, allowing you to specify responses, OpenAPI parameters, etc. @@ -69,14 +69,14 @@ def __init__( self.__doc__ = get_docstring(func) def instance_get(self, obj: Thing) -> Callable: - """Bind the method to the host `.Thing` and return it. + """Bind the method to the host `~lt.Thing` and return it. - This descriptor returns the wrapped function, with the `.Thing` bound as its + This descriptor returns the wrapped function, with the `~lt.Thing` bound as its first argument. This is the usual behaviour for Python methods. - :param obj: The `.Thing` on which the descriptor is defined. + :param obj: The `~lt.Thing` on which the descriptor is defined. - :return: The wrapped function, bound to the `.Thing` (when called as + :return: The wrapped function, bound to the `~lt.Thing` (when called as an instance attribute). """ return wraps(self.func)(partial(self.func, obj)) @@ -89,16 +89,16 @@ def path(self) -> str: def add_to_fastapi(self, app: FastAPI, thing: Thing) -> None: """Add an endpoint for this function to a FastAPI app. - We will add an endpoint to the app, bound to a particular `.Thing`. - The URL will be prefixed with the `.Thing` path, i.e. the specified + We will add an endpoint to the app, bound to a particular `~lt.Thing`. + The URL will be prefixed with the `~lt.Thing` path, i.e. the specified URL (which defaults to the name of this descriptor) is relative to - the host `.Thing`. + the host `~lt.Thing`. :param app: the `fastapi.FastAPI` application we are adding to. - :param thing: the `.Thing` we're bound to. + :param thing: the `~lt.Thing` we're bound to. :raises NotConnectedToServerError: if there is no ``path`` attribute - of the host `.Thing` (which usually means it is not yet connected + of the host `~lt.Thing` (which usually means it is not yet connected to a server). """ if thing.path is None: @@ -124,14 +124,14 @@ def endpoint( ) -> Callable[[Callable], EndpointDescriptor]: r"""Mark a function as a FastAPI endpoint without making it an action. - This decorator will cause a method of a `.Thing` to be directly added to + This decorator will cause a method of a `~lt.Thing` to be directly added to the HTTP API, bypassing the machinery underlying Action and Property affordances. Such endpoints will not be documented in the :ref:`wot_td` but may be used as the target of links. For example, this could allow a file - to be downloaded from the `.Thing` at a known URL, or serve a video stream + to be downloaded from the `~lt.Thing` at a known URL, or serve a video stream that wouldn't be supported as a `.Blob`\ . - The majority of `.Thing` implementations won't need this decorator, but + The majority of `~lt.Thing` implementations won't need this decorator, but it is here to enable flexibility when it's needed. This decorator always takes arguments; in particular, ``method`` is @@ -147,13 +147,13 @@ def plain_text_response(self) -> str: This decorator is intended to work very similarly to the `fastapi` decorators ``@app.get``, ``@app.post``, etc., with two changes: - 1. The path is relative to the host `.Thing` and will default to the name + 1. The path is relative to the host `~lt.Thing` and will default to the name of the method. - 2. The method will be called with the host `.Thing` as its first argument, + 2. The method will be called with the host `~lt.Thing` as its first argument, i.e. it will be bound to the class as usua. :param method: The HTTP verb this endpoint responds to. - :param path: The path, relative to the host `.Thing` base URL. + :param path: The path, relative to the host `~lt.Thing` base URL. :param \**kwargs: Additional keyword arguments are passed to the `fastapi.FastAPI.get` decorator if ``method`` is ``get``, or to the equivalent decorator for other HTTP verbs. diff --git a/src/labthings_fastapi/exceptions.py b/src/labthings_fastapi/exceptions.py index e670e0a9..f31064d0 100644 --- a/src/labthings_fastapi/exceptions.py +++ b/src/labthings_fastapi/exceptions.py @@ -9,15 +9,15 @@ class NotConnectedToServerError(RuntimeError): """The Thing is not connected to a server. This exception is called if an Action is called or - a `.DataProperty` is updated on a Thing that is not + a `~lt.DataProperty` is updated on a Thing that is not connected to a ThingServer. A server connection is needed to manage asynchronous behaviour. - `.Thing` instances are also only assigned a ``path`` when they + `~lt.Thing` instances are also only assigned a ``path`` when they are added to a server, so this error may be raised by functions that implement the HTTP API if an attempt is made to construct - the API before the `.Thing` has been assigned a path. + the API before the `~lt.Thing` has been assigned a path. """ @@ -41,7 +41,7 @@ class ReadOnlyPropertyError(AttributeError): class PropertyNotObservableError(RuntimeError): """The property is not observable. - This exception is raised when `.Thing.observe_property` is called with a + This exception is raised when `~lt.Thing.observe_property` is called with a property that is not observable. Currently, only data properties are observable: functional properties (using a getter/setter) may not be observed. @@ -51,9 +51,9 @@ class PropertyNotObservableError(RuntimeError): class InconsistentTypeError(TypeError): """Different type hints have been given for a descriptor. - Some descriptors in LabThings, particularly `.DataProperty` and `.ThingSlot` + Some descriptors in LabThings, particularly `~lt.DataProperty` and `.ThingSlot` may have their type specified in different ways. If multiple type hints are - provided, they must match. See `.property` for more details. + provided, they must match. See `~lt.property` for more details. """ @@ -64,16 +64,16 @@ class MissingTypeError(TypeError): There are different ways of providing these type hints. This error indicates that no type hint was found. - See documentation for `.property` and `.thing_slot` for more details. + See documentation for `~lt.property` and `~lt.thing_slot` for more details. """ class ThingNotConnectedError(RuntimeError): r"""`.ThingSlot`\ s have not yet been set up. - This error is raised if a `.ThingSlot` is accessed before the `.Thing` has + This error is raised if a `.ThingSlot` is accessed before the `~lt.Thing` has been supplied by the LabThings server. This usually happens because either - the `.Thing` is being used without a server (in which case the attribute + the `~lt.Thing` is being used without a server (in which case the attribute should be mocked), or because it has been accessed before ``__enter__`` has been called. """ @@ -172,7 +172,8 @@ class UnsupportedConstraintError(ValueError): class FailedToInvokeActionError(RuntimeError): """The action could not be started. - This error is raised by a `.ThingClient` instance if an action could not be started. + This error is raised by a `~lt.ThingClient` instance if an action could not be + started. It most commonly occurs because the input to the action could not be converted to the required type: the error message should give more detail on what's wrong. """ @@ -195,12 +196,12 @@ class NotBoundToInstanceError(RuntimeError): """A `.BaseDescriptorInfo` is not bound to an object. Some methods and properties of `.BaseDescriptorInfo` objects require them - to be bound to a `.Thing` instance. If these methods are called on a + to be bound to a `~lt.Thing` instance. If these methods are called on a `.BaseDescriptorInfo` object that is unbound, this exception is raised. This exception should only be seen when `.BaseDescriptorInfo` objects are - generated from a `.Thing` class. Usually, they should be accessed via a - `.Thing` instance, in which case they will be bound. + generated from a `~lt.Thing` class. Usually, they should be accessed via a + `~lt.Thing` instance, in which case they will be bound. """ diff --git a/src/labthings_fastapi/invocation_contexts.py b/src/labthings_fastapi/invocation_contexts.py index b7c31ae5..5969030a 100644 --- a/src/labthings_fastapi/invocation_contexts.py +++ b/src/labthings_fastapi/invocation_contexts.py @@ -6,7 +6,7 @@ If you are writing action code and want to use logging or allow cancellation, most of the time you should just use `.get_invocation_logger` or -`.cancellable_sleep` which are exposed as part of the top-level module. +`~lt.cancellable_sleep` which are exposed as part of the top-level module. This module includes lower-level functions that are useful for testing or managing concurrency. Many of these accept an ``id`` argument, which is @@ -227,7 +227,7 @@ def raise_if_cancelled() -> None: This function checks for cancellation events and, if the current action invocation has been cancelled, it will raise an `.InvocationCancelledError` to signal the thread to terminate. - It is equivalent to `.cancellable_sleep` but without waiting any + It is equivalent to `~lt.cancellable_sleep` but without waiting any time. If called outside of an invocation context, this function does diff --git a/src/labthings_fastapi/logs.py b/src/labthings_fastapi/logs.py index 43efc848..7c679791 100644 --- a/src/labthings_fastapi/logs.py +++ b/src/labthings_fastapi/logs.py @@ -112,8 +112,8 @@ def add_thing_log_destination( ) -> None: """Append logs matching ``invocation_id`` to a specified sequence. - This instructs a handler on the logger used for `.Thing` instances to append a copy - of the logs generated by that invocation to the specified sequence. + This instructs a handler on the logger used for `~lt.Thing` instances to append a + copy of the logs generated by that invocation to the specified sequence. This is primarily used by invocation threads to collect their logs, so they may be returned when the invocation is queried. diff --git a/src/labthings_fastapi/outputs/blob.py b/src/labthings_fastapi/outputs/blob.py index 9df4cd92..390ef27e 100644 --- a/src/labthings_fastapi/outputs/blob.py +++ b/src/labthings_fastapi/outputs/blob.py @@ -864,7 +864,7 @@ def from_url(cls, href: str, client: httpx.Client | None = None) -> Self: def response(self) -> Response: """Return a suitable response for serving the output. - This method is called by the `.ThingServer` to generate a response + This method is called by the `~lt.ThingServer` to generate a response that returns the data over HTTP. :return: an HTTP response that streams data from memory or file. diff --git a/src/labthings_fastapi/outputs/mjpeg_stream.py b/src/labthings_fastapi/outputs/mjpeg_stream.py index 2c142269..0a79aa78 100644 --- a/src/labthings_fastapi/outputs/mjpeg_stream.py +++ b/src/labthings_fastapi/outputs/mjpeg_stream.py @@ -1,6 +1,6 @@ """MJPEG Stream support. -This module defines a descriptor that allows `.Thing` subclasses to expose an +This module defines a descriptor that allows `~lt.Thing` subclasses to expose an MJPEG stream. See `.MJPEGStreamDescriptor`. """ @@ -115,7 +115,7 @@ class MJPEGStream: The minimum needed to make the stream work is to periodically call `add_frame` with JPEG image data. - To add a stream to a `.Thing`, use the `.MJPEGStreamDescriptor` + To add a stream to a `~lt.Thing`, use the `.MJPEGStreamDescriptor` which will handle creating an `.MJPEGStream` object on first access, and will also add it to the HTTP API. @@ -134,8 +134,8 @@ def __init__( See the class docstring for `.MJPEGStream`. Note that it will often be initialised by `.MJPEGStreamDescriptor`. - :param thing_server_interface: the `.ThingServerInterface` of the - `.Thing` associated with this stream. It's used to run the async + :param thing_server_interface: the `~lt.ThingServerInterface` of the + `~lt.Thing` associated with this stream. It's used to run the async code that relays frames to open connections. :param ringbuffer_size: The number of frames to retain in memory, to allow retrieval after the frame has been sent. @@ -369,7 +369,7 @@ async def notify_stream_stopped(self) -> None: class MJPEGStreamDescriptor: """A descriptor that returns a MJPEGStream object when accessed. - If this descriptor is added to a `.Thing`, it will create an `.MJPEGStream` + If this descriptor is added to a `~lt.Thing`, it will create an `.MJPEGStream` object when it is first accessed. It will also add two HTTP endpoints, one with the name of the descriptor serving the MJPEG stream, and another with `/viewer` appended, which serves a basic HTML page that views the stream. @@ -391,7 +391,7 @@ def __set_name__(self, _owner: Thing, name: str) -> None: The name is important, as it will set the URL of the HTTP endpoint used to access the stream. - :param _owner: the `.Thing` to which we are attached. + :param _owner: the `~lt.Thing` to which we are attached. :param name: the name to which this descriptor is assigned. """ self.name = name @@ -412,7 +412,7 @@ def __get__( When accessed on the object, an `.MJPEGStream` is returned. - :param obj: the host `.Thing`, or ``None`` if accessed on the class. + :param obj: the host `~lt.Thing`, or ``None`` if accessed on the class. :param type: the class on which we are defined. :return: an `.MJPEGStream`, or this descriptor. @@ -441,7 +441,7 @@ def add_to_fastapi(self, app: FastAPI, thing: Thing) -> None: """Add the stream to the FastAPI app. We create two endpoints, one for the MJPEG stream (using the name of - the descriptor, relative to the host `.Thing`) and one serving a + the descriptor, relative to the host `~lt.Thing`) and one serving a basic viewer. The example code below would create endpoints at ``/camera/stream`` @@ -459,7 +459,7 @@ class Camera(lt.Thing): server = lt.ThingServer({"camera": Camera}) :param app: the `fastapi.FastAPI` application to which we are being added. - :param thing: the host `.Thing` instance. + :param thing: the host `~lt.Thing` instance. """ app.get( f"{thing.path}{self.name}", diff --git a/src/labthings_fastapi/properties.py b/src/labthings_fastapi/properties.py index 02ed2f90..152cdce7 100644 --- a/src/labthings_fastapi/properties.py +++ b/src/labthings_fastapi/properties.py @@ -1,8 +1,8 @@ -"""Define properties of `.Thing` objects. +"""Define properties of `~lt.Thing` objects. -:ref:`properties` are attributes of a `.Thing` that may be read or written to +:ref:`properties` are attributes of a `~lt.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 +a function `~lt.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: @@ -32,7 +32,7 @@ def _set_remaining(self, value: int) -> None: 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" +`dataclasses` or `pydantic` in that `~lt.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. @@ -135,7 +135,7 @@ class attribute. Documentation is in strings immediately following the @with_config(ConfigDict(extra="forbid")) class FieldConstraints(TypedDict, total=False): - r"""Constraints that may be applied to a `.property`\ .""" + r"""Constraints that may be applied to a `~lt.property`\ .""" gt: int | float ge: int | float @@ -152,7 +152,7 @@ class FieldConstraints(TypedDict, total=False): class OverspecifiedDefaultError(ValueError): """The default value has been specified more than once. - This error is raised when a `.DataProperty` is instantiated with both a + This error is raised when a `~lt.DataProperty` is instantiated with both a ``default`` value and a ``default_factory`` provided. """ @@ -160,7 +160,7 @@ class OverspecifiedDefaultError(ValueError): class MissingDefaultError(ValueError): """The default value has not been specified. - This error is raised when a `.DataProperty` is instantiated without a + This error is raised when a `~lt.DataProperty` is instantiated without a ``default`` value or a ``default_factory`` function. """ @@ -169,7 +169,7 @@ class MissingDefaultError(ValueError): """The value returned by a property.""" Owner = TypeVar("Owner", bound="Thing") -"""The `.Thing` instance on which a property is bound.""" +"""The `~lt.Thing` instance on which a property is bound.""" BasePropertyT = TypeVar("BasePropertyT", bound="BaseProperty") """An instance of (a subclass of) BaseProperty.""" @@ -197,8 +197,8 @@ def default_factory_from_arguments( 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 + This logic originally lived only in the initialiser of `~lt.DataProperty` + but it was needed in the `~lt.property` and `~lt.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). @@ -256,11 +256,11 @@ def property( readonly: bool = False, **constraints: Any, ) -> Value | FunctionalProperty[Owner, Value]: - r"""Define a Property on a `.Thing`\ . + r"""Define a Property on a `~lt.Thing`\ . This function may be used to define :ref:`properties` in two ways, as either a decorator or a field specifier. See the - examples in the :mod:`.property` documentation. + examples in the :ref:`properties`\ . Properties should always have a type annotation. This type annotation will be used in automatic documentation and also to serialise the value @@ -279,9 +279,9 @@ def property( need to use a mutable datatype. For example, it would be better to specify ``default_factory=list`` than ``default=[]`` because the second form would be shared - between all `.Thing`\ s with this property. + between all `~lt.Thing`\ s with this property. :param readonly: whether the property should be read-only - via the `.ThingClient` interface (i.e. over HTTP or via + via the `~lt.ThingClient` interface (i.e. over HTTP or via a `.DirectThingClient`). This is automatically true if ``property`` is used as a decorator and no setter is specified. @@ -292,7 +292,7 @@ def property( of constraint arguments. :return: a property descriptor, either a `.FunctionalProperty` - if used as a decorator, or a `.DataProperty` if used as + if used as a decorator, or a `~lt.DataProperty` if used as a field. :raises MissingDefaultError: if no valid default value is supplied, @@ -311,8 +311,8 @@ def property( its type hint until after it's been called. When used as a field specifier, ``property`` returns a generic - `.DataProperty` descriptor instance, which will determine its type - when it is attached to the `.Thing`. The type hint on the return + `~lt.DataProperty` descriptor instance, which will determine its type + when it is attached to the `~lt.Thing`. The type hint on the return value of ``property`` in that situation is a "white lie": we annotate the return as having the same type as the ``default`` value (or the ``default_factory`` return value). This means that type checkers such @@ -354,12 +354,12 @@ def property( class BaseProperty(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 + This class is used to determine whether an attribute of a `~lt.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. + use `~lt.property` to declare properties on your `~lt.Thing` subclass. """ def __init__(self, constraints: Mapping[str, Any] | None = None) -> None: @@ -412,7 +412,7 @@ def constraints(self) -> FieldConstraints: # noqa[DOC201] Note that these constraints will be enforced when values are received over HTTP, but they are not automatically enforced - when setting the property directly on the `.Thing` instance + when setting the property directly on the `~lt.Thing` instance from Python code. """ return self._constraints @@ -454,7 +454,7 @@ def model(self) -> type[BaseModel]: def get_default(self, obj: Owner | None) -> Value: """Return the default value of this property. - :param obj: the `.Thing` instance on which we are looking for the default. + :param obj: the `~lt.Thing` instance on which we are looking for the default. or `None` if referring to the class. For now, this is ignored. :return: the default value of this property. @@ -475,7 +475,7 @@ def reset(self, obj: Owner) -> None: to handle `.FeatureNotAvailableError` exceptions, which will be raised if this method is not overridden. - :param obj: the `.Thing` instance we want to reset. + :param obj: the `~lt.Thing` instance we want to reset. :raises FeatureNotAvailableError: as only some subclasses implement resetting. """ raise FeatureNotAvailableError( @@ -490,7 +490,7 @@ def is_resettable(self, obj: Owner | None) -> bool: If you override ``reset`` but want more control over this behaviour, you probably need to override `is_resettable`\ . - :param obj: the `.Thing` instance we want to reset. + :param obj: the `~lt.Thing` instance we want to reset. :return: `True` if a call to ``reset()`` should work. """ return BaseProperty.reset is not self.__class__.reset @@ -499,9 +499,9 @@ def add_to_fastapi(self, app: 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. + :param thing: The `~lt.Thing` we are adding the endpoints for. - :raises NotConnectedToServerError: if the `.Thing` does not have + :raises NotConnectedToServerError: if the `~lt.Thing` does not have a ``path`` set. """ if thing.path is None: @@ -565,12 +565,12 @@ def property_affordance( ) -> 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 + :param thing: the `~lt.Thing` to which we are attached. + :param path: the URL of the `~lt.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 + :raises NotConnectedToServerError: if the `~lt.Thing` does not have a ``path`` set. """ path = path or thing.path @@ -645,7 +645,7 @@ def descriptor_info( class DataProperty(BaseProperty[Owner, Value], Generic[Owner, Value]): """A Property descriptor that acts like a regular variable. - `.DataProperty` descriptors remember their value, and can be read and + `~lt.DataProperty` descriptors remember their value, and can be read and written to like a regular Python variable. """ @@ -677,15 +677,15 @@ def __init__( ) -> None: """Create a property that acts like a regular variable. - `.DataProperty` descriptors function just like variables, in that - they can be read and written to as attributes of the `.Thing` and + `~lt.DataProperty` descriptors function just like variables, in that + they can be read and written to as attributes of the `~lt.Thing` and their value will be the same every time it is read (i.e. it changes only when it is set). This differs from `.FunctionalProperty` which uses a "getter" function just like `builtins.property` and may return a different value each time. - `.DataProperty` instances may always be set, when they are accessed - as an attribute of the `.Thing` instance. The ``readonly`` parameter + `~lt.DataProperty` instances may always be set, when they are accessed + as an attribute of the `~lt.Thing` instance. The ``readonly`` parameter applies only to client code, whether it is remote or a `.DirectThingClient` wrapper. @@ -703,7 +703,7 @@ def __init__( a mutable default value can lead to odd behaviour. :param readonly: if ``True``, the property may not be written to via HTTP, or via `.DirectThingClient` objects, i.e. it may only be - set as an attribute of the `.Thing` and not from a client. + set as an attribute of the `~lt.Thing` and not from a client. :param constraints: is passed as keyword arguments to `pydantic.Field` to add validation constraints to the property. See `pydantic.Field` for details. @@ -719,7 +719,7 @@ def instance_get(self, obj: Owner) -> Value: This will supply a default if the property has not yet been set. - :param obj: The `.Thing` on which the property is being accessed. + :param obj: The `~lt.Thing` on which the property is being accessed. :return: the value of the property. """ if self.name not in obj.__dict__: @@ -739,7 +739,7 @@ def __set__( this will validate the value against the property's model, and an error will be raised if the value is not valid. - :param obj: the `.Thing` to which we are attached. + :param obj: the `~lt.Thing` to which we are attached. :param value: the new value for the property. :param emit_changed_event: whether to emit a changed event. """ @@ -754,10 +754,10 @@ def __set__( def get_default(self, obj: Owner | None) -> Value: """Return the default value of this property. - Note that this implementation is independent of the `.Thing` instance, + Note that this implementation is independent of the `~lt.Thing` instance, as there's currently no way to specify a per-instance default. - :param obj: the `.Thing` instance we want to reset. + :param obj: the `~lt.Thing` instance we want to reset. :return: the default value of this property. """ @@ -766,9 +766,9 @@ def get_default(self, obj: Owner | None) -> Value: def reset(self, obj: Owner) -> None: r"""Reset the property to its default value. - This resets to the value returned by ``default`` for `.DataProperty`\ . + This resets to the value returned by ``default`` for `~lt.DataProperty`\ . - :param obj: the `.Thing` instance we want to reset. + :param obj: the `~lt.Thing` instance we want to reset. """ self.__set__(obj, self.get_default(obj)) @@ -778,7 +778,7 @@ def _observers_set(self, obj: Thing) -> WeakSet: Each observer in this set will be notified when the property is changed. See ``.DataProperty.emit_changed_event`` - :param obj: the `.Thing` to which we are attached. + :param obj: the `~lt.Thing` to which we are attached. :return: the set of observers corresponding to ``obj``. """ @@ -799,7 +799,7 @@ def emit_changed_event(self, obj: Thing, value: Value) -> None: This method will raise a `.ServerNotRunningError` if the event loop is not running, and should only be called after the server has started. - :param obj: the `.Thing` to which we are attached. + :param obj: the `~lt.Thing` to which we are attached. :param value: the new property value, to be sent to observers. """ obj._thing_server_interface.start_async_task_soon( @@ -814,7 +814,7 @@ async def emit_changed_event_async(self, obj: Thing, value: Value) -> None: This function may only be run in the `anyio` event loop. See `.DataProperty.emit_changed_event`. - :param obj: the `.Thing` to which we are attached. + :param obj: the `~lt.Thing` to which we are attached. :param value: the new property value, to be sent to observers. """ for observer in self._observers_set(obj): @@ -826,7 +826,7 @@ async def emit_changed_event_async(self, obj: Thing, value: Value) -> None: class FunctionalProperty(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 + For properties that should work like variables, use `~lt.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` @@ -934,7 +934,7 @@ def _set_myprop(self, val: int) -> None: 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` + have the same name or not. The only difference is that the `~lt.Thing` will have an additional method called ``_set_myprop`` in the example above. @@ -975,7 +975,7 @@ def _set_myprop(self, val: int) -> None: def instance_get(self, obj: Owner) -> Value: """Get the value of the property. - :param obj: the `.Thing` on which the attribute is accessed. + :param obj: the `~lt.Thing` on which the attribute is accessed. :return: the value of the property. """ return self.fget(obj) @@ -987,7 +987,7 @@ def __set__(self, obj: Owner, value: Value) -> None: this will validate the value against the property's model, and an error will be raised if the value is not valid. - :param obj: the `.Thing` on which the attribute is accessed. + :param obj: the `~lt.Thing` on which the attribute is accessed. :param value: the value of the property. :raises ReadOnlyPropertyError: if the property cannot be set. @@ -1115,9 +1115,9 @@ def _reset_myprop(self) -> None: def reset(self, obj: Owner) -> None: r"""Reset the property to its default value. - This resets to the value returned by ``default`` for `.DataProperty`\ . + This resets to the value returned by ``default`` for `~lt.DataProperty`\ . - :param obj: the `.Thing` instance we want to reset. + :param obj: the `~lt.Thing` instance we want to reset. :raises FeatureNotAvailable: if no reset method is available, which means there is no default defined, and no resetter method. """ @@ -1153,7 +1153,7 @@ class PropertyInfo( 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. + `~lt.Thing` instance, or may be accessed from the class. """ @builtins.property @@ -1253,10 +1253,10 @@ def validate(self, value: Any) -> Value: class PropertyCollection(DescriptorInfoCollection[Owner, PropertyInfo], Generic[Owner]): - """Access to metadata on all the properties of a `.Thing` instance or subclass. + """Access to metadata on all the properties of a `~lt.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 + each Property of a `~lt.Thing` by name. This allows easy access to metadata like their description and model. """ @@ -1287,18 +1287,18 @@ def setting( readonly: bool = False, **constraints: Any, ) -> FunctionalSetting[Owner, Value] | Value: - r"""Define a Setting on a `.Thing`\ . + r"""Define a Setting on a `~lt.Thing`\ . A setting is a property that is saved to disk. This function defines a setting, which is a special Property that will be saved to disk, so it persists even when the LabThings server is - restarted. It is otherwise very similar to `.property`\ . + restarted. It is otherwise very similar to `~lt.property`\ . A type annotation is required, and should follow the same constraints as - for :deco:`.property`. + for :deco:`~lt.property`. - Every ``setting`` on a `.Thing` will be read each time the settings are + Every ``setting`` on a `~lt.Thing` will be read each time the settings are saved, which may be quite frequent. This means your getter must not take too long to run, or have side-effects. Settings that use getters and setters may be removed in the future pending the outcome of `#159`_. @@ -1327,9 +1327,9 @@ def setting( need to use a mutable datatype. For example, it would be better to specify ``default_factory=list`` than ``default=[]`` because the second form would be shared - between all `.Thing`\ s with this setting. + between all `~lt.Thing`\ s with this setting. :param readonly: whether the setting should be read-only - via the `.ThingClient` interface (i.e. over HTTP or via + via the `~lt.ThingClient` interface (i.e. over HTTP or via a `.DirectThingClient`). :param \**constraints: additional keyword arguments are passed to `pydantic.Field` and allow constraints to be added to the @@ -1345,7 +1345,7 @@ def setting( **Typing Notes** - See the typing notes on `.property` as they all apply to `.setting` as + See the typing notes on `~lt.property` as they all apply to `~lt.setting` as well. """ if getter is not ...: @@ -1376,7 +1376,7 @@ class BaseSetting(BaseProperty[Owner, Value], Generic[Owner, Value]): r"""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 + It is not intended to be used directly, but via `~lt.setting` and the two concrete implementations: `.DataSetting` and `.FunctionalSetting`\ . """ @@ -1387,7 +1387,7 @@ def set_without_emit(self, obj: Owner, value: Value) -> None: 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 obj: the `~lt.Thing` to which we are attached. :param value: the new value of the setting. :raises NotImplementedError: this method should be implemented in subclasses. @@ -1407,12 +1407,12 @@ def descriptor_info(self, owner: Owner | None = None) -> SettingInfo[Owner, Valu class DataSetting( DataProperty[Owner, Value], BaseSetting[Owner, Value], Generic[Owner, Value] ): - """A `.DataProperty` that persists on disk. + """A `~lt.DataProperty` that persists on disk. A setting can be accessed via the HTTP API and is persistent between sessions. - A `.DataSetting` is a `.DataProperty` with extra functionality for triggering - a `.Thing` to save its settings. + A `.DataSetting` is a `~lt.DataProperty` with extra functionality for triggering + a `~lt.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 @@ -1430,7 +1430,7 @@ def __set__( This will cause the settings to be saved to disk. - :param obj: the `.Thing` to which we are attached. + :param obj: the `~lt.Thing` to which we are attached. :param value: the new value of the setting. :param emit_changed_event: whether to emit a changed event. """ @@ -1444,7 +1444,7 @@ def set_without_emit(self, obj: Owner, value: Value) -> None: 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 obj: the `~lt.Thing` to which we are attached. :param value: the new value of the setting. """ super().__set__(obj, value, emit_changed_event=False) @@ -1458,7 +1458,7 @@ class FunctionalSetting( 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. + triggering a `~lt.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 @@ -1475,7 +1475,7 @@ def __set__(self, obj: Owner, value: Value) -> None: This will cause the settings to be saved to disk. - :param obj: the `.Thing` to which we are attached. + :param obj: the `~lt.Thing` to which we are attached. :param value: the new value of the setting. """ super().__set__(obj, value) @@ -1488,7 +1488,7 @@ def set_without_emit(self, obj: Owner, value: Value) -> None: 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 obj: the `~lt.Thing` to which we are attached. :param value: the new value of the setting. """ # FunctionalProperty does not emit changed events, so no special @@ -1511,10 +1511,10 @@ def set_without_emit(self, value: Value) -> None: class SettingCollection(DescriptorInfoCollection[Owner, SettingInfo], Generic[Owner]): - """Access to metadata on all the properties of a `.Thing` instance or subclass. + """Access to metadata on all the properties of a `~lt.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 + each Property of a `~lt.Thing` by name. This allows easy access to metadata like their description and model. """ diff --git a/src/labthings_fastapi/server/__init__.py b/src/labthings_fastapi/server/__init__.py index 0cba0f89..3265bf6c 100644 --- a/src/labthings_fastapi/server/__init__.py +++ b/src/labthings_fastapi/server/__init__.py @@ -1,9 +1,9 @@ """Code supporting the LabThings server. -LabThings wraps the `fastapi.FastAPI` application in a `.ThingServer`, which -provides the tools to serve and manage `.Thing` instances. +LabThings wraps the `fastapi.FastAPI` application in a `~lt.ThingServer`, which +provides the tools to serve and manage `~lt.Thing` instances. -See the :ref:`tutorial` for examples of how to set up a `.ThingServer`. +See the :ref:`tutorial` for examples of how to set up a `~lt.ThingServer`. """ from __future__ import annotations @@ -44,17 +44,17 @@ class ThingServer: - """Use FastAPI to serve `.Thing` instances. + """Use FastAPI to serve `~lt.Thing` instances. - The `.ThingServer` sets up a `fastapi.FastAPI` application and uses it - to expose the capabilities of `.Thing` instances over HTTP. + The `~lt.ThingServer` sets up a `fastapi.FastAPI` application and uses it + to expose the capabilities of `~lt.Thing` instances over HTTP. - There are several functions of a `.ThingServer`: + There are several functions of a `~lt.ThingServer`: - * Manage where settings are stored, to allow `.Thing` instances to + * Manage where settings are stored, to allow `~lt.Thing` instances to load and save their settings from disk. * Configure the server to allow cross-origin requests (required if - we use a web app that is not served from the `.ThingServer`). + we use a web app that is not served from the `~lt.ThingServer`). * Manage the threads used to run :ref:`actions`. * Manage :ref:`blobs` to allow binary data to be returned. * Allow threaded code to call functions in the event loop, by providing @@ -71,25 +71,25 @@ def __init__( ) -> None: r"""Initialise a LabThings server. - Setting up the `.ThingServer` involves creating the underlying + Setting up the `~lt.ThingServer` involves creating the underlying `fastapi.FastAPI` app, setting its lifespan function (used to - set up and shut down the `.Thing` instances), and configuring it + set up and shut down the `~lt.Thing` instances), and configuring it to allow cross-origin requests. We also create the `.ActionManager` to manage :ref:`actions` and the `.BlobManager` to manage the downloading of :ref:`blobs`. - :param things: A mapping of Thing names to `.Thing` subclasses, or - `.ThingConfig` objects specifying the subclass, its initialisation - arguments, and any connections to other `.Thing`\ s. - :param settings_folder: the location on disk where `.Thing` + :param things: A mapping of Thing names to `~lt.Thing` subclasses, or + `~lt.ThingConfig` objects specifying the subclass, its initialisation + arguments, and any connections to other `~lt.Thing`\ s. + :param settings_folder: the location on disk where `~lt.Thing` settings will be saved. :param api_prefix: A prefix for all API routes. This must either be empty, or start with a slash and not end with a slash. :param application_config: A mapping containing custom configuration for the - application. This is not processed by LabThings. Each `.Thing` can access - this via the Thing-Server interface. - :param debug: If ``True``, set the log level for `.Thing` instances to + application. This is not processed by LabThings. Each `~lt.Thing` can + access this via the Thing-Server interface. + :param debug: If ``True``, set the log level for `~lt.Thing` instances to DEBUG. """ self.startup_failure: dict | None = None @@ -124,9 +124,9 @@ def from_config(cls, config: ThingServerConfig, debug: bool = False) -> Self: This is equivalent to ``ThingServer(**dict(config))``\ . :param config: The configuration parameters for the server. - :param debug: If ``True``, set the log level for `.Thing` instances to + :param debug: If ``True``, set the log level for `~lt.Thing` instances to DEBUG. - :return: A `.ThingServer` configured as per the model. + :return: A `~lt.ThingServer` configured as per the model. """ return cls(**dict(config), debug=debug) @@ -135,7 +135,7 @@ def _set_cors_middleware(self) -> None: This is required to allow web applications access to the HTTP API, if they are not served from the same origin (i.e. if they are not - served as part of the `.ThingServer`.). + served as part of the `~lt.ThingServer`.). This is usually needed during development, and may be needed at other times depending on how you are using LabThings. @@ -161,7 +161,7 @@ def _set_url_for_middleware(self) -> None: def things(self) -> Mapping[str, Thing]: """Return a dictionary of all the things. - :return: a dictionary mapping thing paths to `.Thing` instances. + :return: a dictionary mapping thing paths to `~lt.Thing` instances. """ return MappingProxyType(self._things) @@ -190,7 +190,7 @@ def things_by_class(self, cls: type[ThingInstance]) -> Sequence[ThingInstance]: Return all instances of ``cls`` attached to this server. - :param cls: A `.Thing` subclass. + :param cls: A `~lt.Thing` subclass. :return: all instances of ``cls`` that have been added to this server. """ @@ -202,7 +202,7 @@ def thing_by_class(self, cls: type[ThingInstance]) -> ThingInstance: This function calls `.ThingServer.things_by_class`, but asserts that there is exactly one match. - :param cls: a `.Thing` subclass. + :param cls: a `~lt.Thing` subclass. :return: the instance of ``cls`` attached to this server. @@ -231,16 +231,16 @@ def path_for_thing(self, name: str) -> str: def _create_things(self) -> Mapping[str, Thing]: r"""Create the Things, add them to the server, and connect them up if needed. - This method is responsible for creating instances of `.Thing` subclasses - and adding them to the server. It also ensures the `.Thing`\ s are connected + This method is responsible for creating instances of `~lt.Thing` subclasses + and adding them to the server. It also ensures the `~lt.Thing`\ s are connected together if required. The Things are defined in ``self._config.thing_configs`` which in turn is generated from the ``things`` argument to ``__init__``\ . - :return: A mapping of names to `.Thing` instances. + :return: A mapping of names to `~lt.Thing` instances. - :raise TypeError: if ``cls`` is not a subclass of `.Thing`. + :raise TypeError: if ``cls`` is not a subclass of `~lt.Thing`. """ things: dict[str, Thing] = {} for name, config in self._config.thing_configs.items(): @@ -259,10 +259,10 @@ def _create_things(self) -> Mapping[str, Thing]: def _connect_things(self) -> None: r"""Connect the `thing_slot` attributes of Things. - A `.Thing` may have attributes defined as ``lt.thing_slot()``, which - will be populated after all `.Thing` instances are loaded on the server. + A `~lt.Thing` may have attributes defined as ``lt.thing_slot()``, which + will be populated after all `~lt.Thing` instances are loaded on the server. - This function is responsible for supplying the `.Thing` instances required + This function is responsible for supplying the `~lt.Thing` instances required for each connection. This will be done by using the name specified either in the connection's default, or in the configuration of the server. @@ -281,8 +281,8 @@ def _connect_things(self) -> None: def _attach_things_to_server(self) -> None: """Add the Things to the FastAPI App. - This calls `.Thing.attach_to_server` on each `.Thing` that is a part of - this `.ThingServer` in order to add the HTTP endpoints and load settings. + This calls `~lt.Thing.attach_to_server` on each `~lt.Thing` that is a part of + this `~lt.ThingServer` in order to add the HTTP endpoints and load settings. """ for thing in self.things.values(): thing.attach_to_server(self) @@ -351,9 +351,9 @@ def thing_descriptions(request: Request) -> Mapping[str, ThingDescription]: """Describe all the things available from this server. This returns a dictionary, where the keys are the paths to each - `.Thing` attached to the server, and the values are :ref:`wot_td` documents - represented as `.ThingDescription` objects. These should enable - clients to see all the capabilities of the `.Thing` instances and + `~lt.Thing` attached to the server, and the values are :ref:`wot_td` + documents represented as `.ThingDescription` objects. These should enable + clients to see all the capabilities of the `~lt.Thing` instances and access them over HTTP. :param request: is supplied automatically by FastAPI. @@ -375,8 +375,8 @@ def thing_paths(request: Request) -> Mapping[str, str]: :param request: is supplied automatically by FastAPI. - :return: a list of paths pointing to `.Thing` instances. These - URLs will return the :ref:`wot_td` of one `.Thing` each. + :return: a list of paths pointing to `~lt.Thing` instances. These + URLs will return the :ref:`wot_td` of one `~lt.Thing` each. """ # noqa: D403 (URLs is correct capitalisation) return { t: str(request.url_for(f"things.{t}")) diff --git a/src/labthings_fastapi/server/cli.py b/src/labthings_fastapi/server/cli.py index a97c7701..3369b5c3 100644 --- a/src/labthings_fastapi/server/cli.py +++ b/src/labthings_fastapi/server/cli.py @@ -1,4 +1,4 @@ -"""Command-line interface to the `.ThingServer`. +"""Command-line interface to the `~lt.ThingServer`. This module provides a command-line interface that is provided as `labthings-server`. It exposes various functions that may be useful to @@ -130,7 +130,7 @@ def serve_from_cli( This function will parse command line arguments, load configuration, set up a server, and start it. It calls `.parse_args`, - `.config_from_args` and `.ThingServer.from_config` to get a server, then + `.config_from_args` and `~lt.ThingServer.from_config` to get a server, then starts `uvicorn` to serve on the specified host and port. If the ``fallback`` argument is specified, errors that stop the @@ -148,7 +148,7 @@ def serve_from_cli( Things specified can be correctly loaded and instantiated, but does not start `uvicorn`\ . - :return: the `.ThingServer` instance created, if ``dry_run`` is ``True``. + :return: the `~lt.ThingServer` instance created, if ``dry_run`` is ``True``. :raise BaseException: if the server cannot start, and the ``fallback`` option is not specified. diff --git a/src/labthings_fastapi/server/config_model.py b/src/labthings_fastapi/server/config_model.py index 46adf0bc..addb7c9a 100644 --- a/src/labthings_fastapi/server/config_model.py +++ b/src/labthings_fastapi/server/config_model.py @@ -1,6 +1,6 @@ r"""Pydantic models to enable server configuration to be loaded from file. -The models in this module allow `.ThingConfig` dataclasses to be constructed +The models in this module allow `ThingConfig` dataclasses to be constructed from dictionaries or JSON files. They also describe the full server configuration with `.ServerConfigModel`\ . These models are used by the `.cli` module to start servers based on configuration files or strings. @@ -86,7 +86,7 @@ def contain_import_errors(value: Any, handler: ValidatorFunctionWrapHandler) -> # The type: ignore below is a spurious warning about `kwargs`. # see https://github.com/pydantic/pydantic/issues/3125 class ThingConfig(BaseModel): # type: ignore[no-redef] - r"""The information needed to add a `.Thing` to a `.ThingServer`\ .""" + r"""The information needed to add a `~lt.Thing` to a `~lt.ThingServer`\ .""" cls: ThingImportString = Field( validation_alias=AliasChoices("cls", "class"), @@ -127,14 +127,14 @@ class ThingConfig(BaseModel): # type: ignore[no-redef] class ThingServerConfig(BaseModel): - r"""The configuration parameters for a `.ThingServer`\ .""" + r"""The configuration parameters for a `~lt.ThingServer`\ .""" things: ThingsConfig = Field( description=( """A mapping of names to Thing configurations. Each Thing on the server must be given a name, which is the dictionary - key. The value is either the class to be used, or a `.ThingConfig` + key. The value is either the class to be used, or a `ThingConfig` object specifying the class, initial arguments, and other settings. """ ), @@ -152,26 +152,26 @@ def check_things(cls, things: ThingsConfig) -> ThingsConfig: it will accept any Python object. This validator runs `.normalise_thing_config` to check each value is either - a valid `.ThingConfig` or a type or a mapping. If it's a mapping, we - will attempt to make a `.ThingConfig` from it. If it's a `type` we will - create a `.ThingConfig` using that type as the class. We don't check for - `.Thing` subclasses in this module to avoid a dependency loop. + a valid `ThingConfig` or a type or a mapping. If it's a mapping, we + will attempt to make a `ThingConfig` from it. If it's a `type` we will + create a `ThingConfig` using that type as the class. We don't check for + `~lt.Thing` subclasses in this module to avoid a dependency loop. :param things: The validated value of the field. - :return: A copy of the input, with all values converted to `.ThingConfig` + :return: A copy of the input, with all values converted to `ThingConfig` instances. """ return normalise_things_config(things) @property def thing_configs(self) -> Mapping[ThingName, ThingConfig]: - r"""A copy of the ``things`` field where every value is a ``.ThingConfig``\ . + r"""A copy of the ``things`` field where every value is a ``ThingConfig``\ . The field validator on ``things`` already ensures it returns a mapping, but it's not typed strictly, to allow Things to be specified with just a class. - This property returns the list of `.ThingConfig` objects, and is typed strictly. + This property returns the list of `ThingConfig` objects, and is typed strictly. """ return normalise_things_config(self.things) @@ -218,17 +218,17 @@ def thing_configs(self) -> Mapping[ThingName, ThingConfig]: def normalise_things_config(things: ThingsConfig) -> Mapping[ThingName, ThingConfig]: - r"""Ensure every Thing is defined by a `.ThingConfig` object. + r"""Ensure every Thing is defined by a `ThingConfig` object. - Things may be specified either using a `.ThingConfig` object, or just a bare - `.Thing` subclass, if the other parameters are not needed. To simplify code that - uses the configuration, this function wraps bare classes in a `.ThingConfig` so + Things may be specified either using a `ThingConfig` object, or just a bare + `~lt.Thing` subclass, if the other parameters are not needed. To simplify code that + uses the configuration, this function wraps bare classes in a `ThingConfig` so the values are uniformly typed. - :param things: A mapping of names to Things, either classes or `.ThingConfig` + :param things: A mapping of names to Things, either classes or `ThingConfig` objects. - :return: A mapping of names to `.ThingConfig` objects. + :return: A mapping of names to `ThingConfig` objects. :raises ValueError: if a Python object is passed that's neither a `type` nor a `dict`\ . diff --git a/src/labthings_fastapi/testing.py b/src/labthings_fastapi/testing.py index 6c8953e7..fd37209e 100644 --- a/src/labthings_fastapi/testing.py +++ b/src/labthings_fastapi/testing.py @@ -33,7 +33,7 @@ class MockThingServerInterface(ThingServerInterface): r"""A mock class that simulates a ThingServerInterface without the server. - This allows a `.Thing` to be instantiated but not connected to a server. + This allows a `~lt.Thing` to be instantiated but not connected to a server. The methods normally provided by the server are mocked, specifically: * The `name` is set by an argument to `__init__`\ . @@ -68,9 +68,9 @@ def start_async_task_soon( the future object to resolve. Cancelling it may cause errors if you need the return value. - If you need the async code to run, it's best to add the `.Thing` to a + If you need the async code to run, it's best to add the `~lt.Thing` to a `lt.ThingServer` instead. Using a test client will start an event loop - in a background thread, and allow you to use a real `.ThingServerInterface` + in a background thread, and allow you to use a real `~lt.ThingServerInterface` without the overhead of actually starting an HTTP server. :param async_function: the asynchronous function to call. @@ -140,15 +140,15 @@ def create_thing_without_server( mock_all_slots: bool = False, **kwargs: Any, ) -> ThingSubclass: - r"""Create a `.Thing` and supply a mock ThingServerInterface. + r"""Create a `~lt.Thing` and supply a mock ThingServerInterface. - This function is intended for use in testing, where it will enable a `.Thing` + This function is intended for use in testing, where it will enable a `~lt.Thing` to be created without a server, by supplying a `.MockThingServerInterface` - instead of a real `.ThingServerInterface`\ . + instead of a real `~lt.ThingServerInterface`\ . The name of the Thing will be taken from the class name, lowercased. - :param cls: The `.Thing` subclass to instantiate. + :param cls: The `~lt.Thing` subclass to instantiate. :param \*args: positional arguments to ``__init__``. :param settings_folder: The path to the settings folder. A temporary folder is used by default. diff --git a/src/labthings_fastapi/thing.py b/src/labthings_fastapi/thing.py index 401cf1be..cd43a4d0 100644 --- a/src/labthings_fastapi/thing.py +++ b/src/labthings_fastapi/thing.py @@ -1,6 +1,6 @@ """A class to represent hardware or software Things. -The `.Thing` class enables most of the functionality of this library, +The `~lt.Thing` class enables most of the functionality of this library, and is the way in to most of its features. See :ref:`structure` for how it fits with the rest of the library. """ @@ -64,9 +64,9 @@ class Thing: except after errors. You should be safe to ignore them, and just include code that will close down your hardware, which is equivalent to a ``finally:`` block. - * Properties and Actions are defined using decorators: the :deco:`.action` + * Properties and Actions are defined using decorators: the :deco:`lt.action` decorator declares a method to be an action, which will run when it's triggered, - and the :deco:`.property` decorator does the same for a property. + and the :deco:`~lt.property` decorator does the same for a property. Properties may also be defined using dataclass-style syntax, if they do not need getter and setter functions. @@ -76,30 +76,29 @@ class Thing: so it makes sense to set this in a subclass. There are various LabThings methods that you should avoid overriding unless you - know what you are doing: anything not mentioned above that's defined in `.Thing` is - probably best left alone. They may in time be collected together into a single - object to avoid namespace clashes. + know what you are doing: anything not mentioned above that's defined in `Thing` + is probably best left alone. """ title: str """A human-readable description of the Thing""" _thing_server_interface: ThingServerInterface - """Provide access to features of the server that this `.Thing` is attached to.""" + """Provide access to features of the server that this `Thing` is attached to.""" def __init__(self, thing_server_interface: ThingServerInterface) -> None: """Initialise a Thing. The most important function of ``__init__`` is attaching the - thing_server_interface, and setting the path. Note that `.Thing` - instances are usually created by a `.ThingServer` and not instantiated - directly: if you do make a `.Thing` directly, you will need to supply - a `.ThingServerInterface` that is connected to a `.ThingServer` or a + thing_server_interface, and setting the path. Note that `Thing` + instances are usually created by a `~lt.ThingServer` and not instantiated + directly: if you do make a `Thing` directly, you will need to supply + a `~lt.ThingServerInterface` that is connected to a `~lt.ThingServer` or a suitable mock object. :param thing_server_interface: The interface to the server that - is hosting this Thing. It will be supplied when the `.Thing` is - instantiated by the `.ThingServer` or by + is hosting this Thing. It will be supplied when the `Thing` is + instantiated by the `~lt.ThingServer` or by `.create_thing_without_server` which generates a mock interface. """ self._thing_server_interface = thing_server_interface @@ -107,7 +106,7 @@ def __init__(self, thing_server_interface: ThingServerInterface) -> None: @property def path(self) -> str: - """The path at which the `.Thing` is exposed over HTTP.""" + """The path at which the `~lt.Thing` is exposed over HTTP.""" return self._thing_server_interface.path @property @@ -155,11 +154,11 @@ def attach_to_server(self, server: ThingServer) -> None: :param server: The server to attach this Thing to. - Attaching the `.Thing` to a `.ThingServer` allows the `.Thing` to start + Attaching the `~lt.Thing` to a `~lt.ThingServer` allows the `~lt.Thing` to start actions, load its settings from the correct place, and create HTTP endpoints to allow it to be accessed from the HTTP API. - We create HTTP endpoints for all :ref:`wot_affordances` on the `.Thing`, as well + We create HTTP endpoints for all :ref:`wot_affordances` on the `Thing`, as well as any `.EndpointDescriptor` descriptors. """ self.action_manager: ActionManager = server.action_manager @@ -290,9 +289,9 @@ def save_settings(self) -> None: properties: OptionallyBoundDescriptor["Thing", PropertyCollection] = ( OptionallyBoundDescriptor(PropertyCollection) ) - r"""Access to metadata and functions of this `.Thing`\ 's properties. + r"""Access to metadata and functions of this `~lt.Thing`\ 's properties. - `.Thing.properties` is a mapping of names to `.PropertyInfo` objects, which + `~lt.Thing.properties` is a mapping of names to `.PropertyInfo` objects, which allows convenient access to the metadata related to its properties. Note that this includes settings, as they are a subclass of properties. """ @@ -302,16 +301,16 @@ def save_settings(self) -> None: ) r"""Access to settings-related metadata and functions. - `.Thing.settings` is a mapping of names to `.SettingInfo` objects that allows - convenient access to metadata of the settings of this `.Thing`\ . + `~lt.Thing.settings` is a mapping of names to `.SettingInfo` objects that allows + convenient access to metadata of the settings of this `~lt.Thing`\ . """ actions: OptionallyBoundDescriptor["Thing", ActionCollection] = ( OptionallyBoundDescriptor(ActionCollection) ) - r"""Access to metadata for the actions of this `.Thing`\ . + r"""Access to metadata for the actions of this `~lt.Thing`\ . - `.Thing.actions` is a mapping of names to `.ActionInfo` objects that allows + `~lt.Thing.actions` is a mapping of names to `.ActionInfo` objects that allows convenient access to metadata of each action. """ @@ -395,7 +394,7 @@ def thing_description_dict( ) -> dict: r"""Describe this Thing with a Thing Description as a simple dict. - See `.Thing.thing_description`\ . This function converts the + See `~lt.Thing.thing_description`\ . This function converts the return value of that function into a simple dictionary. :param path: the URL pointing to this Thing. diff --git a/src/labthings_fastapi/thing_description/__init__.py b/src/labthings_fastapi/thing_description/__init__.py index 5691c838..e4d5b7c7 100644 --- a/src/labthings_fastapi/thing_description/__init__.py +++ b/src/labthings_fastapi/thing_description/__init__.py @@ -1,7 +1,7 @@ """Thing Description module. This module supports the generation of Thing Descriptions. Currently, the top -level function lives in `.Thing.thing_description`, +level function lives in `~lt.Thing.thing_description`, but most of the supporting code is in this submodule. A Pydantic model implementing the Thing Description is in diff --git a/src/labthings_fastapi/thing_server_interface.py b/src/labthings_fastapi/thing_server_interface.py index b0e46f92..d518c344 100644 --- a/src/labthings_fastapi/thing_server_interface.py +++ b/src/labthings_fastapi/thing_server_interface.py @@ -1,4 +1,4 @@ -r"""Interface between `.Thing` subclasses and the `.ThingServer`\ .""" +r"""Interface between `~lt.Thing` subclasses and the `~lt.ThingServer`\ .""" from __future__ import annotations from concurrent.futures import Future @@ -39,7 +39,7 @@ class ThingServerMissingError(RuntimeError): class ThingServerInterface: r"""An interface for Things to interact with their server. - This is added to every `.Thing` during ``__init__`` and is available + This is added to every `~lt.Thing` during ``__init__`` and is available as ``self._thing_server_interface``\ . """ @@ -54,9 +54,9 @@ def __init__(self, server: ThingServer, name: str) -> None: to mock the server during testing: only functions provided here need be mocked, not the whole functionality of the server. - :param server: the `.ThingServer` instance we're connected to. + :param server: the `~lt.ThingServer` instance we're connected to. This will be retained as a weak reference. - :param name: the name of the `.Thing` instance this interface + :param name: the name of the `~lt.Thing` instance this interface is provided for. """ self._name: str = name @@ -165,12 +165,12 @@ def application_config(self) -> Mapping[str, Any] | None: def get_thing_states(self) -> Mapping[str, Any]: """Retrieve metadata from all Things on the server. - This function will retrieve the `.Thing.thing_state` property from - each `.Thing` on the server, and return it as a dictionary. + This function will retrieve the `~lt.Thing.thing_state` property from + each `~lt.Thing` on the server, and return it as a dictionary. It is intended to make it easy to add metadata to the results of actions, for example to embed in an image. - :return: a dictionary of metadata, with the `.Thing` names as keys. + :return: a dictionary of metadata, with the `~lt.Thing` names as keys. """ return {k: v.thing_state for k, v in self._get_server().things.items()} diff --git a/src/labthings_fastapi/thing_slots.py b/src/labthings_fastapi/thing_slots.py index 4a11d0cb..a69a2311 100644 --- a/src/labthings_fastapi/thing_slots.py +++ b/src/labthings_fastapi/thing_slots.py @@ -2,18 +2,18 @@ It is often desirable for two Things in the same server to be able to communicate. In order to do this in a nicely typed way that is easy to test and inspect, -LabThings-FastAPI provides the `.thing_slot`\ . This allows a `.Thing` -to declare that it depends on another `.Thing` being present, and provides a way for +LabThings-FastAPI provides the `thing_slot`\ . This allows a `~lt.Thing` +to declare that it depends on another `~lt.Thing` being present, and provides a way for the server to automatically connect the two when the server is set up. -Thing connections are set up **after** all the `.Thing` instances are initialised. +Thing connections are set up **after** all the `~lt.Thing` instances are initialised. This means you should not rely on them during initialisation: if you attempt to access a connection before it is provided, it will raise an exception. The advantage of making connections after initialisation is that circular connections are not a problem: Thing `a` may depend on Thing `b` and vice versa. As with properties, thing connections will usually be declared using the function -`.thing_slot` rather than the descriptor directly. This allows them to be +`thing_slot` rather than the descriptor directly. This allows them to be typed and documented on the class, i.e. .. code-block:: python @@ -65,23 +65,23 @@ class ThingSlot( r"""Descriptor that instructs the server to supply other Things. A `.ThingSlot` provides either one or several - `.Thing` instances as a property of a `.Thing`\ . This allows `.Thing`\ s + `~lt.Thing` instances as a property of a `~lt.Thing`\ . This allows `~lt.Thing`\ s to communicate with each other within the server, including accessing attributes that are not exposed over HTTP. - While it is possible to dynamically retrieve a `.Thing` from the `.ThingServer` - this is not recommended: using Thing Connections ensures all the `.Thing` - instances are available before the server starts, reducing the likelihood - of run-time crashes. + While it is possible to dynamically retrieve a `~lt.Thing` from the + `~lt.ThingServer` this is not recommended: using Thing Connections ensures all the + `~lt.Thing` instances are available before the server starts, reducing the + likelihood of run-time crashes. The usual way of creating these connections is the function - `.thing_slot`\ . This class and its subclasses are not usually + `thing_slot`\ . This class and its subclasses are not usually instantiated directly. The type of the `.ThingSlot` attribute is key to its operation. - It should be assigned to an attribute typed either as a `.Thing` subclass, - a mapping of strings to `.Thing` or subclass instances, or an optional - `.Thing` instance: + It should be assigned to an attribute typed either as a `~lt.Thing` subclass, + a mapping of strings to `~lt.Thing` or subclass instances, or an optional + `~lt.Thing` instance: .. code-block:: python @@ -96,7 +96,7 @@ class Example(lt.Thing): # This may evaluate to an `OtherExample` or `None` optional: OtherExample | None = lt.thing_slot("other_thing") - # This evaluates to a mapping of `str` to `.Thing` instances + # This evaluates to a mapping of `str` to `~lt.Thing` instances things: Mapping[str, OtherExample] = lt.thing_slot(["thing_a"]) """ @@ -115,7 +115,7 @@ def __init__( in an error, unless the server has set another value in its configuration. - If the type is a mapping of `str` to `.Thing` the default should be + If the type is a mapping of `str` to `~lt.Thing` the default should be of type `Iterable[str]` (and could be an empty list). """ super().__init__() @@ -126,7 +126,7 @@ def __init__( @property def thing_type(self) -> tuple[type, ...]: - r"""The `.Thing` subclass(es) returned by this connection. + r"""The `~lt.Thing` subclass(es) returned by this connection. A tuple is returned to allow for optional thing connections that are typed as the union of two Thing types. It will work with @@ -174,21 +174,21 @@ def _pick_things( This function is used internally by `.ThingSlot.connect` to choose the Things we return when the `.ThingSlot` is accessed. - :param things: the available `.Thing` instances on the server. + :param things: the available `~lt.Thing` instances on the server. :param target: the name(s) we should connect to, or `None` to set the connection to `None` (if it is optional). A special value is `...` - which will pick the `.Thing` instannce(s) matching this connection's + which will pick the `~lt.Thing` instannce(s) matching this connection's type hint. - :raises ThingSlotError: if the supplied `.Thing` is of the wrong - type, if a sequence is supplied when a single `.Thing` is required, + :raises ThingSlotError: if the supplied `~lt.Thing` is of the wrong + type, if a sequence is supplied when a single `~lt.Thing` is required, or if `None` is supplied and the connection is not optional. :raises TypeError: if ``target`` is not one of the allowed types. `KeyError` will also be raised if names specified in ``target`` do not exist in ``things``\ . - :return: a list of `.Thing` instances to supply in response to ``__get__``\ . + :return: a list of `~lt.Thing` instances to supply in response to ``__get__``\ . """ if target is None: return [] @@ -217,19 +217,19 @@ def connect( things: "Mapping[str, Thing]", target: str | Iterable[str] | None | EllipsisType = ..., ) -> None: - r"""Find the `.Thing`\ (s) we should supply when accessed. + r"""Find the `~lt.Thing`\ (s) we should supply when accessed. This method sets up a ThingSlot on ``host_thing`` by finding the - `.Thing` instance(s) it should supply when its ``__get__`` method is + `~lt.Thing` instance(s) it should supply when its ``__get__`` method is called. The logic for determining this is: - * If ``target`` is specified, we look for the specified `.Thing`\ (s). + * If ``target`` is specified, we look for the specified `~lt.Thing`\ (s). ``None`` means we should return ``None`` - that's only allowed if the type hint permits it. * If ``target`` is not specified or is ``...`` we use the default value set when the connection was defined. * If the default value was ``...`` and no target was specified, we will - attempt to find the `.Thing` by type. Most of the time, this is the + attempt to find the `~lt.Thing` by type. Most of the time, this is the desired behaviour. If the type of this connection is a ``Mapping``\ , ``target`` should be @@ -243,15 +243,15 @@ def connect( is set as ``None``\ , then an error will be raised. ``None`` will only be returned at runtime if it is permitted by the type hint. - :param host: the `.Thing` on which the connection is defined. - :param things: the available `.Thing` instances on the server. + :param host: the `~lt.Thing` on which the connection is defined. + :param things: the available `~lt.Thing` instances on the server. :param target: the name(s) we should connect to, or `None` to set the connection to `None` (if it is optional). The default is `...` which will use the default that was set when this `.ThingSlot` was defined. - :raises ThingSlotError: if the supplied `.Thing` is of the wrong - type, if a sequence is supplied when a single `.Thing` is required, + :raises ThingSlotError: if the supplied `~lt.Thing` is of the wrong + type, if a sequence is supplied when a single `~lt.Thing` is required, or if `None` is supplied and the connection is not optional. """ used_target = self.default if target is ... else target @@ -296,11 +296,11 @@ def connect( raise ThingSlotError(msg) from e def instance_get(self, obj: "Thing") -> ConnectedThings: - r"""Supply the connected `.Thing`\ (s). + r"""Supply the connected `~lt.Thing`\ (s). - :param obj: The `.Thing` on which the connection is defined. + :param obj: The `~lt.Thing` on which the connection is defined. - :return: the `.Thing` instance(s) connected. + :return: the `~lt.Thing` instance(s) connected. :raises ThingNotConnectedError: if the ThingSlot has not yet been set up. :raises ReferenceError: if a connected Thing no longer exists (should not @@ -339,14 +339,14 @@ def instance_get(self, obj: "Thing") -> ConnectedThings: def thing_slot(default: str | Iterable[str] | None | EllipsisType = ...) -> Any: - r"""Declare a connection to another `.Thing` in the same server. + r"""Declare a connection to another `~lt.Thing` in the same server. ``lt.thing_slot`` marks a class attribute as a connection to another - `.Thing` on the same server. This will be automatically supplied when the + `~lt.Thing` on the same server. This will be automatically supplied when the server is started, based on the type hint and default value. - In keeping with `.property` and `.setting`, the type of the attribute should - be the type of the connected `.Thing`\ . For example: + In keeping with `~lt.property` and `~lt.setting`, the type of the attribute should + be the type of the connected `~lt.Thing`\ . For example: .. code-block:: python @@ -368,12 +368,12 @@ class ThingB(lt.Thing): The type hint of a Thing Connection should be one of the following: - * A `.Thing` subclass. An instance of this subclass will be returned when + * A `~lt.Thing` subclass. An instance of this subclass will be returned when the attribute is accessed. - * An optional `.Thing` subclass (e.g. ``MyThing | None``). This will either + * An optional `~lt.Thing` subclass (e.g. ``MyThing | None``). This will either return a ``MyThing`` instance or ``None``\ . - * A mapping of `str` to `.Thing` (e.g. ``Mapping[str, MyThing]``). This will - return a mapping of `.Thing` names to `.Thing` instances. The mapping + * A mapping of `str` to `~lt.Thing` (e.g. ``Mapping[str, MyThing]``). This will + return a mapping of `~lt.Thing` names to `~lt.Thing` instances. The mapping may be empty. Example: @@ -405,26 +405,26 @@ def show_connections(self) -> str: The example above is very contrived, but shows how to apply the different types. If no default value is supplied, and no value is configured for the connection, - the server will attempt to find a `.Thing` that - matches the specified type when the server is started. If no matching `.Thing` + the server will attempt to find a `~lt.Thing` that + matches the specified type when the server is started. If no matching `~lt.Thing` instances are found, the descriptor will return ``None`` or an empty mapping. If that is not allowed by the type hint, the server will fail to start with an error. - The default value may be a string specifying a `.Thing` name, or a sequence of + The default value may be a string specifying a `~lt.Thing` name, or a sequence of strings (for connections that return mappings). In those cases, the relevant - `.Thing` will be returned from the server. If a name is given that either - doesn't correspond to a `.Thing` on the server, or is a `.Thing` that doesn't + `~lt.Thing` will be returned from the server. If a name is given that either + doesn't correspond to a `~lt.Thing` on the server, or is a `~lt.Thing` that doesn't match the type of this connection, the server will fail to start with an error. The default may also be ``None`` which is appropriate when the type is optional or a mapping. If the type is - a `.Thing` subclass, a default value of ``None`` forces the connection to be + a `~lt.Thing` subclass, a default value of ``None`` forces the connection to be specified in configuration. :param default: The name(s) of the Thing(s) that will be connected by default. If the default is omitted or set to ``...`` the server will attempt to find - a matching `.Thing` instance (or instances). A default value of `None` is + a matching `~lt.Thing` instance (or instances). A default value of `None` is allowed if the connection is type hinted as optional. :return: A `.ThingSlot` descriptor. @@ -433,7 +433,7 @@ def show_connections(self) -> str: In the example above, using `.ThingSlot` directly would assign an object with type ``ThingSlot[ThingA]`` to the attribute ``thing_a``, which is typed as ``ThingA``\ . This would cause a type error. Using - `.thing_slot` suppresses this error, as its return type is a`Any``\ . + `thing_slot` suppresses this error, as its return type is a`Any``\ . The use of ``Any`` or an alternative type-checking exemption seems to be inevitable when implementing descriptors that are typed via attribute annotations, diff --git a/src/labthings_fastapi/utilities/__init__.py b/src/labthings_fastapi/utilities/__init__.py index f26fddc5..51515601 100644 --- a/src/labthings_fastapi/utilities/__init__.py +++ b/src/labthings_fastapi/utilities/__init__.py @@ -31,7 +31,7 @@ def class_attributes(obj: Any) -> Iterable[tuple[str, Any]]: It is used to obtain the various descriptors used to represent properties and actions. It calls `.attributes` on ``obj.__class__``. - :param obj: The instance, usually a `.Thing` instance. + :param obj: The instance, usually a `~lt.Thing` instance. :yield: tuples of ``(name, value)`` giving each attribute of the class. """ @@ -59,17 +59,17 @@ def attributes(cls: Any) -> Iterable[tuple[str, Any]]: @dataclass(config=ConfigDict(arbitrary_types_allowed=True)) class LabThingsObjectData: - r"""Data used by LabThings, stored on each `.Thing`. + r"""Data used by LabThings, stored on each `~lt.Thing`. This `pydantic.dataclass` groups together some properties used by LabThings descriptors, to avoid cluttering the namespace of the - `.Thing` subclass on which they are defined. + `~lt.Thing` subclass on which they are defined. """ property_observers: Dict[str, WeakSet] = Field(default_factory=dict) r"""The observers added to each property. - Keys are property names, values are weak sets used by `.DataProperty`\ . + Keys are property names, values are weak sets used by `~lt.DataProperty`\ . """ action_observers: Dict[str, WeakSet] = Field(default_factory=dict) r"""The observers added to each action. @@ -83,9 +83,9 @@ def labthings_data(obj: Thing) -> LabThingsObjectData: """Get (or create) a dictionary for LabThings properties. Ensure there is a `.LabThingsObjectData` dataclass attached to - a particular `.Thing`, and return it. + a particular `~lt.Thing`, and return it. - :param obj: The `.Thing` we are looking for the dataclass on. + :param obj: The `~lt.Thing` we are looking for the dataclass on. :return: a `.LabThingsObjectData` instance attached to ``obj``. """ diff --git a/src/labthings_fastapi/utilities/introspection.py b/src/labthings_fastapi/utilities/introspection.py index daf83d11..cdd081ce 100644 --- a/src/labthings_fastapi/utilities/introspection.py +++ b/src/labthings_fastapi/utilities/introspection.py @@ -138,12 +138,12 @@ def fastapi_dependency_params(func: Callable) -> Sequence[Parameter]: We give special treatment to dependency parameters, as they must not appear in the input model, and they must be supplied by the `.DirectThingClient` wrapper to make the signature identical to that - of the `.ThingClient` over HTTP. + of the `~lt.ThingClient` over HTTP. .. note:: Path and query parameters are ignored. These should not be used as action - parameters, and will most likely raise an error when the `.Thing` is + parameters, and will most likely raise an error when the `~lt.Thing` is added to FastAPI. :param func: a function to inspect. diff --git a/src/labthings_fastapi/websockets.py b/src/labthings_fastapi/websockets.py index a206a4f7..2d3136f2 100644 --- a/src/labthings_fastapi/websockets.py +++ b/src/labthings_fastapi/websockets.py @@ -105,8 +105,8 @@ async def process_messages_from_websocket( :param send_stream: an `anyio.abc.ObjectSendStream` that we use to register for events, i.e. data sent to that stream will be sent through this websocket, by `.relay_notifications_to_websocket`\ . - :param thing: the `.Thing` we are attached to. The websocket is specific to - one `.Thing`, and this is it. + :param thing: the `~lt.Thing` we are attached to. The websocket is specific to + one `~lt.Thing`, and this is it. """ while True: try: @@ -133,11 +133,11 @@ async def process_messages_from_websocket( async def websocket_endpoint(thing: Thing, websocket: WebSocket) -> None: r"""Handle communication to a client via websocket. - This function handles a websocket connection to a `.Thing`\ 's websocket + This function handles a websocket connection to a `~lt.Thing`\ 's websocket endpoint. It can add observers to properties and actions, and will forward notifications from the property or action back to the websocket. - :param thing: the `.Thing` the websocket is attached to. + :param thing: the `~lt.Thing` the websocket is attached to. :param websocket: the web socket that has been created. """ await websocket.accept() diff --git a/tests/test_blob_output.py b/tests/test_blob_output.py index 672b273d..60144a3e 100644 --- a/tests/test_blob_output.py +++ b/tests/test_blob_output.py @@ -315,7 +315,7 @@ def test_blob_output_direct(): def test_blob_output_inserver(client): - """Test that the blob output works the same when used via a `.thing_slot`.""" + """Test that the blob output works the same when used via a `~lt.thing_slot`.""" tc = lt.ThingClient.from_url("/thing_two/", client=client) output = tc.check_both() assert output is True diff --git a/tests/test_property.py b/tests/test_property.py index bce66c76..5aed51ab 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -263,7 +263,7 @@ def test_decorator_exception(): r"""Check decorators work as expected when the setter has a different name. This is done to satisfy ``mypy`` and more information is in the - documentation for `.property`\ , `.DescriptorAddedToClassTwiceError` + documentation for `~lt.property`\ , `.DescriptorAddedToClassTwiceError` and `.FunctionalProperty.__set_name__`\ . """ # The exception should be specific - a simple double assignment is diff --git a/tests/test_settings.py b/tests/test_settings.py index cc46b7d3..eb7ac54e 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -26,7 +26,7 @@ class MyModel(BaseModel): class ThingWithSettings(lt.Thing): - """A test `.Thing` with some settings and actions.""" + """A test `~lt.Thing` with some settings and actions.""" def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) diff --git a/typing_tests/thing_properties.py b/typing_tests/thing_properties.py index 5dbadb5b..73e86d4e 100644 --- a/typing_tests/thing_properties.py +++ b/typing_tests/thing_properties.py @@ -129,7 +129,7 @@ class TestPropertyDefaultsMatch(lt.Thing): class TestExplicitDescriptor(lt.Thing): r"""A Thing that checks our explicit descriptor type hints are working. - This tests `.DataProperty` descriptors work as intended when used directly, + This tests `~lt.DataProperty` descriptors work as intended when used directly, rather than via ``lt.property``\ . ``lt.property`` has a "white lie" on its return type, which makes it From a4ec548cc040653810b97a3ba6ebde2ebd1a14f7 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Thu, 2 Apr 2026 12:32:21 +0100 Subject: [PATCH 2/4] Add a CI check for the public API This builds the docs in CI, and then checks the `lt` symbols against our top-level `__all__`. Hopefully, doing so will stop things getting out of date. --- .github/workflows/docs.yml | 30 ++++++++++++++++++++++++++++++ dev-requirements.txt | 8 +++++++- docs/check_public_api.py | 32 ++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/check_public_api.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..41a40cd2 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,30 @@ +name: Documentation + +on: + - pull_request + +jobs: + build-docs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install Dependencies + run: pip install -e . -r dev-requirements.txt + + - name: Print installed packages + run: pip freeze + + - name: Build docs with Sphinx + working-directory: ./docs/ + run: make html + + - name: Check public API docs are complete + working-directory: ./docs/ + run: python check_public_api.py diff --git a/dev-requirements.txt b/dev-requirements.txt index be8de496..19e5ef39 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -22,6 +22,7 @@ attrs==25.3.0 # via # jsonschema # referencing + # sphobjinv autodocsumm==0.2.14 # via sphinx-toolbox babel==2.17.0 @@ -37,6 +38,7 @@ certifi==2025.7.14 # requests # sentry-sdk # sphinx-prompt + # sphobjinv charset-normalizer==3.4.2 # via requests click==8.2.1 @@ -140,7 +142,9 @@ jinja2==3.1.6 # sphinx-autoapi # sphinx-jinja2-compat jsonschema==4.24.1 - # via labthings-fastapi (pyproject.toml) + # via + # labthings-fastapi (pyproject.toml) + # sphobjinv jsonschema-specifications==2025.4.1 # via jsonschema markdown-it-py==3.0.0 @@ -309,6 +313,8 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx +sphobjinv==2.4 + # via labthings-fastapi (pyproject.toml) starlette==0.47.1 # via fastapi tabulate==0.9.0 diff --git a/docs/check_public_api.py b/docs/check_public_api.py new file mode 100644 index 00000000..d030e4c2 --- /dev/null +++ b/docs/check_public_api.py @@ -0,0 +1,32 @@ +"""Check public API for completeness. + +This module loads the intersphinx inventory, and checks that all the symbols from the +top-level module are present. This should prevent that page from going out of date. +""" + +import sphobjinv as soi + +import labthings_fastapi as lt + +if __name__ == "__main__": + inventory = soi.Inventory("build/html/objects.inv") + + if not inventory.project == "labthings-fastapi": + raise AssertionError(f"The inventory is for {inventory.project} not LabThings!") + + published_lt_namespace = {} + + for object in inventory.objects: + if object.name.startswith("lt.") and object.domain == "py": + published_lt_namespace[object.name] = object + + missing = [] + + for name in lt.__all__: + if f"lt.{name}" not in published_lt_namespace: + missing.append(name) + + if missing: + msg = "Failure: the following symbols are missing from the `lt` namespace: \n\n" + msg += "\n".join(missing) + raise AssertionError(msg) diff --git a/pyproject.toml b/pyproject.toml index 36d88168..6ddac828 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dev = [ "sphinx>=7.2", "sphinx-autoapi", "sphinx-toolbox", + "sphobjinv", "tomli; python_version < '3.11'", "codespell", ] From cdc6e38543ad0f654b761f2377ec870ca5f64697 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Thu, 2 Apr 2026 12:43:15 +0100 Subject: [PATCH 3/4] Fix a problem generating fully qualified name for FEATURE_FLAGS FEATURE_FLAGS is an object not a class, which confuses my code that deduplicates re-exported symbols. I have added FEATURE_FLAGS as a special case. --- docs/source/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 348203c9..8576b84a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -78,6 +78,7 @@ "labthings_fastapi.actions.ActionManager", "labthings_fastapi.descriptors.endpoint.EndpointDescriptor", "labthings_fastapi.utilities.introspection.EmptyObject", + "labthings_fastapi.feature_flags.FEATURE_FLAGS", } # Everything in `labthings_fastapi` is documented elsewhere, so we @@ -86,7 +87,7 @@ canonical_fq_names.update( f"{obj.__module__}.{obj.__qualname__}" for obj in top_level_objects - if not inspect.ismodule(obj) + if not inspect.ismodule(obj) and obj is not lt.FEATURE_FLAGS ) From 079566f26649517440203808474471d1e1085850 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Thu, 2 Apr 2026 13:29:03 +0100 Subject: [PATCH 4/4] Rename and complete public API documentation. This adds in the remaining symbols and renames "quick reference" to "public API". Renaming the full API docs required me to pull in templates for autoapi. This might go away in the future if we transition fully to autodoc. I've had to fix a couple of references too. --- docs/autoapi_templates/index.rst | 13 ++ docs/autoapi_templates/python/attribute.rst | 1 + docs/autoapi_templates/python/class.rst | 104 ++++++++++++ docs/autoapi_templates/python/data.rst | 45 +++++ docs/autoapi_templates/python/exception.rst | 1 + docs/autoapi_templates/python/function.rst | 21 +++ docs/autoapi_templates/python/method.rst | 21 +++ docs/autoapi_templates/python/module.rst | 156 ++++++++++++++++++ docs/autoapi_templates/python/package.rst | 1 + docs/autoapi_templates/python/property.rst | 21 +++ docs/check_public_api.py | 2 + docs/source/conf.py | 1 + docs/source/index.rst | 2 +- .../{quick_reference.rst => public_api.rst} | 28 +++- docs/source/updates_and_features.rst | 2 +- src/labthings_fastapi/properties.py | 2 +- src/labthings_fastapi/server/__init__.py | 2 +- src/labthings_fastapi/server/config_model.py | 5 +- 18 files changed, 420 insertions(+), 8 deletions(-) create mode 100644 docs/autoapi_templates/index.rst create mode 100644 docs/autoapi_templates/python/attribute.rst create mode 100644 docs/autoapi_templates/python/class.rst create mode 100644 docs/autoapi_templates/python/data.rst create mode 100644 docs/autoapi_templates/python/exception.rst create mode 100644 docs/autoapi_templates/python/function.rst create mode 100644 docs/autoapi_templates/python/method.rst create mode 100644 docs/autoapi_templates/python/module.rst create mode 100644 docs/autoapi_templates/python/package.rst create mode 100644 docs/autoapi_templates/python/property.rst rename docs/source/{quick_reference.rst => public_api.rst} (93%) diff --git a/docs/autoapi_templates/index.rst b/docs/autoapi_templates/index.rst new file mode 100644 index 00000000..4f4c5a33 --- /dev/null +++ b/docs/autoapi_templates/index.rst @@ -0,0 +1,13 @@ +Internal API Reference +====================== + +This page contains auto-generated API reference documentation [#f1]_. Most users should find the :ref:`public_api` more useful. + +.. toctree:: + :titlesonly: + + {% for page in pages|selectattr("is_top_level_object") %} + {{ page.include_path }} + {% endfor %} + +.. [#f1] Created with `sphinx-autoapi `_ diff --git a/docs/autoapi_templates/python/attribute.rst b/docs/autoapi_templates/python/attribute.rst new file mode 100644 index 00000000..ebaba555 --- /dev/null +++ b/docs/autoapi_templates/python/attribute.rst @@ -0,0 +1 @@ +{% extends "python/data.rst" %} diff --git a/docs/autoapi_templates/python/class.rst b/docs/autoapi_templates/python/class.rst new file mode 100644 index 00000000..379f83ac --- /dev/null +++ b/docs/autoapi_templates/python/class.rst @@ -0,0 +1,104 @@ +{% if obj.display %} + {% if is_own_page %} +{{ obj.id }} +{{ "=" * obj.id | length }} + + {% endif %} + {% set visible_children = obj.children|selectattr("display")|list %} + {% set own_page_children = visible_children|selectattr("type", "in", own_page_types)|list %} + {% if is_own_page and own_page_children %} +.. toctree:: + :hidden: + + {% for child in own_page_children %} + {{ child.include_path }} + {% endfor %} + + {% endif %} +.. py:{{ obj.type }}:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}{% if obj.args %}({{ obj.args }}){% endif %} + + {% for (args, return_annotation) in obj.overloads %} + {{ " " * (obj.type | length) }} {{ obj.short_name }}{% if args %}({{ args }}){% endif %} + + {% endfor %} + {% if obj.bases %} + {% if "show-inheritance" in autoapi_options %} + + Bases: {% for base in obj.bases %}{{ base|link_objs }}{% if not loop.last %}, {% endif %}{% endfor %} + {% endif %} + + + {% if "show-inheritance-diagram" in autoapi_options and obj.bases != ["object"] %} + .. autoapi-inheritance-diagram:: {{ obj.obj["full_name"] }} + :parts: 1 + {% if "private-members" in autoapi_options %} + :private-bases: + {% endif %} + + {% endif %} + {% endif %} + {% if obj.docstring %} + + {{ obj.docstring|indent(3) }} + {% endif %} + {% for obj_item in visible_children %} + {% if obj_item.type not in own_page_types %} + + {{ obj_item.render()|indent(3) }} + {% endif %} + {% endfor %} + {% if is_own_page and own_page_children %} + {% set visible_attributes = own_page_children|selectattr("type", "equalto", "attribute")|list %} + {% if visible_attributes %} +Attributes +---------- + +.. autoapisummary:: + + {% for attribute in visible_attributes %} + {{ attribute.id }} + {% endfor %} + + + {% endif %} + {% set visible_exceptions = own_page_children|selectattr("type", "equalto", "exception")|list %} + {% if visible_exceptions %} +Exceptions +---------- + +.. autoapisummary:: + + {% for exception in visible_exceptions %} + {{ exception.id }} + {% endfor %} + + + {% endif %} + {% set visible_classes = own_page_children|selectattr("type", "equalto", "class")|list %} + {% if visible_classes %} +Classes +------- + +.. autoapisummary:: + + {% for klass in visible_classes %} + {{ klass.id }} + {% endfor %} + + + {% endif %} + {% set visible_methods = own_page_children|selectattr("type", "equalto", "method")|list %} + {% if visible_methods %} +Methods +------- + +.. autoapisummary:: + + {% for method in visible_methods %} + {{ method.id }} + {% endfor %} + + + {% endif %} + {% endif %} +{% endif %} diff --git a/docs/autoapi_templates/python/data.rst b/docs/autoapi_templates/python/data.rst new file mode 100644 index 00000000..111722e0 --- /dev/null +++ b/docs/autoapi_templates/python/data.rst @@ -0,0 +1,45 @@ +{% if obj.display %} + {% if is_own_page %} +{{ obj.id }} +{{ "=" * obj.id | length }} + + {% endif %} +.. py:{% if obj.is_type_alias() %}type{% else %}{{ obj.type }}{% endif %}:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.name }}{% endif %} + {% if obj.is_type_alias() %} + {% if obj.value %} + + :canonical: {{ obj.value }} + {% endif %} + {% else %} + {% if obj.annotation is not none %} + + :type: {% if obj.annotation %} {{ obj.annotation }}{% endif %} + {% endif %} + {% if obj.value is not none %} + + {% if obj.value.splitlines()|count > 1 %} + :value: Multiline-String + + .. raw:: html + +
Show Value + + .. code-block:: python + + {{ obj.value|indent(width=6,blank=true) }} + + .. raw:: html + +
+ + {% else %} + :value: {{ obj.value|truncate(100) }} + {% endif %} + {% endif %} + {% endif %} + + {% if obj.docstring %} + + {{ obj.docstring|indent(3) }} + {% endif %} +{% endif %} diff --git a/docs/autoapi_templates/python/exception.rst b/docs/autoapi_templates/python/exception.rst new file mode 100644 index 00000000..92f3d38f --- /dev/null +++ b/docs/autoapi_templates/python/exception.rst @@ -0,0 +1 @@ +{% extends "python/class.rst" %} diff --git a/docs/autoapi_templates/python/function.rst b/docs/autoapi_templates/python/function.rst new file mode 100644 index 00000000..5dee5aa0 --- /dev/null +++ b/docs/autoapi_templates/python/function.rst @@ -0,0 +1,21 @@ +{% if obj.display %} + {% if is_own_page %} +{{ obj.id }} +{{ "=" * obj.id | length }} + + {% endif %} +.. py:function:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} + {% for (args, return_annotation) in obj.overloads %} + + {%+ if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} + {% endfor %} + {% for property in obj.properties %} + + :{{ property }}: + {% endfor %} + + {% if obj.docstring %} + + {{ obj.docstring|indent(3) }} + {% endif %} +{% endif %} diff --git a/docs/autoapi_templates/python/method.rst b/docs/autoapi_templates/python/method.rst new file mode 100644 index 00000000..12d42de6 --- /dev/null +++ b/docs/autoapi_templates/python/method.rst @@ -0,0 +1,21 @@ +{% if obj.display %} + {% if is_own_page %} +{{ obj.id }} +{{ "=" * obj.id | length }} + + {% endif %} +.. py:method:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} + {% for (args, return_annotation) in obj.overloads %} + + {%+ if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} + {% endfor %} + {% for property in obj.properties %} + + :{{ property }}: + {% endfor %} + + {% if obj.docstring %} + + {{ obj.docstring|indent(3) }} + {% endif %} +{% endif %} diff --git a/docs/autoapi_templates/python/module.rst b/docs/autoapi_templates/python/module.rst new file mode 100644 index 00000000..53cc65d2 --- /dev/null +++ b/docs/autoapi_templates/python/module.rst @@ -0,0 +1,156 @@ +{% if obj.display %} + {% if is_own_page %} +{{ obj.id }} +{{ "=" * obj.id|length }} + +.. py:module:: {{ obj.name }} + + {% if obj.docstring %} +.. autoapi-nested-parse:: + + {{ obj.docstring|indent(3) }} + + {% endif %} + + {% block submodules %} + {% set visible_subpackages = obj.subpackages|selectattr("display")|list %} + {% set visible_submodules = obj.submodules|selectattr("display")|list %} + {% set visible_submodules = (visible_subpackages + visible_submodules)|sort %} + {% if visible_submodules %} +Submodules +---------- + +.. toctree:: + :maxdepth: 1 + + {% for submodule in visible_submodules %} + {{ submodule.include_path }} + {% endfor %} + + + {% endif %} + {% endblock %} + {% block content %} + {% set visible_children = obj.children|selectattr("display")|list %} + {% if visible_children %} + {% set visible_attributes = visible_children|selectattr("type", "equalto", "data")|list %} + {% if visible_attributes %} + {% if "attribute" in own_page_types or "show-module-summary" in autoapi_options %} +Attributes +---------- + + {% if "attribute" in own_page_types %} +.. toctree:: + :hidden: + + {% for attribute in visible_attributes %} + {{ attribute.include_path }} + {% endfor %} + + {% endif %} +.. autoapisummary:: + + {% for attribute in visible_attributes %} + {{ attribute.id }} + {% endfor %} + {% endif %} + + + {% endif %} + {% set visible_exceptions = visible_children|selectattr("type", "equalto", "exception")|list %} + {% if visible_exceptions %} + {% if "exception" in own_page_types or "show-module-summary" in autoapi_options %} +Exceptions +---------- + + {% if "exception" in own_page_types %} +.. toctree:: + :hidden: + + {% for exception in visible_exceptions %} + {{ exception.include_path }} + {% endfor %} + + {% endif %} +.. autoapisummary:: + + {% for exception in visible_exceptions %} + {{ exception.id }} + {% endfor %} + {% endif %} + + + {% endif %} + {% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %} + {% if visible_classes %} + {% if "class" in own_page_types or "show-module-summary" in autoapi_options %} +Classes +------- + + {% if "class" in own_page_types %} +.. toctree:: + :hidden: + + {% for klass in visible_classes %} + {{ klass.include_path }} + {% endfor %} + + {% endif %} +.. autoapisummary:: + + {% for klass in visible_classes %} + {{ klass.id }} + {% endfor %} + {% endif %} + + + {% endif %} + {% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %} + {% if visible_functions %} + {% if "function" in own_page_types or "show-module-summary" in autoapi_options %} +Functions +--------- + + {% if "function" in own_page_types %} +.. toctree:: + :hidden: + + {% for function in visible_functions %} + {{ function.include_path }} + {% endfor %} + + {% endif %} +.. autoapisummary:: + + {% for function in visible_functions %} + {{ function.id }} + {% endfor %} + {% endif %} + + + {% endif %} + {% set this_page_children = visible_children|rejectattr("type", "in", own_page_types)|list %} + {% if this_page_children %} +{{ obj.type|title }} Contents +{{ "-" * obj.type|length }}--------- + + {% for obj_item in this_page_children %} +{{ obj_item.render()|indent(0) }} + {% endfor %} + {% endif %} + {% endif %} + {% endblock %} + {% else %} +.. py:module:: {{ obj.name }} + + {% if obj.docstring %} + .. autoapi-nested-parse:: + + {{ obj.docstring|indent(6) }} + + {% endif %} + {% for obj_item in visible_children %} + {{ obj_item.render()|indent(3) }} + {% endfor %} + {% endif %} +{% endif %} diff --git a/docs/autoapi_templates/python/package.rst b/docs/autoapi_templates/python/package.rst new file mode 100644 index 00000000..fb9a6496 --- /dev/null +++ b/docs/autoapi_templates/python/package.rst @@ -0,0 +1 @@ +{% extends "python/module.rst" %} diff --git a/docs/autoapi_templates/python/property.rst b/docs/autoapi_templates/python/property.rst new file mode 100644 index 00000000..91653116 --- /dev/null +++ b/docs/autoapi_templates/python/property.rst @@ -0,0 +1,21 @@ +{% if obj.display %} + {% if is_own_page %} +{{ obj.id }} +{{ "=" * obj.id | length }} + + {% endif %} +.. py:property:: {% if is_own_page %}{{ obj.id}}{% else %}{{ obj.short_name }}{% endif %} + {% if obj.annotation %} + + :type: {{ obj.annotation }} + {% endif %} + {% for property in obj.properties %} + + :{{ property }}: + {% endfor %} + + {% if obj.docstring %} + + {{ obj.docstring|indent(3) }} + {% endif %} +{% endif %} diff --git a/docs/check_public_api.py b/docs/check_public_api.py index d030e4c2..35851d07 100644 --- a/docs/check_public_api.py +++ b/docs/check_public_api.py @@ -30,3 +30,5 @@ msg = "Failure: the following symbols are missing from the `lt` namespace: \n\n" msg += "\n".join(missing) raise AssertionError(msg) + + print("All symbols in `lt.__all__` appear in the Sphinx documentation.") diff --git a/docs/source/conf.py b/docs/source/conf.py index 8576b84a..41e72bc3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -42,6 +42,7 @@ autoapi_generate_api_docs = True autoapi_keep_files = True autoapi_python_class_content = "both" +autoapi_template_dir = "../autoapi_templates" # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/docs/source/index.rst b/docs/source/index.rst index 2c6f4663..a7a1001f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -21,7 +21,7 @@ Documentation for LabThings-FastAPI wot_core_concepts.rst removed_features.rst - quick_reference.rst + public_api.rst autoapi/index developer_notes/index.rst diff --git a/docs/source/quick_reference.rst b/docs/source/public_api.rst similarity index 93% rename from docs/source/quick_reference.rst rename to docs/source/public_api.rst index 14221423..d66c311b 100644 --- a/docs/source/quick_reference.rst +++ b/docs/source/public_api.rst @@ -1,7 +1,8 @@ .. _quickref: +.. _public_api: -Quick Reference API Documentation -================================= +Public API Documentation +======================== This page summarises the parts of the LabThings API that should be most frequently used by people writing `lt.Thing` subclasses. It doesn't list options exhaustively: the full :doc:`API documentation ` does that if extra detail is needed. @@ -453,3 +454,26 @@ This page summarises the parts of the LabThings API that should be most frequent Run the target function, with invocation ID set in the context variable. +.. py:class:: DataProperty + + This is an alias of `labthings_fastapi.properties.DataProperty` but is not usually used: consider using `property` instead. + +.. py:class:: DataSetting + + This is an alias of `labthings_fastapi.properties.DataSetting` but is not usually used: consider using `property` instead. + +.. py:attribute:: FEATURE_FLAGS + + Feature flags are used to control optional features of LabThings. Setting the attributes of `FEATURE_FLAGS` enables or disables a feature. It is an instance of the dataclass `labthings_fastapi.feature_flags.LabThingsFeatureFlags` and the various available options are described there. + +.. py:attribute:: outputs + + This is an alias for `labthings_fastapi.outputs` and contains, for example `MJPEGStream`\ . + +.. py:attribute:: blob + + This module implements :ref:`blobs`\ , which is the intended way to return files and binary data from LabThings. It is an alias for `labthings_fastapi.outputs.blob`\ . + +.. py:attribute:: cli + + The CLI module implements command-line functions, and is where the command-line script ``labthings-server`` is implemented. It is an alias for `labthings_fastapi.server.cli`\ . diff --git a/docs/source/updates_and_features.rst b/docs/source/updates_and_features.rst index 026e6035..b674dd66 100644 --- a/docs/source/updates_and_features.rst +++ b/docs/source/updates_and_features.rst @@ -3,7 +3,7 @@ Optional Features and updates ============================= -LabThings allows some features to be turned on and off globally, using the `.FEATURE_FLAGS` object. +LabThings allows some features to be turned on and off globally, using the `lt.FEATURE_FLAGS` object. This was introduced as a way to smooth the upgrade process for downstream projects, meaning that when a new version of LabThings is released, they need not adopt all the new features at once. Typically, your application will set the feature flags once, just after importing LabThings. For example, to validate properties when they are written to in Python, we would do: diff --git a/src/labthings_fastapi/properties.py b/src/labthings_fastapi/properties.py index 152cdce7..bc73d0d5 100644 --- a/src/labthings_fastapi/properties.py +++ b/src/labthings_fastapi/properties.py @@ -402,7 +402,7 @@ def _validate_constraints(constraints: Mapping[str, Any]) -> FieldConstraints: ) from e @builtins.property - def constraints(self) -> FieldConstraints: # noqa[DOC201] + def constraints(self) -> FieldConstraints: # noqa: DOC201 """Validation constraints applied to this property. This mapping contains keyword arguments that will be passed to diff --git a/src/labthings_fastapi/server/__init__.py b/src/labthings_fastapi/server/__init__.py index 3265bf6c..9483baa5 100644 --- a/src/labthings_fastapi/server/__init__.py +++ b/src/labthings_fastapi/server/__init__.py @@ -87,7 +87,7 @@ def __init__( :param api_prefix: A prefix for all API routes. This must either be empty, or start with a slash and not end with a slash. :param application_config: A mapping containing custom configuration for the - application. This is not processed by LabThings. Each `~lt.Thing` can + application. This is not processed by LabThings. Each `~lt.Thing` can access this via the Thing-Server interface. :param debug: If ``True``, set the log level for `~lt.Thing` instances to DEBUG. diff --git a/src/labthings_fastapi/server/config_model.py b/src/labthings_fastapi/server/config_model.py index addb7c9a..679a0d3a 100644 --- a/src/labthings_fastapi/server/config_model.py +++ b/src/labthings_fastapi/server/config_model.py @@ -2,8 +2,9 @@ The models in this module allow `ThingConfig` dataclasses to be constructed from dictionaries or JSON files. They also describe the full server configuration -with `.ServerConfigModel`\ . These models are used by the `.cli` module to -start servers based on configuration files or strings. +with `.ServerConfigModel`\ . These models are used by the +`labthings_fastapi.server.cli` module to start servers based on configuration +files or strings. """ from pydantic import (