From 0a9b6be9bd680b6d512d30fc815a48165799dd4e Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 21 Aug 2025 12:25:52 +0100 Subject: [PATCH] Raise error if tuning algorithm cannot be returned, unless explicityly asked not to --- .../picamera2/test_calibration.py | 32 +++++++++++++----- .../things/camera/picamera.py | 33 +++++++++++++++++-- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/hardware-specific-tests/picamera2/test_calibration.py b/hardware-specific-tests/picamera2/test_calibration.py index a589ca7b..732c02a4 100644 --- a/hardware-specific-tests/picamera2/test_calibration.py +++ b/hardware-specific-tests/picamera2/test_calibration.py @@ -3,14 +3,17 @@ import tempfile from fastapi.testclient import TestClient -from pytest import fixture +import pytest import labthings_fastapi as lt -from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2 +from openflexure_microscope_server.things.camera.picamera import ( + StreamingPiCamera2, + MissingCalibrationError, +) -@fixture() +@pytest.fixture() def picamera_thing() -> StreamingPiCamera2: """Return a StreamingPiCamera2 Thing. @@ -20,7 +23,7 @@ def picamera_thing() -> StreamingPiCamera2: return StreamingPiCamera2() -@fixture() +@pytest.fixture() def client(picamera_thing) -> lt.ThingClient: """Initialise a test client for the StreamingPiCamera2 Thing. @@ -40,22 +43,33 @@ def client(picamera_thing) -> lt.ThingClient: def test_get_tuning_algo(picamera_thing): """Test that get_tuning algorithm retrieves tuning algorithms.""" - # Missing algorithm returns None - assert picamera_thing.get_tuning_algo("foo") is None + # Missing algorithm raises an error. + with pytest.raises(MissingCalibrationError): + picamera_thing.get_tuning_algo("foo") + # Or returns None if explicitly told not to error. + assert picamera_thing.get_tuning_algo("foo", raise_if_missing=False) is None + # Real algorithm is a dict and contains expected keys assert isinstance(picamera_thing.get_tuning_algo("rpi.geq"), dict) assert "offset" in picamera_thing.get_tuning_algo("rpi.geq") # Set the tuning to None as it is technically optional. And check this - # is handled gracefully. + # is handled gracefully. This needs to be done in a try block as LabThings + # tries and fails to emit an event. try: picamera_thing.tuning = None except lt.exceptions.NotConnectedToServerError: # Labthings will complain that it is not connected to a server pass - # Check it is set to None. + + # Check it is set to None despite the above LabThings issue. assert picamera_thing.tuning is None - assert picamera_thing.get_tuning_algo("rpi.geq") is None + + # Raises an error as there is no tuning file set. + with pytest.raises(MissingCalibrationError): + picamera_thing.get_tuning_algo("rpi.geq") + # Or returns None if explicitly told not to error. + assert picamera_thing.get_tuning_algo("rpi.geq", raise_if_missing=False) is None def test_calibration(picamera_thing, client): diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 734e2a88..f3282c61 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -15,7 +15,7 @@ https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf """ from __future__ import annotations -from typing import Annotated, Iterator, Literal, Mapping, Optional +from typing import Annotated, Iterator, Literal, Mapping, Optional, overload from datetime import datetime import json import logging @@ -47,6 +47,10 @@ from . import picamera_recalibrate_utils as recalibrate_utils from . import BaseCamera, JPEGBlob, ArrayModel +class MissingCalibrationError(RuntimeError): + """Picamera tuning file is missing or doesn't contain the requested algorithm.""" + + class PicameraStreamOutput(Output): """An Output class that sends frames to a stream.""" @@ -308,17 +312,40 @@ class StreamingPiCamera2(BaseCamera): tuning = lt.ThingSetting(Optional[dict], None, readonly=True) """The Raspberry PiCamera Tuning File JSON.""" - def get_tuning_algo(self, algorithm_name: str) -> Optional[dict]: + # Use overload to clarify that only a dictionary is returned if `raise_if_missing` + # is True + @overload + def get_tuning_algo( + self, algorithm_name: str, raise_if_missing: Literal[True] + ) -> dict: ... + # Otherwise may also be None + @overload + def get_tuning_algo( + self, algorithm_name: str, raise_if_missing: bool + ) -> Optional[dict]: ... + + def get_tuning_algo( + self, algorithm_name: str, raise_if_missing: bool = True + ) -> Optional[dict]: """Return the active tuning algorithm settings for the given algorithm. :returns: The algorithm dictionary if found, returns None if no tuning data is loaded or if the tuning algorithm is not found. + + :raises MissingCalibrationError: If raise_if_missing is true and there is no + tuning file is available, or the requested algorithm is not present. """ if self.tuning is None: + if raise_if_missing: + raise MissingCalibrationError("No tuning data is set.") return None try: return Picamera2.find_tuning_algo(self.tuning, algorithm_name) - except StopIteration: + except StopIteration as e: + if raise_if_missing: + raise MissingCalibrationError( + f"No tuning algorithm with name {algorithm_name}." + ) from e return None def _initialise_picamera(self):