Raise error if tuning algorithm cannot be returned, unless explicityly asked not to

This commit is contained in:
Julian Stirling 2025-08-21 12:25:52 +01:00
parent 677fb8d988
commit 0a9b6be9bd
2 changed files with 53 additions and 12 deletions

View file

@ -3,14 +3,17 @@
import tempfile import tempfile
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pytest import fixture import pytest
import labthings_fastapi as lt 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: def picamera_thing() -> StreamingPiCamera2:
"""Return a StreamingPiCamera2 Thing. """Return a StreamingPiCamera2 Thing.
@ -20,7 +23,7 @@ def picamera_thing() -> StreamingPiCamera2:
return StreamingPiCamera2() return StreamingPiCamera2()
@fixture() @pytest.fixture()
def client(picamera_thing) -> lt.ThingClient: def client(picamera_thing) -> lt.ThingClient:
"""Initialise a test client for the StreamingPiCamera2 Thing. """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): def test_get_tuning_algo(picamera_thing):
"""Test that get_tuning algorithm retrieves tuning algorithms.""" """Test that get_tuning algorithm retrieves tuning algorithms."""
# Missing algorithm returns None # Missing algorithm raises an error.
assert picamera_thing.get_tuning_algo("foo") is None 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 # Real algorithm is a dict and contains expected keys
assert isinstance(picamera_thing.get_tuning_algo("rpi.geq"), dict) assert isinstance(picamera_thing.get_tuning_algo("rpi.geq"), dict)
assert "offset" in picamera_thing.get_tuning_algo("rpi.geq") assert "offset" in picamera_thing.get_tuning_algo("rpi.geq")
# Set the tuning to None as it is technically optional. And check this # 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: try:
picamera_thing.tuning = None picamera_thing.tuning = None
except lt.exceptions.NotConnectedToServerError: except lt.exceptions.NotConnectedToServerError:
# Labthings will complain that it is not connected to a server # Labthings will complain that it is not connected to a server
pass 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.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): def test_calibration(picamera_thing, client):

View file

@ -15,7 +15,7 @@ https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf
""" """
from __future__ import annotations 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 from datetime import datetime
import json import json
import logging import logging
@ -47,6 +47,10 @@ from . import picamera_recalibrate_utils as recalibrate_utils
from . import BaseCamera, JPEGBlob, ArrayModel from . import BaseCamera, JPEGBlob, ArrayModel
class MissingCalibrationError(RuntimeError):
"""Picamera tuning file is missing or doesn't contain the requested algorithm."""
class PicameraStreamOutput(Output): class PicameraStreamOutput(Output):
"""An Output class that sends frames to a stream.""" """An Output class that sends frames to a stream."""
@ -308,17 +312,40 @@ class StreamingPiCamera2(BaseCamera):
tuning = lt.ThingSetting(Optional[dict], None, readonly=True) tuning = lt.ThingSetting(Optional[dict], None, readonly=True)
"""The Raspberry PiCamera Tuning File JSON.""" """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. """Return the active tuning algorithm settings for the given algorithm.
:returns: The algorithm dictionary if found, returns None if no tuning data :returns: The algorithm dictionary if found, returns None if no tuning data
is loaded or if the tuning algorithm is not found. 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 self.tuning is None:
if raise_if_missing:
raise MissingCalibrationError("No tuning data is set.")
return None return None
try: try:
return Picamera2.find_tuning_algo(self.tuning, algorithm_name) 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 return None
def _initialise_picamera(self): def _initialise_picamera(self):