From ee49dc9ce9ad4bcbc2787b14ebb8f27f8e74c399 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Tue, 27 Oct 2026 23:18:37 +0000 Subject: [PATCH 1/4] Update pinned dev dependencies --- dev-requirements.txt | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 39740097..2520707d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -38,7 +38,7 @@ beautifulsoup4==4.15.0 # via sphinx-toolbox cachecontrol==0.14.4 # via sphinx-toolbox -certifi==2026.7.22 +certifi==2026.10.23 # via # httpcore # httpx @@ -74,7 +74,7 @@ docutils==0.21.2 # sphinx-rtd-theme # sphinx-tabs # sphinx-toolbox -domdf-python-tools==3.10.0 +domdf-python-tools==3.11.0 # via # apeye # apeye-core @@ -96,7 +96,7 @@ fastapi-cloud-cli==0.26.0 # via fastapi-cli fastar==0.12.0 # via fastapi-cloud-cli -filelock==4.0.12 +filelock==4.1.0 # via # cachecontrol # sphinx-toolbox @@ -122,15 +122,15 @@ html5lib==1.1 # via sphinx-toolbox httpcore==1.0.9 # via httpx -httpcore2==2.13.1 +httpcore2==2.14.0 # via httpx2 -httptools==0.8.0 +httptools==0.9.0 # via uvicorn httpx==0.28.1 # via # fastapi # fastapi-cloud-cli -httpx2==2.13.1 +httpx2==2.14.0 # via labthings-fastapi idna==3.20 # via @@ -179,7 +179,7 @@ mdit-py-plugins==0.6.1 # via myst-parser mdurl==0.1.2 # via markdown-it-py -msgpack==1.2.3 +msgpack==1.2.4 # via cachecontrol mypy==1.20.2 # via labthings-fastapi @@ -201,26 +201,26 @@ pillow==12.3.0 # via labthings-fastapi platformdirs==4.12.4 # via apeye -pluggy==1.6.0 +pluggy==1.7.0 # via # pytest # pytest-cov pycodestyle==2.15.0 # via flake8 -pydantic==2.14.0 +pydantic==2.14.1 # via # fastapi # fastapi-cloud-cli # labthings-fastapi # pydantic-extra-types # pydantic-settings -pydantic-core==2.50.0 +pydantic-core==2.50.1 # via pydantic pydantic-extra-types==2.11.1 # via fastapi pydantic-settings==2.15.0 # via fastapi -pydoclint==0.11.0 +pydoclint==0.11.1 # via labthings-fastapi pyflakes==4.0.3 # via flake8 @@ -232,14 +232,14 @@ pygments==2.21.0 # sphinx # sphinx-prompt # sphinx-tabs -pytest==9.1.1 +pytest==9.2.0 # via # labthings-fastapi # pytest-cov # pytest-mock pytest-cov==7.1.0 # via labthings-fastapi -pytest-mock==3.16.0 +pytest-mock==3.17.0 # via labthings-fastapi python-dotenv==1.2.4 # via @@ -283,9 +283,9 @@ rpds-py==0.30.0 # referencing ruamel-yaml==0.19.1 # via sphinx-toolbox -ruff==0.16.10 +ruff==0.17.3 # via labthings-fastapi -sentry-sdk==2.71.0 +sentry-sdk==2.73.0 # via fastapi-cloud-cli shellingham==1.5.4 # via typer @@ -329,7 +329,7 @@ sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jquery==4.1 # via sphinx-rtd-theme -sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-jsmath==2.0.0 # via sphinx sphinxcontrib-qthelp==2.0.0 # via sphinx @@ -353,7 +353,7 @@ tomli==2.5.0 # pydoclint # pytest # sphinx -truststore==0.10.4 +truststore==0.10.5 # via # httpcore2 # httpx2 From 123de73980ac1e919d56718f3c0d7a398da04753 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Tue, 27 Oct 2026 23:30:30 +0000 Subject: [PATCH 2/4] Drop Python 3.10 Python 3.10 will be end of life by the time v0.4.0 is released, so I'm removing it from tests, documentation and metadata. This also allows us to re-lock dependencies using Python 3.11, which fixes several vulnerabilities. I assume versions were being held back because other packages already dropped Python 3.10. I've added pip-audit to our dev dependencies, as it's helpful to run it locally and not just in CI. --- .github/workflows/test.yml | 4 +- README.md | 2 +- dev-requirements.txt | 95 ++++++++++++------- docs/source/index.rst | 2 +- docs/source/testing.md | 2 +- docs/source/tutorial/installing_labthings.rst | 2 +- pyproject.toml | 5 +- 7 files changed, 69 insertions(+), 43 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index efe4ae5b..08fb8ac4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.10", "3.11", "3.12", "3.13"] + python: ["3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v7 @@ -94,7 +94,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.10", "3.11", "3.12", "3.13"] + python: ["3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v7 diff --git a/README.md b/README.md index e4c59e49..6640a08b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ See [readthedocs] for installation instructions that are automatically tested. Y ### Installation Notes -`labthings-fastapi` supports **Python 3.10, 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current `pydantic-core` dependencies. +`labthings-fastapi` supports **Python 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current `pydantic-core` dependencies. > **Note: Windows Installations on devices with ARM processors** > diff --git a/dev-requirements.txt b/dev-requirements.txt index 2520707d..ab9a0920 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -36,8 +36,12 @@ babel==2.18.0 # via sphinx beautifulsoup4==4.15.0 # via sphinx-toolbox +boolean-py==5.0 + # via license-expression cachecontrol==0.14.4 - # via sphinx-toolbox + # via + # pip-audit + # sphinx-toolbox certifi==2026.10.23 # via # httpcore @@ -55,13 +59,17 @@ click==8.5.0 # uvicorn codespell==2.4.3 # via labthings-fastapi -coverage==7.16.2 +coverage==7.17.0 # via pytest-cov +cyclonedx-python-lib==11.12.0 + # via pip-audit +defusedxml==0.7.1 + # via py-serializable detect-installer==0.2.1 # via fastapi-cloud-cli dict2css==0.6.0 # via sphinx-toolbox -dnspython==2.8.0 +dnspython==2.9.0 # via email-validator docstring-parser-fork==0.0.16 # via pydoclint @@ -84,10 +92,6 @@ email-validator==2.3.0 # via # fastapi # pydantic -exceptiongroup==1.3.1 - # via - # anyio - # pytest fastapi==0.135.4 # via labthings-fastapi fastapi-cli==0.0.32 @@ -96,7 +100,7 @@ fastapi-cloud-cli==0.26.0 # via fastapi-cli fastar==0.12.0 # via fastapi-cloud-cli -filelock==4.1.0 +filelock==4.2.4 # via # cachecontrol # sphinx-toolbox @@ -122,7 +126,7 @@ html5lib==1.1 # via sphinx-toolbox httpcore==1.0.9 # via httpx -httpcore2==2.14.0 +httpcore2==2.16.0 # via httpx2 httptools==0.9.0 # via uvicorn @@ -130,7 +134,7 @@ httpx==0.28.1 # via # fastapi # fastapi-cloud-cli -httpx2==2.14.0 +httpx2==2.16.0 # via labthings-fastapi idna==3.20 # via @@ -156,6 +160,7 @@ jinja2==3.1.6 # sphinx # sphinx-autoapi # sphinx-jinja2-compat + # sphinx-prompt jsonschema==4.26.0 # via # labthings-fastapi @@ -164,6 +169,8 @@ jsonschema-specifications==2025.9.1 # via jsonschema librt==0.16.0 # via mypy +license-expression==30.4.4 + # via cyclonedx-python-lib markdown-it-py==3.0.0 # via # mdit-py-plugins @@ -189,22 +196,38 @@ myst-parser==4.0.1 # via labthings-fastapi natsort==8.4.0 # via domdf-python-tools -numpy==2.2.6 +numpy==2.4.6 # via labthings-fastapi +packageurl-python==0.17.6 + # via cyclonedx-python-lib packaging==26.3 # via + # pip-audit + # pip-requirements-parser # pytest # sphinx pathspec==1.1.1 # via mypy -pillow==12.3.0 +pillow==13.0.0 # via labthings-fastapi -platformdirs==4.12.4 - # via apeye +pip==26.2.2 + # via pip-api +pip-api==0.0.35 + # via pip-audit +pip-audit==2.10.1 + # via labthings-fastapi +pip-requirements-parser==32.0.1 + # via pip-audit +platformdirs==4.13.6 + # via + # apeye + # pip-audit pluggy==1.7.0 # via # pytest # pytest-cov +py-serializable==2.1.0 + # via cyclonedx-python-lib pycodestyle==2.15.0 # via flake8 pydantic==2.14.1 @@ -218,7 +241,7 @@ pydantic-core==2.50.1 # via pydantic pydantic-extra-types==2.11.1 # via fastapi -pydantic-settings==2.15.0 +pydantic-settings==2.16.1 # via fastapi pydoclint==0.11.1 # via labthings-fastapi @@ -232,6 +255,8 @@ pygments==2.21.0 # sphinx # sphinx-prompt # sphinx-tabs +pyparsing==3.3.3 + # via pip-requirements-parser pytest==9.2.0 # via # labthings-fastapi @@ -262,11 +287,14 @@ requests==2.34.2 # via # apeye # cachecontrol + # pip-audit # sphinx + # sphinx-prompt restructuredtext-lint==2.0.2 # via flake8-rst-docstrings rich==15.0.0 # via + # pip-audit # rich-toolkit # typer rich-toolkit==0.20.6 @@ -277,7 +305,11 @@ rignore==0.8.1 # via fastapi-cloud-cli roman==5.2 # via sphinx-toolbox -rpds-py==0.30.0 +roman-numerals==4.1.0 + # via roman-numerals-py +roman-numerals-py==4.1.0 + # via sphinx +rpds-py==2026.9.1 # via # jsonschema # referencing @@ -293,9 +325,11 @@ six==1.17.0 # via html5lib snowballstemmer==3.1.1 # via sphinx -soupsieve==2.10 +sortedcontainers==2.4.0 + # via cyclonedx-python-lib +soupsieve==3.0.4 # via beautifulsoup4 -sphinx==8.1.3 +sphinx==8.2.3 # via # autodocsumm # labthings-fastapi @@ -309,11 +343,11 @@ sphinx==8.1.3 # sphinxcontrib-jquery sphinx-autoapi==3.8.1 # via labthings-fastapi -sphinx-autodoc-typehints==3.0.1 +sphinx-autodoc-typehints==3.5.2 # via sphinx-toolbox sphinx-jinja2-compat==0.4.1 # via sphinx-toolbox -sphinx-prompt==1.9.0 +sphinx-prompt==1.10.2 # via sphinx-toolbox sphinx-rtd-theme==3.1.0 # via labthings-fastapi @@ -337,22 +371,16 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx sphobjinv==2.4 # via labthings-fastapi -starlette==1.7.0 +starlette==1.10.0 # via fastapi tabulate==0.10.0 # via sphinx-toolbox tinycss2==1.5.1 # via dict2css tomli==2.5.0 - # via - # coverage - # fastapi-cli - # flake8-pyproject - # labthings-fastapi - # mypy - # pydoclint - # pytest - # sphinx + # via pip-audit +tomli-w==1.2.0 + # via pip-audit truststore==0.10.5 # via # httpcore2 @@ -366,10 +394,9 @@ types-jsonschema==4.26.0.20261006 typing-extensions==4.16.0 # via # anyio - # astroid # beautifulsoup4 + # cyclonedx-python-lib # domdf-python-tools - # exceptiongroup # fastapi # httpx2 # labthings-fastapi @@ -377,12 +404,12 @@ typing-extensions==4.16.0 # pydantic # pydantic-core # pydantic-extra-types + # pydantic-settings # referencing # rich-toolkit # sphinx-toolbox # starlette # typing-inspection - # uvicorn typing-inspection==0.4.4 # via # fastapi @@ -406,7 +433,7 @@ webencodings==0.6.1 # via # html5lib # tinycss2 -websockets==16.1.1 +websockets==17.3 # via uvicorn zeroconf==0.151.5 # via labthings-fastapi diff --git a/docs/source/index.rst b/docs/source/index.rst index a4829189..9622f2c4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -52,7 +52,7 @@ Installation ``pip install labthings-fastapi`` -``labthings-fastapi`` supports **Python 3.10, 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current ``pydantic-core`` dependencies. +``labthings-fastapi`` supports **Python 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current ``pydantic-core`` dependencies. .. include:: _windows_arm_note.rst diff --git a/docs/source/testing.md b/docs/source/testing.md index d929edf4..54e664bc 100644 --- a/docs/source/testing.md +++ b/docs/source/testing.md @@ -9,7 +9,7 @@ You can find out more about contributing code in our [Contributor Guidelines]. Here is what the CI pipeline tests: -* **Matrix Testing:** We run the core test suite (`pytest`) and static type checks (`mypy`) across Python versions 3.10, 3.11, 3.12, and 3.13. +* **Matrix Testing:** We run the core test suite (`pytest`) and static type checks (`mypy`) across Python versions 3.11, 3.12, and 3.13. * **Code Quality & Security:** The pipeline runs `ruff` (formatting and linting), `flake8` (docstrings), `codespell` (spelling), and a `pip-audit` to check for dependency vulnerabilities. * **Coverage:** We track code coverage. A report is generated on your PR to show if your changes increased or decreased overall test coverage. * **Dependency Checks:** We run the tests twice: once with pinned dependencies (`dev-requirements.txt`) for reproducibility, and once with unpinned dependencies (`.[dev]`) to catch upstream breakages early. diff --git a/docs/source/tutorial/installing_labthings.rst b/docs/source/tutorial/installing_labthings.rst index c52e4df8..6a0b9d77 100644 --- a/docs/source/tutorial/installing_labthings.rst +++ b/docs/source/tutorial/installing_labthings.rst @@ -29,7 +29,7 @@ It is also possible to install LabThings from source, by cloning the GitHub repo Installation Notes ++++++++++++++++++ -``labthings-fastapi`` supports **Python 3.10, 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current ``pydantic-core`` dependencies. +``labthings-fastapi`` supports **Python 3.11, 3.12, and 3.13**. The upper limit is strictly capped at 3.13 due to current ``pydantic-core`` dependencies. .. include:: ../_windows_arm_note.rst diff --git a/pyproject.toml b/pyproject.toml index 1d109b1a..d022113c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,8 @@ authors = [ ] description = "An implementation of LabThings using FastAPI" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -39,6 +38,7 @@ dev = [ "flake8-pyproject", "flake8-rst", "flake8-rst-docstrings", + "pip-audit", "pydoclint[flake8]", "sphinx-rtd-theme", "sphinx>=7.2", @@ -46,7 +46,6 @@ dev = [ "sphinx-toolbox", "sphobjinv", "myst-parser<5", - "tomli; python_version < '3.11'", "codespell", ] From 686d3febd64b5456589c8b1c0695d9ed5552a3ae Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Tue, 27 Oct 2026 23:40:26 +0000 Subject: [PATCH 3/4] Import `Self` from typing Since Python 3.11, `Self` is available in the standard library, so there's no need to import it from typing_extensions. This commit swaps the import. It doesn't drop the dependency, as Pydantic still needs us to import TypedDict from typing_extensions. --- src/labthings_fastapi/base_descriptor.py | 4 +--- src/labthings_fastapi/client/__init__.py | 3 +-- src/labthings_fastapi/invocation_contexts.py | 4 +--- src/labthings_fastapi/middleware/url_for.py | 3 +-- src/labthings_fastapi/outputs/blob.py | 2 +- src/labthings_fastapi/outputs/mjpeg_stream.py | 2 +- src/labthings_fastapi/problem_details.py | 3 +-- src/labthings_fastapi/properties.py | 3 ++- src/labthings_fastapi/server/__init__.py | 3 +-- src/labthings_fastapi/thing.py | 3 +-- 10 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/labthings_fastapi/base_descriptor.py b/src/labthings_fastapi/base_descriptor.py index c3b5bc18..b24b2dd8 100644 --- a/src/labthings_fastapi/base_descriptor.py +++ b/src/labthings_fastapi/base_descriptor.py @@ -33,11 +33,9 @@ from collections.abc import Iterator from itertools import pairwise from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Generic, Mapping, TypeVar, overload +from typing import TYPE_CHECKING, Any, Generic, Mapping, Self, TypeVar, overload from weakref import WeakKeyDictionary, ref -from typing_extensions import Self - from labthings_fastapi.exceptions import ( DescriptorAddedToClassTwiceError, DescriptorNotAddedToClassError, diff --git a/src/labthings_fastapi/client/__init__.py b/src/labthings_fastapi/client/__init__.py index 3cceea85..18cbea8f 100644 --- a/src/labthings_fastapi/client/__init__.py +++ b/src/labthings_fastapi/client/__init__.py @@ -9,12 +9,11 @@ import time from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any, Optional, Self, Union from urllib.parse import urljoin, urlparse import httpx2 from pydantic import BaseModel, TypeAdapter, ValidationError -from typing_extensions import Self # 3.9, 3.10 compatibility from labthings_fastapi.exceptions import ( ClientPropertyError, diff --git a/src/labthings_fastapi/invocation_contexts.py b/src/labthings_fastapi/invocation_contexts.py index 94bf72b9..67a65e16 100644 --- a/src/labthings_fastapi/invocation_contexts.py +++ b/src/labthings_fastapi/invocation_contexts.py @@ -19,12 +19,10 @@ from contextlib import contextmanager from contextvars import ContextVar from threading import Event, Thread -from typing import Any, Callable +from typing import Any, Callable, Self from uuid import UUID, uuid4 from weakref import WeakValueDictionary -from typing_extensions import Self - from labthings_fastapi.exceptions import ( InvocationCancelledError, NoInvocationContextError, diff --git a/src/labthings_fastapi/middleware/url_for.py b/src/labthings_fastapi/middleware/url_for.py index b6702b02..ca4079d7 100644 --- a/src/labthings_fastapi/middleware/url_for.py +++ b/src/labthings_fastapi/middleware/url_for.py @@ -35,14 +35,13 @@ from collections.abc import Awaitable, Callable, Iterator from contextlib import contextmanager from contextvars import ContextVar -from typing import Any +from typing import Any, Self from fastapi import Request, Response from pydantic import GetCoreSchemaHandler from pydantic.networks import AnyUrl from pydantic_core import core_schema from starlette.datastructures import URL -from typing_extensions import Self from labthings_fastapi.exceptions import NoUrlForContextError diff --git a/src/labthings_fastapi/outputs/blob.py b/src/labthings_fastapi/outputs/blob.py index c3f6275e..763b3bfe 100644 --- a/src/labthings_fastapi/outputs/blob.py +++ b/src/labthings_fastapi/outputs/blob.py @@ -95,6 +95,7 @@ def get_image(self) -> MyImageBlob: ClassVar, Literal, Mapping, + Self, ) from weakref import WeakValueDictionary @@ -108,7 +109,6 @@ def get_image(self) -> MyImageBlob: ) from pydantic.json_schema import JsonSchemaValue from pydantic_core import core_schema -from typing_extensions import Self from labthings_fastapi.exceptions import MediaTypeMismatchError from labthings_fastapi.middleware.url_for import url_for diff --git a/src/labthings_fastapi/outputs/mjpeg_stream.py b/src/labthings_fastapi/outputs/mjpeg_stream.py index 39f4d7e2..2c9b0205 100644 --- a/src/labthings_fastapi/outputs/mjpeg_stream.py +++ b/src/labthings_fastapi/outputs/mjpeg_stream.py @@ -16,6 +16,7 @@ AsyncGenerator, Literal, Optional, + Self, Union, overload, ) @@ -23,7 +24,6 @@ import anyio from fastapi import FastAPI from fastapi.responses import HTMLResponse, StreamingResponse -from typing_extensions import Self if TYPE_CHECKING: from labthings_fastapi.thing import Thing diff --git a/src/labthings_fastapi/problem_details.py b/src/labthings_fastapi/problem_details.py index 09c9d4ce..67c166e5 100644 --- a/src/labthings_fastapi/problem_details.py +++ b/src/labthings_fastapi/problem_details.py @@ -6,12 +6,11 @@ from functools import wraps from logging import Logger -from typing import Any, Callable, ParamSpec, TypeVar +from typing import Any, Callable, ParamSpec, Self, TypeVar from fastapi import Response from fastapi.responses import JSONResponse from pydantic import BaseModel, ConfigDict -from typing_extensions import Self from labthings_fastapi import exceptions diff --git a/src/labthings_fastapi/properties.py b/src/labthings_fastapi/properties.py index 2de6fc68..1515b8b2 100644 --- a/src/labthings_fastapi/properties.py +++ b/src/labthings_fastapi/properties.py @@ -57,6 +57,7 @@ class attribute. Documentation is in strings immediately following the Callable, Generic, Literal, + Self, TypeVar, overload, ) @@ -72,7 +73,7 @@ class attribute. Documentation is in strings immediately following the create_model, with_config, ) -from typing_extensions import Self, TypedDict +from typing_extensions import TypedDict from labthings_fastapi.base_descriptor import ( DescriptorInfoCollection, diff --git a/src/labthings_fastapi/server/__init__.py b/src/labthings_fastapi/server/__init__.py index 5d336198..208a4765 100644 --- a/src/labthings_fastapi/server/__init__.py +++ b/src/labthings_fastapi/server/__init__.py @@ -12,7 +12,7 @@ from collections.abc import Iterator, Mapping, Sequence from contextlib import AsyncExitStack, asynccontextmanager, contextmanager from types import MappingProxyType -from typing import Any, AsyncGenerator, Optional, TypeVar, overload +from typing import Any, AsyncGenerator, Optional, Self, TypeVar, overload import uvicorn from anyio.from_thread import BlockingPortal @@ -22,7 +22,6 @@ from fastapi.testclient import TestClient from pydantic import ValidationError from pydantic_core import PydanticSerializationError -from typing_extensions import Self from labthings_fastapi.actions import ActionManager from labthings_fastapi.exceptions import GlobalLockBusyError diff --git a/src/labthings_fastapi/thing.py b/src/labthings_fastapi/thing.py index 986b6d26..f2de6ddd 100644 --- a/src/labthings_fastapi/thing.py +++ b/src/labthings_fastapi/thing.py @@ -12,13 +12,12 @@ import os from collections.abc import Mapping from json.decoder import JSONDecodeError -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Optional, Self from anyio.to_thread import run_sync from fastapi import Request, WebSocket from fastapi.encoders import jsonable_encoder from pydantic import ValidationError -from typing_extensions import Self from labthings_fastapi.actions import ActionCollection from labthings_fastapi.base_descriptor import OptionallyBoundDescriptor From 12a38a5d71a25228382ba34c78e6d1d04c2becfa Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Tue, 27 Oct 2026 23:55:16 +0000 Subject: [PATCH 4/4] Fix test of MJPEG stream Starlette's `TestClient` now correctly relays HTTP streams. I have updated this test accordingly. The improved version of this test introduced in #409 will also need updating in due course, but fixing it here untangles the merge ordering. --- tests/test_mjpeg_stream.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_mjpeg_stream.py b/tests/test_mjpeg_stream.py index 1aefa2b8..ba05bd86 100644 --- a/tests/test_mjpeg_stream.py +++ b/tests/test_mjpeg_stream.py @@ -55,20 +55,19 @@ def client(): def test_mjpeg_stream(client): """Verify the MJPEG stream contains at least one frame marker. - A limitation of the TestClient is that it can't actually stream. - This means that all of the frames sent by our test Thing will - arrive in a single packet. - - For now, we just check it starts with the frame separator, - but it might be possible in the future to check there are three - images there. + Each time a new frame is sent, the stream actually emits three chunks + of data: the frame separator, the frame itself, and a newline. """ with client.stream("GET", "/telly/stream") as stream: stream.raise_for_status() received = 0 for b in stream.iter_bytes(): received += 1 - assert b.startswith(b"--frame") + if received % 3 == 1: + # We yield three times per frame, with + # the separator, the frame data, and a newline. + # This checks for the frame separator. + assert b.startswith(b"--frame") if __name__ == "__main__":