From d673ceab7b9df316dbbef1f137134755870d4582 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Tue, 3 Mar 2026 15:13:54 +0000 Subject: [PATCH 1/5] Update test suite to cope with LabThings validating properties on write (https://github.com/labthings/labthings-fastapi/pull/278) --- tests/unit_tests/test_simulated_camera.py | 3 ++- tests/unit_tests/test_smart_scan.py | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/test_simulated_camera.py b/tests/unit_tests/test_simulated_camera.py index 4935d9d6..dd499fc9 100644 --- a/tests/unit_tests/test_simulated_camera.py +++ b/tests/unit_tests/test_simulated_camera.py @@ -7,6 +7,7 @@ import numpy as np import pytest from hypothesis import given from hypothesis import strategies as st +from pydantic import ValidationError import labthings_fastapi as lt @@ -210,7 +211,7 @@ def test_objective_getter_setter(camera): camera.objective = 15 with pytest.raises(ValueError, match=err_msg): camera.objective = 0 - with pytest.raises(ValueError, match=err_msg): + with pytest.raises(ValidationError): camera.objective = "twenty" diff --git a/tests/unit_tests/test_smart_scan.py b/tests/unit_tests/test_smart_scan.py index e8abf2d5..566faea9 100644 --- a/tests/unit_tests/test_smart_scan.py +++ b/tests/unit_tests/test_smart_scan.py @@ -25,7 +25,7 @@ from unittest import mock import pytest from fastapi import HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from labthings_fastapi.exceptions import InvocationCancelledError from labthings_fastapi.testing import create_thing_without_server @@ -206,8 +206,11 @@ def test_setting_workflows(caplog, mocker): with caplog.at_level(logging.WARNING), smart_scan_thing: assert smart_scan_thing._workflow_name == "foo" assert smart_scan_thing._workflow is workflows["foo"] - # Can't set None, warns doesn't change - smart_scan_thing.workflow_name = None + # Can't set None, raises a ValidationError + with pytest.raises(ValidationError): + smart_scan_thing.workflow_name = None + # Empty string still won't change, but just warns + smart_scan_thing.workflow_name = "" assert len(caplog.records) == 1 assert smart_scan_thing._workflow_name == "foo" assert smart_scan_thing._workflow is workflows["foo"] From 05f92b9b3301fc72fa0a1656f6e964504f8fade9 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 1 Apr 2026 09:24:21 +0100 Subject: [PATCH 2/5] Specify the feature branch of LabThings and enable the feature flag. I've put the feature flag in `openflexure_microscope_server.things.__init__` so that we only need it once, rather than next to every Thing definition. I decided it was better here than, for example, in the server or cli modules because this way we still get the feature flags even if the Things are served from a different LabThings server. --- src/openflexure_microscope_server/things/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/openflexure_microscope_server/things/__init__.py b/src/openflexure_microscope_server/things/__init__.py index c497209f..838e649c 100644 --- a/src/openflexure_microscope_server/things/__init__.py +++ b/src/openflexure_microscope_server/things/__init__.py @@ -9,6 +9,10 @@ from typing import Optional, Self import labthings_fastapi as lt +# To ensure consistency, we enable the feature flags we want here. This means +# they will be enabled in any code that imports a `Thing` from this package. +lt.FEATURE_FLAGS.validate_properties_on_set = True + class OFMThing(lt.Thing): """A custom LabThings Thing class for the OpenFlexure Microscope.""" From 622fa176346be37f5259605e82aaf79141bd3649 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 22 Apr 2026 00:28:10 +0100 Subject: [PATCH 3/5] Enable validation for all Things and pick correct branch This commit updates the branch to use Thing._class_settings instead of FEATURE_FLAGS for a safer way to implement new features. --- src/openflexure_microscope_server/things/__init__.py | 4 ---- src/openflexure_microscope_server/things/autofocus.py | 1 + src/openflexure_microscope_server/things/background_detect.py | 2 ++ src/openflexure_microscope_server/things/camera/__init__.py | 2 ++ .../things/camera_stage_mapping.py | 2 ++ src/openflexure_microscope_server/things/illumination.py | 2 ++ src/openflexure_microscope_server/things/scan_workflows.py | 4 ++++ src/openflexure_microscope_server/things/smart_scan.py | 2 ++ src/openflexure_microscope_server/things/stage/__init__.py | 1 + src/openflexure_microscope_server/things/stage_measure.py | 2 ++ src/openflexure_microscope_server/things/system.py | 2 ++ 11 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/openflexure_microscope_server/things/__init__.py b/src/openflexure_microscope_server/things/__init__.py index 838e649c..c497209f 100644 --- a/src/openflexure_microscope_server/things/__init__.py +++ b/src/openflexure_microscope_server/things/__init__.py @@ -9,10 +9,6 @@ from typing import Optional, Self import labthings_fastapi as lt -# To ensure consistency, we enable the feature flags we want here. This means -# they will be enabled in any code that imports a `Thing` from this package. -lt.FEATURE_FLAGS.validate_properties_on_set = True - class OFMThing(lt.Thing): """A custom LabThings Thing class for the OpenFlexure Microscope.""" diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index eebff335..695c349c 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -405,6 +405,7 @@ class AutofocusThing(lt.Thing): field of view to assess focus (autofocus and testing the success of a z-stack) """ + _class_settings = {"validate_properties_on_set": True} _cam: BaseCamera = lt.thing_slot() _stage: BaseStage = lt.thing_slot() diff --git a/src/openflexure_microscope_server/things/background_detect.py b/src/openflexure_microscope_server/things/background_detect.py index c2753a75..fcf03b9d 100644 --- a/src/openflexure_microscope_server/things/background_detect.py +++ b/src/openflexure_microscope_server/things/background_detect.py @@ -38,6 +38,8 @@ class ChannelBlankError(lt.exceptions.InvocationError): class BackgroundDetectAlgorithm(lt.Thing): """The base class for defining background detect algorithms.""" + _class_settings = {"validate_properties_on_set": True} + display_name: str = lt.property(default="Base Detector", readonly=True) def __init__(self, thing_server_interface: lt.ThingServerInterface) -> None: diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 96840889..b9f0f2a7 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -172,6 +172,8 @@ class BaseCamera(OFMThing): ``__init__`` method of the subclass. """ + _class_settings = {"validate_properties_on_set": True} + _all_background_detectors: Mapping[str, BackgroundDetectAlgorithm] = lt.thing_slot() mjpeg_stream = lt.outputs.MJPEGStreamDescriptor() diff --git a/src/openflexure_microscope_server/things/camera_stage_mapping.py b/src/openflexure_microscope_server/things/camera_stage_mapping.py index 813c762f..341683a9 100644 --- a/src/openflexure_microscope_server/things/camera_stage_mapping.py +++ b/src/openflexure_microscope_server/things/camera_stage_mapping.py @@ -119,6 +119,8 @@ class CameraStageMapper(lt.Thing): override the ``get_xyz_position()`` and ``move_to_xyz_position()`` methods. """ + _class_settings = {"validate_properties_on_set": True} + _cam: BaseCamera = lt.thing_slot() _stage: BaseStage = lt.thing_slot() diff --git a/src/openflexure_microscope_server/things/illumination.py b/src/openflexure_microscope_server/things/illumination.py index 2e444138..ce8647b0 100644 --- a/src/openflexure_microscope_server/things/illumination.py +++ b/src/openflexure_microscope_server/things/illumination.py @@ -12,6 +12,8 @@ from .stage.sangaboard import SangaboardThing class Illumination(lt.Thing): """Base class for an illumination controller.""" + _class_settings = {"validate_properties_on_set": True} + @lt.action def set_led(self, led_on: bool = True) -> None: """Set the LED to on or off.""" diff --git a/src/openflexure_microscope_server/things/scan_workflows.py b/src/openflexure_microscope_server/things/scan_workflows.py index c92900c2..952b9dcb 100644 --- a/src/openflexure_microscope_server/things/scan_workflows.py +++ b/src/openflexure_microscope_server/things/scan_workflows.py @@ -62,6 +62,8 @@ class ScanWorkflow(Generic[SettingModelType], lt.Thing): scan planning, acquisition routine. """ + _class_settings = {"validate_properties_on_set": True} + display_name: str = lt.property(default="Base Workflow", readonly=True) ui_blurb: str = lt.property( default="If you see this message, something is wrong.", readonly=True @@ -327,6 +329,8 @@ class SmartStackCompatibleSettings(Protocol): class SmartStackMixin: """A mixin for scan workflows that use smart stacking.""" + _class_settings = {"validate_properties_on_set": True} + stack_images_to_save: int = lt.setting(default=1, ge=1, le=9) """The number of images to save in a stack. diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 5b2e54af..25b0a60b 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -119,6 +119,8 @@ class SmartScanThing(OFMThing): past scans. """ + _class_settings = {"validate_properties_on_set": True} + _cam: BaseCamera = lt.thing_slot() _stage: BaseStage = lt.thing_slot() _all_workflows: Mapping[str, ScanWorkflow] = lt.thing_slot() diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index d0bdeab6..e5b0b72b 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -106,6 +106,7 @@ class BaseStage(lt.Thing): """ _axis_names = ("x", "y", "z") + _class_settings = {"validate_properties_on_set": True} def __init__(self, thing_server_interface: lt.ThingServerInterface) -> None: """Initialise the stage. diff --git a/src/openflexure_microscope_server/things/stage_measure.py b/src/openflexure_microscope_server/things/stage_measure.py index 411e57ed..4d426a51 100644 --- a/src/openflexure_microscope_server/things/stage_measure.py +++ b/src/openflexure_microscope_server/things/stage_measure.py @@ -117,6 +117,8 @@ class ParasiticMotionError(lt.exceptions.InvocationError): class RangeofMotionThing(lt.Thing): """A class used to measure the range of motion of the stage in X and Y.""" + _class_settings = {"validate_properties_on_set": True} + _autofocus: AutofocusThing = lt.thing_slot() _cam: BaseCamera = lt.thing_slot() _csm: CameraStageMapper = lt.thing_slot() diff --git a/src/openflexure_microscope_server/things/system.py b/src/openflexure_microscope_server/things/system.py index ff6a2fed..d1d02517 100644 --- a/src/openflexure_microscope_server/things/system.py +++ b/src/openflexure_microscope_server/things/system.py @@ -46,6 +46,8 @@ class OpenFlexureSystem(lt.Thing): of the system. """ + _class_settings = {"validate_properties_on_set": True} + _microscope_id: Optional[str] = None @lt.setting From b00f90aadacfcd7a3f73986687dc2ce02b808fc0 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 22 Apr 2026 10:13:29 +0100 Subject: [PATCH 4/5] Remove settings from SmartStackMixin As properties now take validation settings from the final class, we no longer need to redefine settings here, so I've removed them. --- src/openflexure_microscope_server/things/scan_workflows.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/openflexure_microscope_server/things/scan_workflows.py b/src/openflexure_microscope_server/things/scan_workflows.py index 952b9dcb..fe4ee898 100644 --- a/src/openflexure_microscope_server/things/scan_workflows.py +++ b/src/openflexure_microscope_server/things/scan_workflows.py @@ -329,8 +329,6 @@ class SmartStackCompatibleSettings(Protocol): class SmartStackMixin: """A mixin for scan workflows that use smart stacking.""" - _class_settings = {"validate_properties_on_set": True} - stack_images_to_save: int = lt.setting(default=1, ge=1, le=9) """The number of images to save in a stack. From e7599702069f3b5c5dd4d766c8ecd263329264f7 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 13 May 2026 11:53:20 +0100 Subject: [PATCH 5/5] update picamera coverage zip --- picamera_coverage.zip | Bin 54038 -> 54038 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/picamera_coverage.zip b/picamera_coverage.zip index 7c9090de9076b9c1bd178612c060e28d019a9d14..121e8c33f5b1271b9fbaf3f925059ecd133bca41 100644 GIT binary patch delta 348 zcmbQXjCtBJW}yIYW)=|!5SSUaHs;7Im$w^*R2oFyF!2B6f6Kp%Kc4R)-%`F}J_p`6 zn;iu%@otvuD`u>ZWMO6GY&2qJ`t{vxvySm>$*wER4DT5j7#vvm7@BuAM68cvmSbRQ zIGDgL-5`75>SeZuT;?0?yd@i$4H#}SGZ--KX#B@{p)&7|Pi+Ge-wg(ds(7G=nswMDrxmM9U--Gm8|Xq-4|NL=yw!Buf)RGXoQg zv?SxiWTVX&F4QyHS(v68n_H%t86_nfn3x)xSs0q7nx+{gCK;I~r#1P;XMNbg98g6L$jX3wbj=f*c%uU z3~n4`OJFbfx`i>}Hp90>X4y9kJO{opF!V6wEvRQInI~-?EWe06T zFkoh3