Merge branch 'v3-labthings-validate-properties-on-set' into 'v3'

Update to validating properties on write.

See merge request openflexure/openflexure-microscope-server!555
This commit is contained in:
Julian Stirling 2026-05-13 11:26:21 +00:00
commit eff66eb667
13 changed files with 26 additions and 4 deletions

Binary file not shown.

View file

@ -405,6 +405,7 @@ class AutofocusThing(lt.Thing):
field of view to assess focus (autofocus and testing the success of a z-stack) 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() _cam: BaseCamera = lt.thing_slot()
_stage: BaseStage = lt.thing_slot() _stage: BaseStage = lt.thing_slot()

View file

@ -38,6 +38,8 @@ class ChannelBlankError(lt.exceptions.InvocationError):
class BackgroundDetectAlgorithm(lt.Thing): class BackgroundDetectAlgorithm(lt.Thing):
"""The base class for defining background detect algorithms.""" """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) display_name: str = lt.property(default="Base Detector", readonly=True)
def __init__(self, thing_server_interface: lt.ThingServerInterface) -> None: def __init__(self, thing_server_interface: lt.ThingServerInterface) -> None:

View file

@ -172,6 +172,8 @@ class BaseCamera(OFMThing):
``__init__`` method of the subclass. ``__init__`` method of the subclass.
""" """
_class_settings = {"validate_properties_on_set": True}
_all_background_detectors: Mapping[str, BackgroundDetectAlgorithm] = lt.thing_slot() _all_background_detectors: Mapping[str, BackgroundDetectAlgorithm] = lt.thing_slot()
mjpeg_stream = lt.outputs.MJPEGStreamDescriptor() mjpeg_stream = lt.outputs.MJPEGStreamDescriptor()

View file

@ -119,6 +119,8 @@ class CameraStageMapper(lt.Thing):
override the ``get_xyz_position()`` and ``move_to_xyz_position()`` methods. override the ``get_xyz_position()`` and ``move_to_xyz_position()`` methods.
""" """
_class_settings = {"validate_properties_on_set": True}
_cam: BaseCamera = lt.thing_slot() _cam: BaseCamera = lt.thing_slot()
_stage: BaseStage = lt.thing_slot() _stage: BaseStage = lt.thing_slot()

View file

@ -12,6 +12,8 @@ from .stage.sangaboard import SangaboardThing
class Illumination(lt.Thing): class Illumination(lt.Thing):
"""Base class for an illumination controller.""" """Base class for an illumination controller."""
_class_settings = {"validate_properties_on_set": True}
@lt.action @lt.action
def set_led(self, led_on: bool = True) -> None: def set_led(self, led_on: bool = True) -> None:
"""Set the LED to on or off.""" """Set the LED to on or off."""

View file

@ -62,6 +62,8 @@ class ScanWorkflow(Generic[SettingModelType], lt.Thing):
scan planning, acquisition routine. scan planning, acquisition routine.
""" """
_class_settings = {"validate_properties_on_set": True}
display_name: str = lt.property(default="Base Workflow", readonly=True) display_name: str = lt.property(default="Base Workflow", readonly=True)
ui_blurb: str = lt.property( ui_blurb: str = lt.property(
default="If you see this message, something is wrong.", readonly=True default="If you see this message, something is wrong.", readonly=True

View file

@ -119,6 +119,8 @@ class SmartScanThing(OFMThing):
past scans. past scans.
""" """
_class_settings = {"validate_properties_on_set": True}
_cam: BaseCamera = lt.thing_slot() _cam: BaseCamera = lt.thing_slot()
_stage: BaseStage = lt.thing_slot() _stage: BaseStage = lt.thing_slot()
_all_workflows: Mapping[str, ScanWorkflow] = lt.thing_slot() _all_workflows: Mapping[str, ScanWorkflow] = lt.thing_slot()

View file

@ -106,6 +106,7 @@ class BaseStage(lt.Thing):
""" """
_axis_names = ("x", "y", "z") _axis_names = ("x", "y", "z")
_class_settings = {"validate_properties_on_set": True}
def __init__(self, thing_server_interface: lt.ThingServerInterface) -> None: def __init__(self, thing_server_interface: lt.ThingServerInterface) -> None:
"""Initialise the stage. """Initialise the stage.

View file

@ -117,6 +117,8 @@ class ParasiticMotionError(lt.exceptions.InvocationError):
class RangeofMotionThing(lt.Thing): class RangeofMotionThing(lt.Thing):
"""A class used to measure the range of motion of the stage in X and Y.""" """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() _autofocus: AutofocusThing = lt.thing_slot()
_cam: BaseCamera = lt.thing_slot() _cam: BaseCamera = lt.thing_slot()
_csm: CameraStageMapper = lt.thing_slot() _csm: CameraStageMapper = lt.thing_slot()

View file

@ -46,6 +46,8 @@ class OpenFlexureSystem(lt.Thing):
of the system. of the system.
""" """
_class_settings = {"validate_properties_on_set": True}
_microscope_id: Optional[str] = None _microscope_id: Optional[str] = None
@lt.setting @lt.setting

View file

@ -7,6 +7,7 @@ import numpy as np
import pytest import pytest
from hypothesis import given from hypothesis import given
from hypothesis import strategies as st from hypothesis import strategies as st
from pydantic import ValidationError
import labthings_fastapi as lt import labthings_fastapi as lt
@ -210,7 +211,7 @@ def test_objective_getter_setter(camera):
camera.objective = 15 camera.objective = 15
with pytest.raises(ValueError, match=err_msg): with pytest.raises(ValueError, match=err_msg):
camera.objective = 0 camera.objective = 0
with pytest.raises(ValueError, match=err_msg): with pytest.raises(ValidationError):
camera.objective = "twenty" camera.objective = "twenty"

View file

@ -25,7 +25,7 @@ from unittest import mock
import pytest import pytest
from fastapi import HTTPException from fastapi import HTTPException
from pydantic import BaseModel from pydantic import BaseModel, ValidationError
from labthings_fastapi.exceptions import InvocationCancelledError from labthings_fastapi.exceptions import InvocationCancelledError
from labthings_fastapi.testing import create_thing_without_server 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: with caplog.at_level(logging.WARNING), smart_scan_thing:
assert smart_scan_thing._workflow_name == "foo" assert smart_scan_thing._workflow_name == "foo"
assert smart_scan_thing._workflow is workflows["foo"] assert smart_scan_thing._workflow is workflows["foo"]
# Can't set None, warns doesn't change # Can't set None, raises a ValidationError
smart_scan_thing.workflow_name = None 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 len(caplog.records) == 1
assert smart_scan_thing._workflow_name == "foo" assert smart_scan_thing._workflow_name == "foo"
assert smart_scan_thing._workflow is workflows["foo"] assert smart_scan_thing._workflow is workflows["foo"]