From 8877cd4a64c925739e0e3dfe86b0fb22d19f4f68 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Tue, 23 Jun 2026 23:19:41 +0100 Subject: [PATCH 1/4] Fix D421 failures Ruff now complains if property docstrings start with a verb, and I started lots of them with "return". This is now fixed. https://docs.astral.sh/ruff/rules/property-docstring-starts-with-verb/ --- src/labthings_fastapi/actions.py | 4 ++-- src/labthings_fastapi/base_descriptor.py | 4 ++-- src/labthings_fastapi/outputs/blob.py | 2 +- src/labthings_fastapi/server/__init__.py | 2 +- src/labthings_fastapi/testing.py | 4 ++-- src/labthings_fastapi/thing.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/labthings_fastapi/actions.py b/src/labthings_fastapi/actions.py index 06c1e8eb..475f23de 100644 --- a/src/labthings_fastapi/actions.py +++ b/src/labthings_fastapi/actions.py @@ -163,13 +163,13 @@ def id(self) -> uuid.UUID: @property def output(self) -> Any: - """Return value of the Action. If the Action is still running, returns None.""" + """The Action's output (return value) or `None` if it's still running.""" with self._status_lock: return RootModelWrapper.unwrap(self._output_model_instance) @property def output_model_instance(self) -> BaseModel | None: - """Return value of the Action, as a model, or None.""" + """The Action's output (return value), as a model, or None.""" with self._status_lock: return self._output_model_instance diff --git a/src/labthings_fastapi/base_descriptor.py b/src/labthings_fastapi/base_descriptor.py index fad45929..64762593 100644 --- a/src/labthings_fastapi/base_descriptor.py +++ b/src/labthings_fastapi/base_descriptor.py @@ -101,12 +101,12 @@ def __init__(self, obj: Owner | None, cls: type[Owner] | None = None) -> None: @property def owning_class(self) -> type[Owner]: - """Retrieve the class this info object is describing.""" + """The class this info object is describing.""" return self._descriptor_cls @property def owning_object(self) -> Owner | None: - """Retrieve the object to which this info object is bound, if present.""" + """The object to which this info object is bound, if present.""" return self._bound_to_obj @property diff --git a/src/labthings_fastapi/outputs/blob.py b/src/labthings_fastapi/outputs/blob.py index 5229f7e5..cf5233a5 100644 --- a/src/labthings_fastapi/outputs/blob.py +++ b/src/labthings_fastapi/outputs/blob.py @@ -743,7 +743,7 @@ def data(self) -> BlobData: @property def content(self) -> bytes: - """Return the the output as a `bytes` object. + """The blob's content as a `bytes` object in memory. This property may return the `bytes` object, or if we have a file it will read the file and return the contents. Client objects may use diff --git a/src/labthings_fastapi/server/__init__.py b/src/labthings_fastapi/server/__init__.py index d45e3fe2..05afbe29 100644 --- a/src/labthings_fastapi/server/__init__.py +++ b/src/labthings_fastapi/server/__init__.py @@ -291,7 +291,7 @@ def things(self) -> Mapping[str, Thing]: @property def application_config(self) -> Mapping[str, Any] | None: - """Return the application configuration from the config file. + """The application configuration from the config file. :return: The custom configuration as specified in the configuration file. diff --git a/src/labthings_fastapi/testing.py b/src/labthings_fastapi/testing.py index da890973..c95fa3fd 100644 --- a/src/labthings_fastapi/testing.py +++ b/src/labthings_fastapi/testing.py @@ -153,7 +153,7 @@ def _action_manager(self) -> ActionManager: @property def application_config(self) -> None: - """Return an empty application configuration when mocking. + """An empty application configuration when mocking. :return: None """ @@ -161,7 +161,7 @@ def application_config(self) -> None: @property def global_lock(self) -> GlobalLock | None: - """Return a global lock.""" + """A global lock.""" return self._global_lock diff --git a/src/labthings_fastapi/thing.py b/src/labthings_fastapi/thing.py index fb945505..b3a06a93 100644 --- a/src/labthings_fastapi/thing.py +++ b/src/labthings_fastapi/thing.py @@ -338,7 +338,7 @@ def save_settings(self) -> None: @property def thing_state(self) -> Mapping: - """Return a dictionary summarising our current state. + """A dictionary summarising our current state. This is intended to be an easy way to collect metadata from a Thing that summarises its state. It might be used, for example, to record metadata From a09fa70ea6e0cfbae6b89077a52ea5bf1545dcea Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Tue, 23 Jun 2026 23:29:17 +0100 Subject: [PATCH 2/4] Fix ASYNC119 failure This gets rid of a linter error relating to async context manager clenup. The `buffer_for_reading` function was intended to leave open the possibility of better management of the "ringbuffer" used by the `MJPEGStream`. In fact, this doesn't provide any extra functionality, and isn't even a real ringbuffer. I've removed the problematic context manager as it doesn't do anything - there's already a function that provides what's needed in a way that Ruff is happy with. --- src/labthings_fastapi/outputs/mjpeg_stream.py | 50 +++---------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/src/labthings_fastapi/outputs/mjpeg_stream.py b/src/labthings_fastapi/outputs/mjpeg_stream.py index 2688fe21..16966a35 100644 --- a/src/labthings_fastapi/outputs/mjpeg_stream.py +++ b/src/labthings_fastapi/outputs/mjpeg_stream.py @@ -12,7 +12,6 @@ from typing import ( Any, AsyncGenerator, - AsyncIterator, Literal, Optional, TYPE_CHECKING, @@ -20,8 +19,6 @@ overload, ) from typing_extensions import Self -from copy import copy -from contextlib import asynccontextmanager import threading import anyio import logging @@ -203,36 +200,12 @@ async def ringbuffer_entry(self, i: int) -> RingbufferEntry: raise ValueError("the ith frame has been overwritten") return entry - @asynccontextmanager - async def buffer_for_reading(self, i: int) -> AsyncIterator[bytes]: - """Yield the ith frame as a bytes object. - - Retrieve frame ``i`` from the ringbuffer. - - This allows async code access to a frame in the ringbuffer. - The frame will not be copied, and should not be written to. - The frame may not exist after the function has completed (i.e. - after any ``with`` statement has finished). - - Using a context manager is intended to allow future versions of this - code to manage access to the ringbuffer (e.g. allowing buffer reuse). - Currently, buffers are always created as fresh `bytes` objects, so - this context manager does not provide additional functionality - over `.MJPEGStream.ringbuffer_entry`. - - :param i: The index of the frame to read - - :yield: The frame's data as `bytes`, along with timestamp and index. - """ - entry = await self.ringbuffer_entry(i) - yield entry.frame - async def next_frame(self) -> int: """Wait for the next frame, and return its index. This async function will yield until a new frame arrives, then return its index. The index may then be used to retrieve the new frame - with `.MJPEGStream.buffer_for_reading`. + with `.MJPEGStream.ringbuffer_entry`. :return: the index of the next frame to arrive. @@ -253,8 +226,8 @@ async def grab_frame(self) -> bytes: :return: The next JPEG frame, as a `bytes` object. """ i = await self.next_frame() - async with self.buffer_for_reading(i) as frame: - return copy(frame) + entry = await self.ringbuffer_entry(i) + return entry.frame async def next_frame_size(self) -> int: """Wait for the next frame and return its size. @@ -264,20 +237,13 @@ async def next_frame_size(self) -> int: :return: The size of the next JPEG frame, in bytes. """ i = await self.next_frame() - async with self.buffer_for_reading(i) as frame: - return len(frame) + entry = await self.ringbuffer_entry(i) + return len(entry.frame) async def frame_async_generator(self) -> AsyncGenerator[bytes, None]: """Yield frames as bytes objects. - This generator will return frames from the MJPEG stream. These are - taken from the ringbuffer by `.MJPEGStream.buffer_for_reading` and - so should have any buffer-management considerations taken care of. - - Code using this generator should complete as quickly as possible, - because future implementations may hold a lock while this function - yields. If lengthy processing is required, please copy the buffer - and continue processing elsewhere. + This generator will return frames from the MJPEG stream. Note that this will wait for a new frame each time. There is no guarantee that we won't skip frames. @@ -288,8 +254,8 @@ async def frame_async_generator(self) -> AsyncGenerator[bytes, None]: while self._streaming: try: i = await self.next_frame() - async with self.buffer_for_reading(i) as frame: - yield frame + entry = await self.ringbuffer_entry(i) + yield entry.frame except StopAsyncIteration: break except Exception as e: # noqa: BLE001 From 6a01a7256aecf1d1aa344012aaa42474e7961a4c Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Tue, 23 Jun 2026 23:36:05 +0100 Subject: [PATCH 3/4] Use a `requirements.in` so `-e .` appears in our lockfile. This means I don't constantly have to reinstall labthings_fastapi after doing a `pip sync`. This commit does not change any package versions. --- README.md | 2 +- dev-requirements.in | 1 + dev-requirements.txt | 58 +++++++++++++++++++++++--------------------- 3 files changed, 32 insertions(+), 29 deletions(-) create mode 100644 dev-requirements.in diff --git a/README.md b/README.md index 9395a8ec..93dbb005 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ All changes to the codebase should go via pull requests, and should only be merg Dependencies are defined in `pyproject.toml` and can be compiled to `dev-requirements.txt` with: ``` -uv pip compile --extra dev pyproject.toml --output-file dev-requirements.txt +uv pip compile dev-requirements.in -o dev-requirements.txt ``` If you're not using `uv`, just regular `pip-compile` from `pip-tools` should do the same thing. diff --git a/dev-requirements.in b/dev-requirements.in new file mode 100644 index 00000000..aefbcb6d --- /dev/null +++ b/dev-requirements.in @@ -0,0 +1 @@ +-e .[dev] diff --git a/dev-requirements.txt b/dev-requirements.txt index 8b910ac6..5fe9f6bc 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,7 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --extra dev pyproject.toml --output-file dev-requirements.txt +# uv pip compile dev-requirements.in -o dev-requirements.txt +-e . + # via -r dev-requirements.in alabaster==1.0.0 # via sphinx annotated-doc==0.0.4 @@ -10,8 +12,8 @@ annotated-types==0.7.0 # via pydantic anyio==4.13.0 # via - # labthings-fastapi (pyproject.toml) # httpx + # labthings-fastapi # starlette # watchfiles apeye==1.4.1 @@ -49,7 +51,7 @@ click==8.4.1 # rich-toolkit # uvicorn codespell==2.4.2 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi coverage==7.14.1 # via pytest-cov detect-installer==0.1.0 @@ -83,7 +85,7 @@ exceptiongroup==1.3.1 # anyio # pytest fastapi==0.135.4 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi fastapi-cli==0.0.24 # via fastapi fastapi-cloud-cli==0.18.0 @@ -96,17 +98,17 @@ filelock==3.29.0 # sphinx-toolbox flake8==7.3.0 # via - # labthings-fastapi (pyproject.toml) # flake8-pyproject # flake8-rst # flake8-rst-docstrings + # labthings-fastapi # pydoclint flake8-pyproject==1.2.4 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi flake8-rst==0.8.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi flake8-rst-docstrings==0.4.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi h11==0.16.0 # via # httpcore @@ -119,9 +121,9 @@ httptools==0.8.0 # via uvicorn httpx==0.28.1 # via - # labthings-fastapi (pyproject.toml) # fastapi # fastapi-cloud-cli + # labthings-fastapi idna==3.16 # via # anyio @@ -146,7 +148,7 @@ jinja2==3.1.6 # sphinx-jinja2-compat jsonschema==4.26.0 # via - # labthings-fastapi (pyproject.toml) + # labthings-fastapi # sphobjinv jsonschema-specifications==2025.9.1 # via jsonschema @@ -165,13 +167,13 @@ mdurl==0.1.2 msgpack==1.1.2 # via cachecontrol mypy==1.20.2 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi mypy-extensions==1.1.0 # via mypy natsort==8.4.0 # via domdf-python-tools numpy==2.2.6 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi packaging==26.2 # via # pytest @@ -179,7 +181,7 @@ packaging==26.2 pathspec==1.1.1 # via mypy pillow==12.2.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi platformdirs==4.9.6 # via apeye pluggy==1.6.0 @@ -190,9 +192,9 @@ pycodestyle==2.14.0 # via flake8 pydantic==2.13.4 # via - # labthings-fastapi (pyproject.toml) # fastapi # fastapi-cloud-cli + # labthings-fastapi # pydantic-extra-types # pydantic-settings pydantic-core==2.46.4 @@ -202,7 +204,7 @@ pydantic-extra-types==2.11.1 pydantic-settings==2.14.1 # via fastapi pydoclint==0.8.4 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi pyflakes==3.4.0 # via flake8 pygments==2.20.0 @@ -215,13 +217,13 @@ pygments==2.20.0 # sphinx-tabs pytest==9.0.3 # via - # labthings-fastapi (pyproject.toml) + # labthings-fastapi # pytest-cov # pytest-mock pytest-cov==7.1.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi pytest-mock==3.15.1 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi python-dotenv==1.2.2 # via # pydantic-settings @@ -264,7 +266,7 @@ rpds-py==0.30.0 ruamel-yaml==0.19.1 # via sphinx-toolbox ruff==0.15.14 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi sentry-sdk==2.60.0 # via fastapi-cloud-cli shellingham==1.5.4 @@ -277,8 +279,8 @@ soupsieve==2.8.4 # via beautifulsoup4 sphinx==8.1.3 # via - # labthings-fastapi (pyproject.toml) # autodocsumm + # labthings-fastapi # sphinx-autoapi # sphinx-autodoc-typehints # sphinx-prompt @@ -287,7 +289,7 @@ sphinx==8.1.3 # sphinx-toolbox # sphinxcontrib-jquery sphinx-autoapi==3.8.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi sphinx-autodoc-typehints==3.0.1 # via sphinx-toolbox sphinx-jinja2-compat==0.4.1 @@ -295,11 +297,11 @@ sphinx-jinja2-compat==0.4.1 sphinx-prompt==1.9.0 # via sphinx-toolbox sphinx-rtd-theme==3.1.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi sphinx-tabs==3.5.0 # via sphinx-toolbox sphinx-toolbox==4.2.0 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -315,7 +317,7 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sphobjinv==2.4 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi starlette==1.1.0 # via fastapi tabulate==0.10.0 @@ -324,10 +326,10 @@ tinycss2==1.5.1 # via dict2css tomli==2.4.1 # via - # labthings-fastapi (pyproject.toml) # coverage # fastapi-cli # flake8-pyproject + # labthings-fastapi # mypy # pydoclint # pytest @@ -337,16 +339,16 @@ typer==0.26.1 # fastapi-cli # fastapi-cloud-cli types-jsonschema==4.26.0.20260518 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi typing-extensions==4.15.0 # via - # labthings-fastapi (pyproject.toml) # anyio # astroid # beautifulsoup4 # domdf-python-tools # exceptiongroup # fastapi + # labthings-fastapi # mypy # pydantic # pydantic-core @@ -383,4 +385,4 @@ webencodings==0.5.1 websockets==16.0 # via uvicorn zeroconf==0.149.16 - # via labthings-fastapi (pyproject.toml) + # via labthings-fastapi From 1d3f4336596d36ef147bb842d0b184f4a6ca9c04 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Tue, 23 Jun 2026 23:38:07 +0100 Subject: [PATCH 4/4] Update pinned packages This should fix the `pip_audit` failure about `starlette` and also updates `ruff` and various other packages. --- dev-requirements.txt | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 5fe9f6bc..d5a51512 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,7 +10,7 @@ annotated-doc==0.0.4 # typer annotated-types==0.7.0 # via pydantic -anyio==4.13.0 +anyio==4.14.0 # via # httpx # labthings-fastapi @@ -31,11 +31,11 @@ autodocsumm==0.2.15 # via sphinx-toolbox babel==2.18.0 # via sphinx -beautifulsoup4==4.14.3 +beautifulsoup4==4.15.0 # via sphinx-toolbox cachecontrol==0.14.4 # via sphinx-toolbox -certifi==2026.5.20 +certifi==2026.6.17 # via # httpcore # httpx @@ -52,7 +52,7 @@ click==8.4.1 # uvicorn codespell==2.4.2 # via labthings-fastapi -coverage==7.14.1 +coverage==7.14.3 # via pytest-cov detect-installer==0.1.0 # via fastapi-cloud-cli @@ -86,13 +86,13 @@ exceptiongroup==1.3.1 # pytest fastapi==0.135.4 # via labthings-fastapi -fastapi-cli==0.0.24 +fastapi-cli==0.0.27 # via fastapi -fastapi-cloud-cli==0.18.0 +fastapi-cloud-cli==0.20.0 # via fastapi-cli fastar==0.11.0 # via fastapi-cloud-cli -filelock==3.29.0 +filelock==3.29.4 # via # cachecontrol # sphinx-toolbox @@ -124,7 +124,7 @@ httpx==0.28.1 # fastapi # fastapi-cloud-cli # labthings-fastapi -idna==3.16 +idna==3.18 # via # anyio # apeye-core @@ -164,7 +164,7 @@ mccabe==0.7.0 # via flake8 mdurl==0.1.2 # via markdown-it-py -msgpack==1.1.2 +msgpack==1.2.1 # via cachecontrol mypy==1.20.2 # via labthings-fastapi @@ -182,7 +182,7 @@ pathspec==1.1.1 # via mypy pillow==12.2.0 # via labthings-fastapi -platformdirs==4.9.6 +platformdirs==4.10.0 # via apeye pluggy==1.6.0 # via @@ -201,9 +201,9 @@ pydantic-core==2.46.4 # via pydantic pydantic-extra-types==2.11.1 # via fastapi -pydantic-settings==2.14.1 +pydantic-settings==2.14.2 # via fastapi -pydoclint==0.8.4 +pydoclint==0.8.7 # via labthings-fastapi pyflakes==3.4.0 # via flake8 @@ -215,7 +215,7 @@ pygments==2.20.0 # sphinx # sphinx-prompt # sphinx-tabs -pytest==9.0.3 +pytest==9.1.1 # via # labthings-fastapi # pytest-cov @@ -228,7 +228,7 @@ python-dotenv==1.2.2 # via # pydantic-settings # uvicorn -python-multipart==0.0.29 +python-multipart==0.0.32 # via fastapi pyyaml==6.0.3 # via @@ -251,7 +251,7 @@ rich==15.0.0 # via # rich-toolkit # typer -rich-toolkit==0.19.10 +rich-toolkit==0.20.1 # via # fastapi-cli # fastapi-cloud-cli @@ -265,15 +265,15 @@ rpds-py==0.30.0 # referencing ruamel-yaml==0.19.1 # via sphinx-toolbox -ruff==0.15.14 +ruff==0.15.18 # via labthings-fastapi -sentry-sdk==2.60.0 +sentry-sdk==2.63.0 # via fastapi-cloud-cli shellingham==1.5.4 # via typer six==1.17.0 # via html5lib -snowballstemmer==3.1.0 +snowballstemmer==3.1.1 # via sphinx soupsieve==2.8.4 # via beautifulsoup4 @@ -318,7 +318,7 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx sphobjinv==2.4 # via labthings-fastapi -starlette==1.1.0 +starlette==1.3.1 # via fastapi tabulate==0.10.0 # via sphinx-toolbox @@ -334,7 +334,7 @@ tomli==2.4.1 # pydoclint # pytest # sphinx -typer==0.26.1 +typer==0.26.7 # via # fastapi-cli # fastapi-cloud-cli @@ -369,7 +369,7 @@ urllib3==2.7.0 # requests # sentry-sdk # sphinx-prompt -uvicorn==0.48.0 +uvicorn==0.49.0 # via # fastapi # fastapi-cli @@ -384,5 +384,5 @@ webencodings==0.5.1 # tinycss2 websockets==16.0 # via uvicorn -zeroconf==0.149.16 +zeroconf==0.150.0 # via labthings-fastapi