diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 5d3c68ec..0ee8d32b 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -16,7 +16,7 @@ from types import TracebackType from typing import Literal, Mapping, Optional, Self, Sequence import numpy as np -from pydantic import BaseModel, computed_field, field_validator, model_validator +from pydantic import BaseModel, Field, computed_field, field_validator, model_validator import labthings_fastapi as lt from labthings_fastapi.types.numpy import NDArray @@ -59,12 +59,12 @@ class StackParams(BaseModel): """A class for holding stack parameters, and returning computed ones.""" stack_dz: int - images_to_save: int + images_to_save: int = Field(gt=0) # Using docstrings under variables as this is how pdoc would expect # attributed to be documented - settling_time: float = 0.3 + settling_time: float = Field(default=0.3, ge=0) """Time (in seconds) between moving and capturing an image""" backlash_correction: int = 250 @@ -75,22 +75,6 @@ class StackParams(BaseModel): origin: StackOrigin = StackOrigin.START """Where the stack is positioned relative to the current z position.""" - @field_validator("images_to_save") - @classmethod - def validate_images_to_save(cls, value: int) -> int: - """Validate that the number of images to save is greater than zero.""" - if value <= 0: - raise ValueError(f"Invalid number of images to save: {value}. Must be > 0.") - return value - - @field_validator("settling_time") - @classmethod - def validate_settling_time(cls, value: float) -> float: - """Validate that settling time is zero or positive.""" - if value < 0: - raise ValueError(f"Invalid settling time: {value}. Must be positive or 0.") - return value - class SmartStackParams(StackParams): """A class for holding smart stack parameters, and returning computed ones.""" diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index bd09f173..f656ed1b 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -15,12 +15,12 @@ import tempfile import time from datetime import datetime from types import TracebackType -from typing import Any, Literal, Mapping, Optional, Self, Tuple +from typing import Annotated, Any, Literal, Mapping, Optional, Self, Tuple import numpy as np import piexif from PIL import Image -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, Field import labthings_fastapi as lt from labthings_fastapi.types.numpy import NDArray @@ -48,34 +48,15 @@ class CaptureError(RuntimeError): """An error trying to capture from a CameraThing.""" +PositiveInt = Annotated[int, Field(ge=1)] +NonEmptyString = Annotated[str, Field(min_length=1)] + + class CaptureParams(BaseModel): """A class for capturing at least a single image.""" - images_dir: str - save_resolution: tuple[int, int] - - @field_validator("save_resolution") - @classmethod - def validate_save_resolution(cls, value: Tuple[int, int]) -> Tuple[int, int]: - """Validate that save_resolution is a tuple with exactly two positive integers.""" - if not isinstance(value, tuple): - raise TypeError("Save resolution should be a tuple") - if len(value) != 2 or any(not isinstance(x, int) or x <= 0 for x in value): - raise ValueError( - f"Invalid save_resolution: {value}. " - "Must be a tuple of two positive integers." - ) - return value - - @field_validator("images_dir") - @classmethod - def validate_images_dir(cls, value: str) -> str: - """Validate that images_dir is a non-empty string.""" - if not isinstance(value, str) or not value: - raise ValueError( - f"Invalid images_dir: {value}. Must be a non-empty string." - ) - return value + images_dir: NonEmptyString + save_resolution: tuple[PositiveInt, PositiveInt] class NoImageInMemoryError(RuntimeError): diff --git a/tests/unit_tests/test_stack.py b/tests/unit_tests/test_stack.py index 8ed3611d..900e58ab 100644 --- a/tests/unit_tests/test_stack.py +++ b/tests/unit_tests/test_stack.py @@ -109,8 +109,8 @@ def test_stack_params_negative_images_to_save(save_ims, extra_ims): # Depending on the values multiple messages are possible match = ( "(Can't test for focus with fewer than 3 images|" - "Images to save must be positive and odd)|" - "Invalid number of images to save" + "Images to save must be positive and odd|" + "Input should be greater than 0)" ) with pytest.raises(ValueError, match=match): SmartStackParams( @@ -154,8 +154,8 @@ def test_even_images_to_save(save_ims, extra_ims): """ match = ( "(Can't test for focus with fewer than 3 images|" - "Images to save must be positive and odd)|" - "Invalid number of images to save" + "Images to save must be positive and odd|" + "Input should be greater than 0)" ) with pytest.raises(ValueError, match=match): SmartStackParams( @@ -948,7 +948,7 @@ def test_run_basic_stack_end_origin( def test_invalid_stack_images_raises(): """Test basic stack raises expected error for negative or zero image count.""" for capture_count in [-3, 0]: - with pytest.raises(ValueError, match="Invalid number of images to save"): + with pytest.raises(ValueError, match="Input should be greater than 0"): StackParams( stack_dz=10, images_to_save=capture_count, @@ -960,7 +960,7 @@ def test_invalid_stack_images_raises(): def test_invalid_stack_settling_raises(): """Test basic stack raises expected error for negative settling time.""" - with pytest.raises(ValueError, match="Invalid settling time"): + with pytest.raises(ValueError, match="Input should be greater than or equal to 0"): StackParams( stack_dz=10, images_to_save=1, @@ -973,7 +973,7 @@ def test_invalid_stack_settling_raises(): @pytest.mark.parametrize( ("bad_path", "match_err"), [ - ("", "Must be a non-empty string"), + ("", "String should have at least 1 character"), (None, "Input should be a valid string"), (67, "Input should be a valid string"), ], @@ -987,8 +987,8 @@ def test_invalid_capture_dir_raises(bad_path, match_err): @pytest.mark.parametrize( ("bad_res", "match_err"), [ - ((-100, 50), "Invalid save_resolution:"), - ((20, 0), "Invalid save_resolution:"), + ((-100, 50), "Input should be greater than or equal to 1"), + ((20, 0), "Input should be greater than or equal to 1"), ("", "Input should be a valid tuple"), (None, "Input should be a valid tuple"), (67, "Input should be a valid tuple"),