From 24233412af0016c44edd84fca1483d168a9004be Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 27 Aug 2025 13:21:34 +0100 Subject: [PATCH 1/7] Start adding ANN ruff rules --- change_log_helper.py | 2 +- pyproject.toml | 8 +++++- src/openflexure_microscope_server/logging.py | 4 +-- .../scan_directories.py | 2 +- .../scan_planners.py | 2 +- .../stitching.py | 2 +- .../things/autofocus.py | 4 +-- .../things/camera/__init__.py | 10 +++++-- .../things/camera/opencv.py | 8 +++++- .../things/camera/picamera.py | 24 ++++++++++++---- .../camera/picamera_recalibrate_utils.py | 4 ++- .../things/camera/simulation.py | 8 +++++- .../things/smart_scan.py | 13 ++++++--- .../things/stage/__init__.py | 8 +++--- .../things/stage/dummy.py | 13 +++++++-- .../things/stage/sangaboard.py | 14 +++++++--- src/openflexure_microscope_server/ui.py | 10 +++++-- .../utilities.py | 28 ++++++++++++++----- tests/test_stage.py | 5 ++-- 19 files changed, 123 insertions(+), 46 deletions(-) diff --git a/change_log_helper.py b/change_log_helper.py index 94202aaf..79296132 100755 --- a/change_log_helper.py +++ b/change_log_helper.py @@ -12,7 +12,7 @@ from gitlab.exceptions import GitlabGetError PROJECT_ID = 9238334 -def main(tag, branch: str = "v3"): +def main(tag: str, branch: str = "v3"): """Create a list of the MRs into a given branch after a tag. :param tag: The tag the mrs should be since. diff --git a/pyproject.toml b/pyproject.toml index 746cc24e..b1d8aaa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,8 +124,9 @@ select = [ "C90", # McCabe complexity! "NPY", # Numpy linting "N", # PEP8 naming - "D", # Docstring checks these may need to be added gradually + "D", # Docstring checks "ERA001", # Commented out code! + "ANN001", ] ignore = [ @@ -140,6 +141,11 @@ ignore = [ # Tests are currently not fully docstring-ed, we'll ignore this for now. "tests/*" = [ "B018", # Complaining about useless attribute access in tests, but we need them to check errors are raised + "ANN", # Tests are not typehinted for fixtures etc +] +"hardware-specific-tests/*" = [ + "B018", # Complaining about useless attribute access in tests, but we need them to check errors are raised + "ANN", # Tests are not typehinted for fixtures etc ] [tool.ruff.lint.pydocstyle] diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index 69a03ffc..0df9a60e 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -21,7 +21,7 @@ from fastapi import HTTPException OFM_LOG_FILE = None -def configure_logging(log_folder): +def configure_logging(log_folder: str): """Configure logging for the server while it is running. This modifies the root logger to have a rotating file handler and @@ -120,7 +120,7 @@ class OFMLogFileFormatter(logging.Formatter): class OFMHandler(logging.Handler): """A logging.Handler that stores the most recent logs for access by the server.""" - def __init__(self, level=logging.INFO, max_logs=250): + def __init__(self, level: int = logging.INFO, max_logs: int = 250): """Initialise the handler with a set logging level and message buffer size. :param level: The level of logs captured. As standard logs of INFO and above diff --git a/src/openflexure_microscope_server/scan_directories.py b/src/openflexure_microscope_server/scan_directories.py index a6fd124f..cb96bd25 100644 --- a/src/openflexure_microscope_server/scan_directories.py +++ b/src/openflexure_microscope_server/scan_directories.py @@ -551,7 +551,7 @@ class ScanDirectory: return zip_fname -def get_files_in_zip(zip_path): +def get_files_in_zip(zip_path: str): """List the relative paths of all files and folders in the zip folder specified.""" scan_zip = zipfile.ZipFile(zip_path) return [os.path.normpath(i) for i in scan_zip.namelist()] diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 065b3258..87f9000b 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -332,7 +332,7 @@ class SmartSpiral(ScanPlanner): """Sort the remaining positions based on the current location.""" # Defined rather than use a lambda for readability - def sort_key(pos): + def sort_key(pos: XYPos): return ( self.moves_between(current_pos, pos), self.moves_between(self._initial_position, pos), diff --git a/src/openflexure_microscope_server/stitching.py b/src/openflexure_microscope_server/stitching.py index 1660944e..32a5253e 100644 --- a/src/openflexure_microscope_server/stitching.py +++ b/src/openflexure_microscope_server/stitching.py @@ -185,7 +185,7 @@ class FinalStitcher(BaseStitcher): def __init__( self, - images_dir, + images_dir: str, *, logger: lt.deps.InvocationLogger, overlap: Optional[float] = None, diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index a6c0e92a..e80d1e47 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -127,7 +127,7 @@ class StackParams: """ return self.min_images_to_test + 15 - def slice_to_save(self, sharpest_index): + def slice_to_save(self, sharpest_index: int): """Return the slice of images to save given the index of the sharpest image.""" images_each_side = (self.images_to_save - 1) // 2 return slice( @@ -401,7 +401,7 @@ class AutofocusThing(lt.Thing): self, stage: Stage, sharpness_monitor: SharpnessMonitorDep, - dz=2000, + dz: int = 2000, start: Literal["centre", "base"] = "centre", ): """Repeatedly autofocus the stage until it looks focused. diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index abee8f06..94badd0c 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -8,6 +8,7 @@ See repository root for licensing information. from __future__ import annotations from typing import Literal, Optional, Tuple, Any +from types import TracebackType import json import io import time @@ -185,7 +186,12 @@ class BaseCamera(lt.Thing): """Open hardware connection when the Thing context manager is opened.""" raise NotImplementedError("CameraThings must define their own __enter__ method") - def __exit__(self, _exc_type, _exc_value, _traceback) -> None: + def __exit__( + self, + _exc_type: type[BaseException], + _exc_value: Optional[BaseException], + _traceback: Optional[TracebackType], + ) -> None: """Close hardware connection when the Thing context manager is closed.""" raise NotImplementedError("CameraThings must define their own __exit__ method") @@ -575,7 +581,7 @@ class BaseCamera(lt.Thing): return self.active_detector.status @lt.thing_action - def update_detector_settings(self, data) -> None: + def update_detector_settings(self, data: dict[str, Any]) -> None: """Update the settings of the current detector. This is an action not a setting/property as the data model depends on the diff --git a/src/openflexure_microscope_server/things/camera/opencv.py b/src/openflexure_microscope_server/things/camera/opencv.py index 5f179301..e87befec 100644 --- a/src/openflexure_microscope_server/things/camera/opencv.py +++ b/src/openflexure_microscope_server/things/camera/opencv.py @@ -10,6 +10,7 @@ from __future__ import annotations import logging from typing import Literal, Optional +from types import TracebackType from threading import Thread import cv2 @@ -42,7 +43,12 @@ class OpenCVCamera(BaseCamera): self._capture_thread.start() return self - def __exit__(self, _exc_type, _exc_value, _traceback): + def __exit__( + self, + _exc_type: type[BaseException], + _exc_value: Optional[BaseException], + _traceback: Optional[TracebackType], + ): """Release the camera when the Thing context manager is closed. Before releasing the camera the capture thread is closed. diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 5e55ea92..25b29a44 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -15,7 +15,8 @@ https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf """ from __future__ import annotations -from typing import Annotated, Iterator, Literal, Mapping, Optional, overload +from typing import Annotated, Iterator, Literal, Mapping, Optional, overload, Any +from types import TracebackType import json import logging import os @@ -65,7 +66,12 @@ class PicameraStreamOutput(Output): self.portal = portal def outputframe( - self, frame, _keyframe=True, _timestamp=None, _packet=None, _audio=False + self, + frame: bytes, + _keyframe: Optional[bool] = True, + _timestamp: Optional[int] = None, + _packet: Any = None, + _audio: bool = False, ): """Add a frame to the stream's ringbuffer.""" self.stream.add_frame(frame, self.portal) @@ -396,7 +402,7 @@ class StreamingPiCamera2(BaseCamera): return self._picamera is not None and self._picamera.started @contextmanager - def _streaming_picamera(self, pause_stream=False) -> Iterator[Picamera2]: + def _streaming_picamera(self, pause_stream: bool = False) -> Iterator[Picamera2]: """Lock access to picamera and return the underlying ``Picamera2`` instance. Optionally the stream can be paused to allow updating the camera settings. @@ -419,7 +425,12 @@ class StreamingPiCamera2(BaseCamera): if pause_stream and already_streaming: self.start_streaming() - def __exit__(self, exc_type, exc_value, traceback): + def __exit__( + self, + _exc_type: type[BaseException], + _exc_value: Optional[BaseException], + _traceback: Optional[TracebackType], + ): """Close the picamera connection when the Thing context manager is closed.""" self.stop_streaming() with self._streaming_picamera() as cam: @@ -725,7 +736,10 @@ class StreamingPiCamera2(BaseCamera): return tuple(recalibrate_utils.get_static_ccm(self.tuning)[0]["ccm"]) @colour_correction_matrix.setter # type: ignore - def colour_correction_matrix(self, value) -> None: + def colour_correction_matrix( + self, + value: tuple[float, float, float, float, float, float, float, float, float], + ) -> None: recalibrate_utils.set_static_ccm(self.tuning, value) if self._picamera is not None: diff --git a/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py b/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py index fbf827df..44f269f6 100644 --- a/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py +++ b/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py @@ -353,7 +353,9 @@ def upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray: return zoom(grids, zoom_factors, order=1)[:, : shape[0], : shape[1]] -def downsampled_channels(channels: np.ndarray, blacklevel=64) -> list[np.ndarray]: +def downsampled_channels( + channels: np.ndarray, blacklevel: int = 64 +) -> list[np.ndarray]: """Generate a downsampled, un-normalised image from which to calculate the LST. TODO: blacklevel probably ought to be determined from the camera... diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 7c7a856b..31e1c9aa 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -9,6 +9,7 @@ See repository root for licensing information. from __future__ import annotations import logging from typing import Literal, Optional +from types import TracebackType from threading import Thread import time @@ -212,7 +213,12 @@ class SimulatedCamera(BaseCamera): self.start_streaming() return self - def __exit__(self, _exc_type, _exc_value, _traceback): + def __exit__( + self, + _exc_type: type[BaseException], + _exc_value: Optional[BaseException], + _traceback: Optional[TracebackType], + ): """Close the capture thread when the Thing context manager is closed.""" if self.stream_active: self._capture_enabled = False diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 9e44a329..a7b70fb8 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -7,7 +7,7 @@ It also controls external processes for live stitching composite images, and the creation of the final stitched images. """ -from typing import Optional +from typing import Optional, Self, TypeVar, ParamSpec, Callable, Concatenate import threading import os import time @@ -30,6 +30,9 @@ from .camera_stage_mapping import CameraStageMapper from .camera import CameraDependency as CameraClient from .stage import StageDependency as StageDep +T = TypeVar("T") +P = ParamSpec("P") + CSMDep = lt.deps.direct_thing_client_dependency( CameraStageMapper, "/camera_stage_mapping/" ) @@ -43,7 +46,9 @@ class ScanNotRunningError(RuntimeError): """Exception called when scan not running that requires a scan to be running.""" -def _scan_running(method): +def _scan_running( + method: Callable[Concatenate[Self, P], T], +) -> Callable[Concatenate[Self, P], T]: """Decorate a method so that it will error if a scan is not running. This decorator is used by all methods in SmartScanThing that are using @@ -52,7 +57,7 @@ def _scan_running(method): the same time and released with the lock """ - def scan_running_wrapper(self, *args, **kwargs): + def scan_running_wrapper(self: Self, *args: P.args, **kwargs: P.kwargs) -> T: # Only start the method is the scan logger is set if self._scan_logger is not None: return method(self, *args, **kwargs) @@ -623,7 +628,7 @@ class SmartScanThing(lt.Thing): if scan_info.number_of_images == 0: self._delete_scan(scan_info.name, logger) - def _delete_scan(self, scan_name, logger: lt.deps.InvocationLogger) -> bool: + def _delete_scan(self, scan_name: str, logger: lt.deps.InvocationLogger) -> bool: """Delete a scan. This is a wrapper around scan manager's delete_scan that logs to the diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index a57f903d..92bd76d7 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -142,7 +142,7 @@ class BaseStage(lt.Thing): self, cancel: lt.deps.CancelHook, block_cancellation: bool = False, - **kwargs: Mapping[str, int], + **kwargs: int, ): """Make a relative move. Keyword arguments should be axis names.""" self._hardware_move_relative( @@ -155,7 +155,7 @@ class BaseStage(lt.Thing): self, cancel: lt.deps.CancelHook, block_cancellation: bool = False, - **kwargs: Mapping[str, int], + **kwargs: int, ): """Make a relative move in the coordinate system used by the physical hardware. @@ -170,7 +170,7 @@ class BaseStage(lt.Thing): self, cancel: lt.deps.CancelHook, block_cancellation: bool = False, - **kwargs: Mapping[str, int], + **kwargs: int, ): """Make an absolute move. Keyword arguments should be axis names.""" self._hardware_move_absolute( @@ -183,7 +183,7 @@ class BaseStage(lt.Thing): self, cancel: lt.deps.CancelHook, block_cancellation: bool = False, - **kwargs: Mapping[str, int], + **kwargs: int, ): """Make a absolute move in the coordinate system used by the physical hardware. diff --git a/src/openflexure_microscope_server/things/stage/dummy.py b/src/openflexure_microscope_server/things/stage/dummy.py index 8b71f78f..3015b42c 100644 --- a/src/openflexure_microscope_server/things/stage/dummy.py +++ b/src/openflexure_microscope_server/things/stage/dummy.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Optional +from types import TracebackType from collections.abc import Mapping import time @@ -33,7 +35,12 @@ class DummyStage(BaseStage): """Register the stage position when the Thing context manager is opened.""" self.instantaneous_position = self._hardware_position - def __exit__(self, _exc_type, _exc_value, _traceback): + def __exit__( + self, + _exc_type: type[BaseException], + _exc_value: Optional[BaseException], + _traceback: Optional[TracebackType], + ): """Nothing to do when the Thing context manager is closed.""" axis_inverted = lt.ThingSetting( @@ -47,7 +54,7 @@ class DummyStage(BaseStage): self, cancel: lt.deps.CancelHook, block_cancellation: bool = False, - **kwargs: Mapping[str, int], + **kwargs: int, ): """Make a relative move. Keyword arguments should be axis names.""" displacement = [kwargs.get(k, 0) for k in self.axis_names] @@ -85,7 +92,7 @@ class DummyStage(BaseStage): self, cancel: lt.deps.CancelHook, block_cancellation: bool = False, - **kwargs: Mapping[str, int], + **kwargs: int, ): """Make an absolute move. Keyword arguments should be axis names.""" displacement = { diff --git a/src/openflexure_microscope_server/things/stage/sangaboard.py b/src/openflexure_microscope_server/things/stage/sangaboard.py index d6ab3b29..8fea6684 100644 --- a/src/openflexure_microscope_server/things/stage/sangaboard.py +++ b/src/openflexure_microscope_server/things/stage/sangaboard.py @@ -5,7 +5,8 @@ import logging import threading import time from copy import copy -from typing import Iterator, Literal +from typing import Iterator, Literal, Optional +from types import TracebackType from contextlib import contextmanager from collections.abc import Mapping @@ -53,7 +54,12 @@ class SangaboardThing(BaseStage): sb.query("blocking_moves false") self.update_position() - def __exit__(self, _exc_type, _exc_value, _traceback): + def __exit__( + self, + _exc_type: type[BaseException], + _exc_value: Optional[BaseException], + _traceback: Optional[TracebackType], + ): """Close the sangaboard connection when the Thing context manager is closed.""" with self.sangaboard() as sb: sb.close() @@ -85,7 +91,7 @@ class SangaboardThing(BaseStage): self, cancel: lt.deps.CancelHook, block_cancellation: bool = False, - **kwargs: Mapping[str, int], + **kwargs: int, ) -> None: """Make a relative move in the coordinate system used by the sangaboard.""" displacement = [kwargs.get(axis, 0) for axis in self.axis_names] @@ -112,7 +118,7 @@ class SangaboardThing(BaseStage): self, cancel: lt.deps.CancelHook, block_cancellation: bool = False, - **kwargs: Mapping[str, int], + **kwargs: int, ) -> None: """Make a absolute move in the coordinate system used by the sangaboard.""" with self.sangaboard(): diff --git a/src/openflexure_microscope_server/ui.py b/src/openflexure_microscope_server/ui.py index be2a5c12..7a595dd9 100644 --- a/src/openflexure_microscope_server/ui.py +++ b/src/openflexure_microscope_server/ui.py @@ -1,7 +1,11 @@ """Functionality for communicating the required user interface for a thing.""" +from typing import Callable, Any + from pydantic import BaseModel +import labthings_fastapi as lt + class ActionButton(BaseModel): """The data required for creating an actionButton in Vue. @@ -43,7 +47,7 @@ class ActionButton(BaseModel): """The message to show on successful completion.""" -def action_button_for(action, **kwargs) -> ActionButton: +def action_button_for(action: Callable[..., Any], **kwargs: Any) -> ActionButton: """Create a ActionButton data for the specified Thing Action. :param action: The thing action to create a button for. @@ -80,7 +84,9 @@ class PropertyControl(BaseModel): """The delay in ms before reading back the property.""" -def property_control_for(thing, property_name, **kwargs) -> PropertyControl: +def property_control_for( + thing: lt.Thing, property_name: str, **kwargs: Any +) -> PropertyControl: """Create an PropertyControl data for the specified Thing Property. :param thing: The instance of the thing that has the property to be controlled. diff --git a/src/openflexure_microscope_server/utilities.py b/src/openflexure_microscope_server/utilities.py index bbad56f9..5975ed1e 100644 --- a/src/openflexure_microscope_server/utilities.py +++ b/src/openflexure_microscope_server/utilities.py @@ -1,6 +1,6 @@ """Utility functions and classes.""" -from typing import TypeVar, Callable, ParamSpec +from typing import TypeVar, Callable, ParamSpec, Optional, Any, Concatenate, Self import os import re import sys @@ -24,7 +24,7 @@ COMMIT_REGEX = re.compile(r"[0-9a-f]{40}") REF_REGEX = re.compile(r"^ref:\s(.*)$") -def _is_lock_like(obj): +def _is_lock_like(obj: Any) -> bool: """Check if an object is a lock. Cannot use ``isinstance(obj, threading.RLock)``, may be possible to use @@ -34,14 +34,16 @@ def _is_lock_like(obj): return hasattr(obj, "acquire") and hasattr(obj, "release") -def requires_lock(method: Callable[P, T]) -> Callable[P, T]: +def requires_lock( + method: Callable[Concatenate[Self, P], T], +) -> Callable[Concatenate[Self, P], T]: """Decorate a class method so that it requires the class lock to run. The class should have a reentrant lock with the name ``self._lock``. """ @wraps(method) - def wrapper(self, *args: P.args, **kwargs: P.kwargs) -> T: + def wrapper(self: Self, *args: P.args, **kwargs: P.kwargs) -> T: # Confirm the object the method is attached to has a self._lock property if not hasattr(self, "_lock"): raise AttributeError(f"{self.__class__.__name__} has no '_lock' attribute") @@ -67,7 +69,16 @@ class ErrorCapturingThread(Thread): join is run. """ - def __init__(self, group=None, target=None, args=None, kwargs=None, daemon=None): + def __init__( + self, + group: Optional[Any] = None, + target: Optional[Callable[..., Any]] = None, + name: Optional[str] = None, + args: tuple[Any, ...] = (), + kwargs: Optional[dict[str, Any]] = None, + *, + daemon: Optional[bool] = None, + ): """Initialise with the same arguments as Thread.""" # As all inputs are keywords we need to set the default values for args and kwargs: if args is None: @@ -88,12 +99,13 @@ class ErrorCapturingThread(Thread): super().__init__( group=group, target=_wrap_and_catch_errors, + name=name, args=args, kwargs=kwargs, daemon=daemon, ) - def join(self, timeout=None): + def join(self, timeout: Optional[float] = None): """Join when the thread is complete. If the thread ended due to an unhandled exception, the exception will be raised @@ -107,7 +119,9 @@ class ErrorCapturingThread(Thread): raise err -def _wrap_and_catch_errors(target, error_buffer, *args, **kwargs): +def _wrap_and_catch_errors( + target: Callable[..., Any], error_buffer: list, *args, **kwargs +): """Run target function in a try-except block. This function is designed only to be used by ErrorCapturingThread. diff --git a/tests/test_stage.py b/tests/test_stage.py index c9ae0918..1beebf9b 100644 --- a/tests/test_stage.py +++ b/tests/test_stage.py @@ -1,6 +1,5 @@ """Test the stage without creating a full HTTP server and socket connection.""" -from collections.abc import Mapping import tempfile import itertools @@ -69,7 +68,7 @@ def test_override_base_movement(): self, cancel: lt.deps.CancelHook, block_cancellation: bool = False, - **kwargs: Mapping[str, int], + **kwargs: int, ): pass @@ -82,7 +81,7 @@ def test_override_base_movement(): self, cancel: lt.deps.CancelHook, block_cancellation: bool = False, - **kwargs: Mapping[str, int], + **kwargs: int, ): pass From d6731966715e260389bed2d17d9b9f6fcb62370e Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 27 Aug 2025 13:33:11 +0100 Subject: [PATCH 2/7] Add ANN002, and ANN003 rules --- pyproject.toml | 2 +- src/openflexure_microscope_server/server/__init__.py | 6 +++--- src/openflexure_microscope_server/things/autofocus.py | 4 ++-- src/openflexure_microscope_server/things/stage/dummy.py | 4 ++-- .../things/stage/sangaboard.py | 4 ++-- src/openflexure_microscope_server/utilities.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b1d8aaa9..58430873 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,7 +126,7 @@ select = [ "N", # PEP8 naming "D", # Docstring checks "ERA001", # Commented out code! - "ANN001", + "ANN001", "ANN002", "ANN003", ] ignore = [ diff --git a/src/openflexure_microscope_server/server/__init__.py b/src/openflexure_microscope_server/server/__init__.py index d5058fe8..e7964da3 100644 --- a/src/openflexure_microscope_server/server/__init__.py +++ b/src/openflexure_microscope_server/server/__init__.py @@ -2,13 +2,13 @@ from __future__ import annotations -from typing import Optional, Callable +from typing import Optional, Callable, Any from functools import wraps from copy import copy +import logging import labthings_fastapi as lt import uvicorn -import logging from uvicorn.main import Server from .serve_static_files import add_static_files from .legacy_api import add_v2_endpoints @@ -32,7 +32,7 @@ def set_shutdown_function(shutdown_function: Callable[[], None]): original_handler = Server.handle_exit @wraps(Server.handle_exit) - def handle_exit(*args, **kwargs): + def handle_exit(*args: Any, **kwargs: Any): shutdown_function() original_handler(*args, **kwargs) diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index e80d1e47..48c475ac 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -252,7 +252,7 @@ class JPEGSharpnessMonitor: finally: self.running = False - def focus_rel(self, dz: int, **kwargs) -> tuple[int, int]: + def focus_rel(self, dz: int, block_cancellation: int = False) -> tuple[int, int]: """Move the stage by dz, monitoring the position over time. This performs exactly one move. Multiple calls of this method @@ -268,7 +268,7 @@ class JPEGSharpnessMonitor: self.stage_positions.append(self.stage.position) # Main move - self.stage.move_relative(z=dz, **kwargs) + self.stage.move_relative(z=dz, block_cancellation=block_cancellation) # Store the end time and position self.stage_times.append(time.time()) diff --git a/src/openflexure_microscope_server/things/stage/dummy.py b/src/openflexure_microscope_server/things/stage/dummy.py index 3015b42c..45f7f38c 100644 --- a/src/openflexure_microscope_server/things/stage/dummy.py +++ b/src/openflexure_microscope_server/things/stage/dummy.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional +from typing import Optional, Any from types import TracebackType from collections.abc import Mapping import time @@ -19,7 +19,7 @@ class DummyStage(BaseStage): hardware attached. """ - def __init__(self, step_time: float = 0.001, **kwargs): + def __init__(self, step_time: float = 0.001, **kwargs: Any): """Initialise the Dummy stage, setting the step_time to adjust the speed. :param step_time: The time in seconds per "motor" step. The default of 0.001 diff --git a/src/openflexure_microscope_server/things/stage/sangaboard.py b/src/openflexure_microscope_server/things/stage/sangaboard.py index 8fea6684..05fb55a9 100644 --- a/src/openflexure_microscope_server/things/stage/sangaboard.py +++ b/src/openflexure_microscope_server/things/stage/sangaboard.py @@ -5,7 +5,7 @@ import logging import threading import time from copy import copy -from typing import Iterator, Literal, Optional +from typing import Iterator, Literal, Optional, Any from types import TracebackType from contextlib import contextmanager from collections.abc import Mapping @@ -25,7 +25,7 @@ class SangaboardThing(BaseStage): functionality is accessed by directly querying the serial interface. """ - def __init__(self, port: str = None, **kwargs): + def __init__(self, port: str = None, **kwargs: Any): """Initialise SangaboardThing. Initialise the "Thing", but do not initialise an underlying diff --git a/src/openflexure_microscope_server/utilities.py b/src/openflexure_microscope_server/utilities.py index 5975ed1e..065b2683 100644 --- a/src/openflexure_microscope_server/utilities.py +++ b/src/openflexure_microscope_server/utilities.py @@ -120,7 +120,7 @@ class ErrorCapturingThread(Thread): def _wrap_and_catch_errors( - target: Callable[..., Any], error_buffer: list, *args, **kwargs + target: Callable[..., Any], error_buffer: list, *args: Any, **kwargs: Any ): """Run target function in a try-except block. From 4c46330959c2a7f1adb9fb9a571f7ad687b7c9c1 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 27 Aug 2025 14:00:49 +0100 Subject: [PATCH 3/7] Add ANN20X rules to ruff --- change_log_helper.py | 2 +- pull_webapp.py | 2 +- pyproject.toml | 6 ++-- src/openflexure_microscope_server/logging.py | 12 +++---- .../scan_directories.py | 14 ++++---- .../scan_planners.py | 2 +- .../server/__init__.py | 10 +++--- .../server/serve_static_files.py | 4 +-- .../stitching.py | 4 +-- .../things/autofocus.py | 8 ++--- .../things/camera/__init__.py | 4 +-- .../things/camera/opencv.py | 2 +- .../things/camera/picamera.py | 20 +++++------ .../things/camera/simulation.py | 24 +++++++------- .../things/camera_stage_mapping.py | 9 ++--- .../things/smart_scan.py | 33 ++++++++++++------- .../things/stage/__init__.py | 16 ++++----- .../things/stage/dummy.py | 6 ++-- .../things/system.py | 2 +- .../utilities.py | 4 +-- 20 files changed, 98 insertions(+), 86 deletions(-) diff --git a/change_log_helper.py b/change_log_helper.py index 79296132..5caaac6a 100755 --- a/change_log_helper.py +++ b/change_log_helper.py @@ -12,7 +12,7 @@ from gitlab.exceptions import GitlabGetError PROJECT_ID = 9238334 -def main(tag: str, branch: str = "v3"): +def main(tag: str, branch: str = "v3") -> None: """Create a list of the MRs into a given branch after a tag. :param tag: The tag the mrs should be since. diff --git a/pull_webapp.py b/pull_webapp.py index 6584342c..4c8db77a 100755 --- a/pull_webapp.py +++ b/pull_webapp.py @@ -43,7 +43,7 @@ def extract_zip(zip_data: bytes) -> None: print(f"Extracted zip to: {THIS_DIR}") -def main(branch: str = "v3"): +def main(branch: str = "v3") -> None: """Fetch build artifacts from GitLab for a given branch and unzip. Clear any existing static directory before extract the new files. diff --git a/pyproject.toml b/pyproject.toml index 58430873..884a32ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,7 +126,7 @@ select = [ "N", # PEP8 naming "D", # Docstring checks "ERA001", # Commented out code! - "ANN001", "ANN002", "ANN003", + "ANN", ] ignore = [ @@ -134,7 +134,9 @@ ignore = [ "D203", # incompatible with D204 "D213", # incompatible with D212 "D400", # A stricter version of #415 that doesn't allow ! - # The checkers below should be turned on as they complain about missing docstrings. + "ANN204", # Requrite type hints for + "ANN401", # Disalows Any, Any is needed at times, Once MyPy is running Any will be + # handled appropriately ] [tool.ruff.lint.per-file-ignores] diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index 0df9a60e..b1b5074b 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -21,7 +21,7 @@ from fastapi import HTTPException OFM_LOG_FILE = None -def configure_logging(log_folder: str): +def configure_logging(log_folder: str) -> None: """Configure logging for the server while it is running. This modifies the root logger to have a rotating file handler and @@ -103,7 +103,7 @@ def retrieve_log_from_file() -> PlainTextResponse: class OFMLogFileFormatter(logging.Formatter): """The formatter used for the OpenFlexure Microscope Server log file.""" - def format(self, record: logging.LogRecord): + def format(self, record: logging.LogRecord) -> str: """Adjust the logging formatting for uvicorn logs. uvicorn has two loggers. Each API access is ``uvicorn.access`` which we filter @@ -132,7 +132,7 @@ class OFMHandler(logging.Handler): self._log = [] self._max_logs = max_logs - def append_record(self, record: logging.LogRecord): + def append_record(self, record: logging.LogRecord) -> None: """Format message and append it to a list of records. The built in formatter is used to format the record. @@ -143,7 +143,7 @@ class OFMHandler(logging.Handler): while len(self._log) > self._max_logs: self._log.pop(0) - def emit(self, record: logging.LogRecord): + def emit(self, record: logging.LogRecord) -> None: """Emit will save the logged record to the log.""" try: if record.levelno >= self.level: @@ -156,7 +156,7 @@ class OFMHandler(logging.Handler): self.handleError(record) @property - def log_history(self): + def log_history(self) -> str: """Return the log history up to the maximum number of logs.""" return "\n".join(self._log) @@ -164,7 +164,7 @@ class OFMHandler(logging.Handler): class UvicornAccessFilter(logging.Filter): """A logging filter to filter out "uvicorn.access" messages.""" - def filter(self, record: logging.LogRecord): + def filter(self, record: logging.LogRecord) -> bool: """Return False if record is from "uvicorn.access".""" return not record.name.startswith("uvicorn.access") diff --git a/src/openflexure_microscope_server/scan_directories.py b/src/openflexure_microscope_server/scan_directories.py index cb96bd25..f34c0836 100644 --- a/src/openflexure_microscope_server/scan_directories.py +++ b/src/openflexure_microscope_server/scan_directories.py @@ -105,7 +105,7 @@ class ScanData(BaseModel): This should be set with ``set_final_data()`` to ensure duration is set. """ - def set_final_data(self, result: str): + def set_final_data(self, result: str) -> None: """Set the final data for the scan, scan duration is automatically calculated. :param result: A string describing the result. @@ -423,13 +423,13 @@ class ScanDirectory: """The time the directory was created on disk.""" return os.path.getctime(self.dir_path) - def get_scan_files(self): + def get_scan_files(self) -> list[str]: """Return a list of the files in the images dir.""" if self.images_dir is None: return [] return os.listdir(self.images_dir) - def _extract_scan_images(self, file_list: list[str]): + def _extract_scan_images(self, file_list: list[str]) -> list[str]: """Extract files which match the naming convention for scan images. :param file_list: The list of files to search. Normally this would be @@ -439,7 +439,7 @@ class ScanDirectory: """ return [i for i in file_list if IMAGE_REGEX.search(i)] - def _extract_final_stitches(self, file_list: list[str]): + def _extract_final_stitches(self, file_list: list[str]) -> list[str]: """Extract files which match the naming convention for final stitches. :param file_list: The list of files to search. @@ -448,7 +448,7 @@ class ScanDirectory: """ return [i for i in file_list if STITCH_REGEX.search(i)] - def _extract_dzi_files(self, file_list: list[str]): + def _extract_dzi_files(self, file_list: list[str]) -> list[str]: """Extract files which match the naming convention for dzi_files. :param file_list: The list of files to search. @@ -509,7 +509,7 @@ class ScanDirectory: files.append(os.path.relpath(full_path, self.dir_path)) return files - def save_scan_data(self, scan_data: ScanData): + def save_scan_data(self, scan_data: ScanData) -> None: """Save the scan data for this scan to disk.""" if self.scan_data_path is None: raise FileNotFoundError( @@ -551,7 +551,7 @@ class ScanDirectory: return zip_fname -def get_files_in_zip(zip_path: str): +def get_files_in_zip(zip_path: str) -> list[str]: """List the relative paths of all files and folders in the zip folder specified.""" scan_zip = zipfile.ZipFile(zip_path) return [os.path.normpath(i) for i in scan_zip.namelist()] diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 87f9000b..67efbf65 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -332,7 +332,7 @@ class SmartSpiral(ScanPlanner): """Sort the remaining positions based on the current location.""" # Defined rather than use a lambda for readability - def sort_key(pos: XYPos): + def sort_key(pos: XYPos) -> tuple[float, float, float]: return ( self.moves_between(current_pos, pos), self.moves_between(self._initial_position, pos), diff --git a/src/openflexure_microscope_server/server/__init__.py b/src/openflexure_microscope_server/server/__init__.py index e7964da3..3dfbb721 100644 --- a/src/openflexure_microscope_server/server/__init__.py +++ b/src/openflexure_microscope_server/server/__init__.py @@ -15,7 +15,7 @@ from .legacy_api import add_v2_endpoints from ..logging import configure_logging, retrieve_log, retrieve_log_from_file -def set_shutdown_function(shutdown_function: Callable[[], None]): +def set_shutdown_function(shutdown_function: Callable[[], None]) -> None: """Ensure a function is called before the shutdown. This monkey patches the Uvicorn Server's handle_exit. This is needed because @@ -32,7 +32,7 @@ def set_shutdown_function(shutdown_function: Callable[[], None]): original_handler = Server.handle_exit @wraps(Server.handle_exit) - def handle_exit(*args: Any, **kwargs: Any): + def handle_exit(*args: Any, **kwargs: Any) -> None: shutdown_function() original_handler(*args, **kwargs) @@ -41,7 +41,7 @@ def set_shutdown_function(shutdown_function: Callable[[], None]): def customise_server( server: lt.ThingServer, log_folder: str, scans_folder: Optional[str] -): +) -> None: """Customise the server with additional endpoints, etc.""" configure_logging(log_folder) add_v2_endpoints(server) @@ -66,7 +66,7 @@ def _get_scans_dir(config: dict) -> Optional[str]: return None -def serve_from_cli(argv: Optional[list[str]] = None): +def serve_from_cli(argv: Optional[list[str]] = None) -> None: """Start the server from the command line.""" args = lt.cli.parse_args(argv) @@ -85,7 +85,7 @@ def serve_from_cli(argv: Optional[list[str]] = None): server = lt.cli.server_from_config(config) customise_server(server, log_folder, scans_folder) - def shutdown_call(): + def shutdown_call() -> None: try: # Kill any mjpeg streams so that StreamingResponses close. server.things["/camera/"].kill_mjpeg_streams() diff --git a/src/openflexure_microscope_server/server/serve_static_files.py b/src/openflexure_microscope_server/server/serve_static_files.py index 5defc149..542aa383 100644 --- a/src/openflexure_microscope_server/server/serve_static_files.py +++ b/src/openflexure_microscope_server/server/serve_static_files.py @@ -50,7 +50,7 @@ def add_static_files(app: FastAPI, scans_folder: Optional[str]) -> None: check_static_dir() @app.get("/", response_class=RedirectResponse) - async def redirect_fastapi(): + async def redirect_fastapi() -> str: return "/index.html" # Mounting the webapp at / file by file to allow other endpoints to be created @@ -77,7 +77,7 @@ def add_static_files(app: FastAPI, scans_folder: Optional[str]) -> None: ) -def check_static_dir(): +def check_static_dir() -> None: """Check that the static dir exists and contains expected files and dirs.""" if not os.path.isdir(STATIC_PATH): raise FileNotFoundError( diff --git a/src/openflexure_microscope_server/stitching.py b/src/openflexure_microscope_server/stitching.py index 32a5253e..f8754425 100644 --- a/src/openflexure_microscope_server/stitching.py +++ b/src/openflexure_microscope_server/stitching.py @@ -34,7 +34,7 @@ class StitcherValidationError(RuntimeError): """The stitcher received values that it deems unsafe to create a command from.""" -def validate_command(cmd: list[str]): +def validate_command(cmd: list[str]) -> None: """Validate that the command only characters that are allowed in a path. The values in the commands should be numbers, commandline flags, paths, and @@ -340,7 +340,7 @@ class FinalStitcher(BaseStitcher): # Print everything in the buffer when program finishes self.log_buffer(process.stdout) - def log_buffer(self, buffer: TextIOWrapper): + def log_buffer(self, buffer: TextIOWrapper) -> None: """Log everything in the buffer at INFO level.""" while line := buffer.readline(): self.logger.info(line) diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 48c475ac..ca1adfcd 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -127,7 +127,7 @@ class StackParams: """ return self.min_images_to_test + 15 - def slice_to_save(self, sharpest_index: int): + def slice_to_save(self, sharpest_index: int) -> slice: """Return the slice of images to save given the index of the sharpest image.""" images_each_side = (self.images_to_save - 1) // 2 return slice( @@ -234,7 +234,7 @@ class JPEGSharpnessMonitor: running = False - async def monitor_sharpness(self): + async def monitor_sharpness(self) -> None: """Start monitoring the frame sizes.""" self.running = True async for frame in self.camera.lores_mjpeg_stream.frame_async_generator(): @@ -244,7 +244,7 @@ class JPEGSharpnessMonitor: break @contextmanager - def run(self): + def run(self) -> None: """Context manager, during which we will monitor sharpness from the camera.""" self.portal.start_task_soon(self.monitor_sharpness) try: @@ -403,7 +403,7 @@ class AutofocusThing(lt.Thing): sharpness_monitor: SharpnessMonitorDep, dz: int = 2000, start: Literal["centre", "base"] = "centre", - ): + ) -> tuple[list[float], list[float]]: """Repeatedly autofocus the stage until it looks focused. This action will run the ``fast_autofocus`` action until it settles on a point diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 94badd0c..e5e171e0 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -133,7 +133,7 @@ class CameraMemoryBuffer: "No image with matching id in memory to retrieve." ) from e - def clear(self): + def clear(self) -> None: """Clear all images from memory.""" self._storage.clear() @@ -208,7 +208,7 @@ class BaseCamera(lt.Thing): "CameraThings must define their own start_streaming method" ) - def kill_mjpeg_streams(self): + def kill_mjpeg_streams(self) -> None: """Kill the streams now as the server is shutting down. This is called when uvicorn gets the a shutdown signal. As this is called from diff --git a/src/openflexure_microscope_server/things/camera/opencv.py b/src/openflexure_microscope_server/things/camera/opencv.py index e87befec..763096e5 100644 --- a/src/openflexure_microscope_server/things/camera/opencv.py +++ b/src/openflexure_microscope_server/things/camera/opencv.py @@ -65,7 +65,7 @@ class OpenCVCamera(BaseCamera): return self._capture_thread.is_alive() return False - def _capture_frames(self): + def _capture_frames(self) -> None: portal = lt.get_blocking_portal(self) while self._capture_enabled: ret, frame = self.cap.read() diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 25b29a44..4ff2844d 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -72,7 +72,7 @@ class PicameraStreamOutput(Output): _timestamp: Optional[int] = None, _packet: Any = None, _audio: bool = False, - ): + ) -> None: """Add a frame to the stream's ringbuffer.""" self.stream.add_frame(frame, self.portal) @@ -173,7 +173,7 @@ class StreamingPiCamera2(BaseCamera): ) """Whether the MJPEG stream is active.""" - def save_settings(self): + def save_settings(self) -> None: """Override save_settings to ensure that camera properties don't recurse. This method is run by any Thing when a ThingSetting is saved. However, the @@ -204,7 +204,7 @@ class StreamingPiCamera2(BaseCamera): return self._analogue_gain @analogue_gain.setter - def analogue_gain(self, value: float): + def analogue_gain(self, value: float) -> None: self._analogue_gain = value if self.streaming: with self._streaming_picamera() as cam: @@ -224,7 +224,7 @@ class StreamingPiCamera2(BaseCamera): return self._colour_gains @colour_gains.setter - def colour_gains(self, value: tuple[float, float]): + def colour_gains(self, value: tuple[float, float]) -> None: self._colour_gains = value if self.streaming: with self._streaming_picamera() as cam: @@ -248,7 +248,7 @@ class StreamingPiCamera2(BaseCamera): return self._exposure_time @exposure_time.setter - def exposure_time(self, value: int): + def exposure_time(self, value: int) -> None: self._exposure_time = value if self.streaming: with self._streaming_picamera() as cam: @@ -292,7 +292,7 @@ class StreamingPiCamera2(BaseCamera): return SensorModeSelector(**self._sensor_mode) @sensor_mode.setter - def sensor_mode(self, new_mode: Optional[SensorModeSelector | dict]): + def sensor_mode(self, new_mode: Optional[SensorModeSelector | dict]) -> None: """Change the sensor mode used.""" if new_mode is None: self._sensor_mode = None @@ -352,7 +352,7 @@ class StreamingPiCamera2(BaseCamera): ) from e return None - def _initialise_picamera(self): + def _initialise_picamera(self) -> None: """Acquire the picamera device and store it as ``self._picamera``. This duplicates logic in ``Picamera2.__init__`` to provide a tuning file that @@ -646,7 +646,7 @@ class StreamingPiCamera2(BaseCamera): self, target_white_level: int = 700, percentile: float = 99.9, - ): + ) -> None: """Adjust exposure until a the target white level is reached. Starting from the minimum exposure, gradually increase exposure until @@ -672,7 +672,7 @@ class StreamingPiCamera2(BaseCamera): self, method: Literal["percentile", "centre"] = "centre", luminance_power: float = 1.0, - ): + ) -> None: """Correct the white balance of the image. This calibration requires a neutral image, such that the 99th centile @@ -747,7 +747,7 @@ class StreamingPiCamera2(BaseCamera): self._initialise_picamera() @lt.thing_action - def reset_ccm(self): + def reset_ccm(self) -> None: """Overwrite the colour correction matrix in camera tuning with default values. These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 31e1c9aa..2882a216 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -8,7 +8,7 @@ See repository root for licensing information. from __future__ import annotations import logging -from typing import Literal, Optional +from typing import Literal, Optional, Mapping from types import TracebackType from threading import Thread import time @@ -76,7 +76,7 @@ class SimulatedCamera(BaseCamera): self.generate_blobs() self.generate_canvas() - def generate_sprites(self): + def generate_sprites(self) -> None: """Generate sprites to populate the image.""" sprite_sizes = [5, 7, 10, 21, 36, 40] self.sprites = [] @@ -109,7 +109,7 @@ class SimulatedCamera(BaseCamera): # Convert to uint8 and append to the list self.sprites.append(sprite.astype(np.uint8)) - def generate_blobs(self, n_blobs: int = 1000): + def generate_blobs(self, n_blobs: int = 1000) -> None: """Generate coordinates of blobs and their sizes. A 1000x3 array is returned. Each row represents (x,y) coordinate @@ -125,7 +125,7 @@ class SimulatedCamera(BaseCamera): self.blobs[:, 1] = RNG.uniform(w / 2, self.canvas_shape[1] - w / 2, n_blobs) self.blobs[:, 2] = RNG.choice(len(self.sprites), n_blobs) - def generate_canvas(self): + def generate_canvas(self) -> None: """Generate a canvas. Canvas is int16 so that random noise can be added to simulation image before @@ -145,7 +145,7 @@ class SimulatedCamera(BaseCamera): self.canvas[self.canvas < 0] = 0 self.canvas[self.canvas > 255] = 255 - def generate_image(self, pos: tuple[int, int, int]): + def generate_image(self, pos: tuple[int, int, int]) -> np.ndarray: """Generate an image with blobs based on supplied coordinates.""" canvas_width, canvas_height, _ = self.canvas_shape image_width, image_height, _ = self.shape @@ -180,16 +180,16 @@ class SimulatedCamera(BaseCamera): def attach_to_server( self, server: lt.ThingServer, path: str, setting_storage_path: str - ): + ) -> None: """Wrap the attach_to_server method so the server instance can be stored. Direct access to the server instance is needed to get the stage position while maintaining the same public API as a real camera that doesn't need this access. """ self._server = server - return super().attach_to_server(server, path, setting_storage_path) + super().attach_to_server(server, path, setting_storage_path) - def get_stage_position(self): + def get_stage_position(self) -> Mapping[str, int]: """Return the stage position. The simulation camera has access to the stage position so it can generate a @@ -199,7 +199,7 @@ class SimulatedCamera(BaseCamera): self._stage = self._server.things["/stage/"] return self._stage.instantaneous_position - def generate_frame(self): + def generate_frame(self) -> np.ndarray: """Generate a frame with blobs based on the stage coordinates.""" try: pos = self.get_stage_position() @@ -256,7 +256,7 @@ class SimulatedCamera(BaseCamera): noise_level = lt.ThingProperty(float, 2.0) - def _capture_frames(self): + def _capture_frames(self) -> None: portal = lt.get_blocking_portal(self) while self._capture_enabled: time.sleep(self.frame_interval) @@ -312,14 +312,14 @@ class SimulatedCamera(BaseCamera): return Image.fromarray(self.generate_frame()) @lt.thing_action - def remove_sample(self): + def remove_sample(self) -> None: """Show the simulated background with no sample.""" if not self._show_sample: raise RuntimeError("Sample is already removed.") self._show_sample = False @lt.thing_action - def load_sample(self): + def load_sample(self) -> None: """Show the simulated sample.""" if self._show_sample: raise RuntimeError("Sample is already in place.") diff --git a/src/openflexure_microscope_server/things/camera_stage_mapping.py b/src/openflexure_microscope_server/things/camera_stage_mapping.py index c74d6c1b..d9281e22 100644 --- a/src/openflexure_microscope_server/things/camera_stage_mapping.py +++ b/src/openflexure_microscope_server/things/camera_stage_mapping.py @@ -18,6 +18,7 @@ from typing import ( NamedTuple, Optional, Tuple, + Mapping, ) from fastapi import HTTPException @@ -85,7 +86,7 @@ class RecordedMove: positions: List[CoordinateType] = [p for t, p in self._history if p is not None] return MoveHistory(times, positions) - def clear_history(self): + def clear_history(self) -> None: """Reset our history to be an empty list.""" self._history = [] @@ -241,7 +242,7 @@ class CameraStageMapper(lt.Thing): return None return self.last_calibration["image_resolution"] - def assert_calibrated(self): + def assert_calibrated(self) -> None: """Raise an exception if the image_to_stage_displacement matrix is not set.""" if self.image_to_stage_displacement_matrix is None: # Disable check of no message in raised exception as the message is explicitly @@ -254,7 +255,7 @@ class CameraStageMapper(lt.Thing): stage: Stage, x: float, y: float, - ): + ) -> None: """Move by a given number of pixels on the camera. NB x and y here refer to what is usually understood to be the horizontal and @@ -273,7 +274,7 @@ class CameraStageMapper(lt.Thing): stage.move_relative(x=relative_move[0], y=relative_move[1]) @lt.thing_property - def thing_state(self) -> dict[str, Any]: + def thing_state(self) -> Mapping[str, Any]: """Summary metadata describing the current state of the Thing.""" return { k: getattr(self, k) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index a7b70fb8..3e0badba 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -38,8 +38,17 @@ CSMDep = lt.deps.direct_thing_client_dependency( ) AutofocusDep = lt.deps.direct_thing_client_dependency(AutofocusThing, "/autofocus/") -JPEGBlob = lt.blob.blob_type("image/jpeg") -ZipBlob = lt.blob.blob_type("application/zip") + +class JPEGBlob(lt.blob.Blob): + """A class representing a JPEG image as a LabThings FastAPI Blob.""" + + media_type: str = "image/jpeg" + + +class ZipBlob(lt.blob.Blob): + """A class representing a Zip file as a LabThings FastAPI Blob.""" + + media_type: str = "application/zip" class ScanNotRunningError(RuntimeError): @@ -116,7 +125,7 @@ class SmartScanThing(lt.Thing): cam: CameraClient, csm: CSMDep, scan_name: str = "", - ): + ) -> None: """Move the stage to cover an area, taking images that can be tiled together. The stage will move in a pattern that grows outwards from the starting point, @@ -171,7 +180,7 @@ class SmartScanThing(lt.Thing): self._preview_stitcher = None @_scan_running - def _check_background_and_csm_set(self): + def _check_background_and_csm_set(self) -> None: """Before starting a scan, check that background and camera-stage-mapping are set. Raise error if: @@ -311,7 +320,7 @@ class SmartScanThing(lt.Thing): ) @_scan_running - def _save_final_scan_data(self, scan_result: str): + def _save_final_scan_data(self, scan_result: str) -> None: """Update scan data JSON file with data only known at the end of the scan. Takes scan_result, a string that is either "success", "cancelled by user", @@ -321,7 +330,7 @@ class SmartScanThing(lt.Thing): self._ongoing_scan.save_scan_data(self._scan_data) @_scan_running - def _manage_stitching_threads(self): + def _manage_stitching_threads(self) -> None: """Manage the stitching threads, starting them if needed and not already running.""" # Assume 4 images means at least one offset in x and y, making the stitching # well constrained. @@ -330,7 +339,7 @@ class SmartScanThing(lt.Thing): self._preview_stitcher.start() @_scan_running - def _run_scan(self): + def _run_scan(self) -> None: """Prepare and run the main scan, and perform final actions on completion. The result (or exception) from the main scan loop determines whether the @@ -382,7 +391,7 @@ class SmartScanThing(lt.Thing): self.purge_empty_scans(logger=self._scan_logger) @_scan_running - def _main_scan_loop(self): + def _main_scan_loop(self) -> None: """Run the main loop of the scan. This loop runs during a scan, until no more scan x,y positions @@ -449,7 +458,7 @@ class SmartScanThing(lt.Thing): self._ongoing_scan.zip_files() @_scan_running - def _return_to_starting_position(self): + def _return_to_starting_position(self) -> None: """Return to the initial scan position, if set.""" self._scan_logger.info("Returning to starting position.") if self._scan_data is not None: @@ -458,7 +467,7 @@ class SmartScanThing(lt.Thing): ) @_scan_running - def _perform_final_stitch(self): + def _perform_final_stitch(self) -> None: """Update the scan zip and perform final stitch of the data.""" if self._scan_data.image_count <= 3: self._scan_logger.info("Not performing a stitch as 3 or fewer images taken") @@ -729,7 +738,7 @@ class SmartScanThing(lt.Thing): def download_zip( self, scan_name: str, - ): + ) -> ZipBlob: """Return zip after including any files left until the end. The zipfile is returned as a Blob. @@ -740,7 +749,7 @@ class SmartScanThing(lt.Thing): @lt.thing_action def stitch_all_scans( self, logger: lt.deps.InvocationLogger, cancel: lt.deps.CancelHook - ): + ) -> None: """Check the list of scans, and stitch any that don't have a DZI associated with it. :raises RuntimeError: if the microscope is currently running a scan diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index 92bd76d7..f6a49590 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -11,7 +11,7 @@ As the object will be used as a context manager create the hardware connection i from __future__ import annotations from collections.abc import Sequence, Mapping -from typing import Literal +from typing import Literal, Never, Any import labthings_fastapi as lt @@ -119,12 +119,12 @@ class BaseStage(lt.Thing): ) @property - def thing_state(self): + def thing_state(self) -> Mapping[str, Any]: """Summary metadata describing the current state of the stage.""" return {"position": self.position} @lt.thing_action - def invert_axis_direction(self, axis: Literal["x", "y", "z"]): + def invert_axis_direction(self, axis: Literal["x", "y", "z"]) -> None: """Invert the direction setting of the given axis. :param axis: The axis name (x, y or z) to invert. @@ -143,7 +143,7 @@ class BaseStage(lt.Thing): cancel: lt.deps.CancelHook, block_cancellation: bool = False, **kwargs: int, - ): + ) -> None: """Make a relative move. Keyword arguments should be axis names.""" self._hardware_move_relative( cancel=cancel, @@ -156,7 +156,7 @@ class BaseStage(lt.Thing): cancel: lt.deps.CancelHook, block_cancellation: bool = False, **kwargs: int, - ): + ) -> Never: """Make a relative move in the coordinate system used by the physical hardware. Make sure to use and update ``self._hardware_position`` not ``self.position``. @@ -171,7 +171,7 @@ class BaseStage(lt.Thing): cancel: lt.deps.CancelHook, block_cancellation: bool = False, **kwargs: int, - ): + ) -> None: """Make an absolute move. Keyword arguments should be axis names.""" self._hardware_move_absolute( cancel=cancel, @@ -184,7 +184,7 @@ class BaseStage(lt.Thing): cancel: lt.deps.CancelHook, block_cancellation: bool = False, **kwargs: int, - ): + ) -> Never: """Make a absolute move in the coordinate system used by the physical hardware. Make sure to use and update ``self._hardware_position`` not ``self.position``. @@ -194,7 +194,7 @@ class BaseStage(lt.Thing): ) @lt.thing_action - def set_zero_position(self): + def set_zero_position(self) -> Never: """Make the current position zero in all axes. This action does not move the stage, but resets the position to zero. diff --git a/src/openflexure_microscope_server/things/stage/dummy.py b/src/openflexure_microscope_server/things/stage/dummy.py index 45f7f38c..b238c1c2 100644 --- a/src/openflexure_microscope_server/things/stage/dummy.py +++ b/src/openflexure_microscope_server/things/stage/dummy.py @@ -55,7 +55,7 @@ class DummyStage(BaseStage): cancel: lt.deps.CancelHook, block_cancellation: bool = False, **kwargs: int, - ): + ) -> None: """Make a relative move. Keyword arguments should be axis names.""" displacement = [kwargs.get(k, 0) for k in self.axis_names] self.moving = True @@ -93,7 +93,7 @@ class DummyStage(BaseStage): cancel: lt.deps.CancelHook, block_cancellation: bool = False, **kwargs: int, - ): + ) -> None: """Make an absolute move. Keyword arguments should be axis names.""" displacement = { axis: int(pos) - self._hardware_position[axis] @@ -105,7 +105,7 @@ class DummyStage(BaseStage): ) @lt.thing_action - def set_zero_position(self): + def set_zero_position(self) -> None: """Make the current position zero in all axes. This action does not move the stage, but resets the position to zero. diff --git a/src/openflexure_microscope_server/things/system.py b/src/openflexure_microscope_server/things/system.py index 12aaadf9..48417e31 100644 --- a/src/openflexure_microscope_server/things/system.py +++ b/src/openflexure_microscope_server/things/system.py @@ -51,7 +51,7 @@ class OpenFlexureSystem(lt.Thing): return UUID(self._microscope_id) @microscope_id.setter - def microscope_id(self, uuid: UUID): + def microscope_id(self, uuid: UUID) -> None: # TODO make read only but still settable from disk self._microscope_id = uuid diff --git a/src/openflexure_microscope_server/utilities.py b/src/openflexure_microscope_server/utilities.py index 065b2683..87503aaa 100644 --- a/src/openflexure_microscope_server/utilities.py +++ b/src/openflexure_microscope_server/utilities.py @@ -105,7 +105,7 @@ class ErrorCapturingThread(Thread): daemon=daemon, ) - def join(self, timeout: Optional[float] = None): + def join(self, timeout: Optional[float] = None) -> None: """Join when the thread is complete. If the thread ended due to an unhandled exception, the exception will be raised @@ -121,7 +121,7 @@ class ErrorCapturingThread(Thread): def _wrap_and_catch_errors( target: Callable[..., Any], error_buffer: list, *args: Any, **kwargs: Any -): +) -> None: """Run target function in a try-except block. This function is designed only to be used by ErrorCapturingThread. From 2ce49088e7575fc9334f931b157cf64491cdbc7d Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 27 Aug 2025 14:04:25 +0100 Subject: [PATCH 4/7] Add return typehints to magic methods --- pyproject.toml | 1 - src/openflexure_microscope_server/background_detect.py | 2 +- src/openflexure_microscope_server/logging.py | 2 +- src/openflexure_microscope_server/scan_directories.py | 4 ++-- src/openflexure_microscope_server/scan_planners.py | 2 +- src/openflexure_microscope_server/stitching.py | 10 +++++++--- src/openflexure_microscope_server/things/autofocus.py | 4 +++- .../things/camera/__init__.py | 4 ++-- .../things/camera/opencv.py | 6 +++--- .../things/camera/picamera.py | 10 ++++++---- .../things/camera/simulation.py | 6 +++--- .../things/camera_stage_mapping.py | 6 +++--- src/openflexure_microscope_server/things/smart_scan.py | 2 +- .../things/stage/__init__.py | 2 +- .../things/stage/dummy.py | 6 +++--- .../things/stage/sangaboard.py | 6 +++--- src/openflexure_microscope_server/utilities.py | 2 +- 17 files changed, 41 insertions(+), 34 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 884a32ef..9957a3b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,7 +134,6 @@ ignore = [ "D203", # incompatible with D204 "D213", # incompatible with D212 "D400", # A stricter version of #415 that doesn't allow ! - "ANN204", # Requrite type hints for "ANN401", # Disalows Any, Any is needed at times, Once MyPy is running Any will be # handled appropriately ] diff --git a/src/openflexure_microscope_server/background_detect.py b/src/openflexure_microscope_server/background_detect.py index d9e39e15..d9e44cfc 100644 --- a/src/openflexure_microscope_server/background_detect.py +++ b/src/openflexure_microscope_server/background_detect.py @@ -55,7 +55,7 @@ class BackgroundDetectAlgorithm: settings_data_model: BaseModel = BaseModel """The data model of algorithm settings. This must be set by child classes""" - def __init__(self): + def __init__(self) -> None: """Initialise the algorithm settings.""" try: self._settings: BaseModel = self.settings_data_model() diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index b1b5074b..0dd3ee12 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -120,7 +120,7 @@ class OFMLogFileFormatter(logging.Formatter): class OFMHandler(logging.Handler): """A logging.Handler that stores the most recent logs for access by the server.""" - def __init__(self, level: int = logging.INFO, max_logs: int = 250): + def __init__(self, level: int = logging.INFO, max_logs: int = 250) -> None: """Initialise the handler with a set logging level and message buffer size. :param level: The level of logs captured. As standard logs of INFO and above diff --git a/src/openflexure_microscope_server/scan_directories.py b/src/openflexure_microscope_server/scan_directories.py index f34c0836..8256f414 100644 --- a/src/openflexure_microscope_server/scan_directories.py +++ b/src/openflexure_microscope_server/scan_directories.py @@ -167,7 +167,7 @@ class ScanDirectoryManager: _base_scan_dir: str - def __init__(self, base_scan_dir: str): + def __init__(self, base_scan_dir: str) -> None: """Initialise the scan directory manager. :param base_scan_dir: Path of the directory that holds all scans. @@ -374,7 +374,7 @@ class ScanDirectory: _name: str _base_scan_dir: str - def __init__(self, name: str, base_scan_dir: str): + def __init__(self, name: str, base_scan_dir: str) -> None: """Initialise the scan directory. :param name: the name of the scan (the scan directory basename). diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 67efbf65..7682443c 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -80,7 +80,7 @@ class ScanPlanner: def __init__( self, initial_position: XYPos, planner_settings: Optional[dict] = None - ): + ) -> None: """Set up lists for the path planning, and scan history.""" self._initial_position = enforce_xy_tuple(initial_position) self._parse(planner_settings) diff --git a/src/openflexure_microscope_server/stitching.py b/src/openflexure_microscope_server/stitching.py index f8754425..c81bd34d 100644 --- a/src/openflexure_microscope_server/stitching.py +++ b/src/openflexure_microscope_server/stitching.py @@ -59,7 +59,9 @@ class BaseStitcher: complete. """ - def __init__(self, images_dir: str, *, overlap: float, correlation_resize: float): + def __init__( + self, images_dir: str, *, overlap: float, correlation_resize: float + ) -> None: """Initialise a stitcher. All args except images_dir are positional only. @@ -128,7 +130,9 @@ class PreviewStitcher(BaseStitcher): one preview must finish before another can be started. """ - def __init__(self, images_dir: str, *, overlap: float, correlation_resize: float): + def __init__( + self, images_dir: str, *, overlap: float, correlation_resize: float + ) -> None: """Initialise a preview stitcher. All args except images_dir are positional only. @@ -192,7 +196,7 @@ class FinalStitcher(BaseStitcher): correlation_resize: Optional[float] = None, stitch_tiff: bool = False, scan_data_dict: Optional[dict[str, Any]] = None, - ): + ) -> None: """Initialise a final stitcher, this has more args than the base class. All args except images_dir are positional only. diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index ca1adfcd..e0b956f1 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -215,7 +215,9 @@ class JPEGSharpnessMonitor: SharpnessMonitorDep as an argument is called. """ - def __init__(self, stage: Stage, camera: RawCamera, portal: lt.deps.BlockingPortal): + def __init__( + self, stage: Stage, camera: RawCamera, portal: lt.deps.BlockingPortal + ) -> None: """Initialise a new JPEGSharpnessMonitor. The args are injected automatically. :param stage: A direct_thing_client dependency for the the microscope stage. diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index e5e171e0..ace9d74c 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -69,7 +69,7 @@ class CameraMemoryBuffer: _storage: dict[int, tuple[Any, Optional[dict]]] - def __init__(self): + def __init__(self) -> None: """Create the buffer instance.""" # This dictionary is the main store for data. Dictionaries are ordered since # Python 3.6, so the order in the dictionary is the capture order @@ -170,7 +170,7 @@ class BaseCamera(lt.Thing): lores_mjpeg_stream = lt.outputs.MJPEGStreamDescriptor() _memory_buffer = CameraMemoryBuffer() - def __init__(self): + def __init__(self) -> None: """Initialise the base camera, this creates the background detectors. This must be run by all child camera classes. diff --git a/src/openflexure_microscope_server/things/camera/opencv.py b/src/openflexure_microscope_server/things/camera/opencv.py index 763096e5..c159ce41 100644 --- a/src/openflexure_microscope_server/things/camera/opencv.py +++ b/src/openflexure_microscope_server/things/camera/opencv.py @@ -25,7 +25,7 @@ from . import BaseCamera class OpenCVCamera(BaseCamera): """A Thing that provides and interface to an OpenCV Camera.""" - def __init__(self, camera_index: int = 0): + def __init__(self, camera_index: int = 0) -> None: """Iniatilise the thing storing the index of the camera to use. :param camera_index: The index of the camera to use for the microscope. @@ -35,7 +35,7 @@ class OpenCVCamera(BaseCamera): self._capture_thread: Optional[Thread] = None self._capture_enabled = False - def __enter__(self): + def __enter__(self) -> None: """Start the capture thread when the Thing context manager is opened.""" self.cap = cv2.VideoCapture(self.camera_index) self._capture_enabled = True @@ -48,7 +48,7 @@ class OpenCVCamera(BaseCamera): _exc_type: type[BaseException], _exc_value: Optional[BaseException], _traceback: Optional[TracebackType], - ): + ) -> None: """Release the camera when the Thing context manager is closed. Before releasing the camera the capture thread is closed. diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 4ff2844d..e8ab2e2f 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -53,7 +53,9 @@ class MissingCalibrationError(RuntimeError): class PicameraStreamOutput(Output): """An Output class that sends frames to a stream.""" - def __init__(self, stream: lt.outputs.MJPEGStream, portal: lt.deps.BlockingPortal): + def __init__( + self, stream: lt.outputs.MJPEGStream, portal: lt.deps.BlockingPortal + ) -> None: """Create an output that puts frames in an MJPEGStream. We need to pass the stream object, and also the blocking portal, because @@ -126,7 +128,7 @@ class StreamingPiCamera2(BaseCamera): generalisation. """ - def __init__(self, camera_num: int = 0): + def __init__(self, camera_num: int = 0) -> None: """Initialise the camera with the given camera number. This makes no connection to the camera (except to get the default tuning file). @@ -384,7 +386,7 @@ class StreamingPiCamera2(BaseCamera): ) self._picamera_lock = RLock() - def __enter__(self): + def __enter__(self) -> None: """Start streaming when the Thing context manager is opened. This opens the picamera connection, initialises the camera, sets the @@ -430,7 +432,7 @@ class StreamingPiCamera2(BaseCamera): _exc_type: type[BaseException], _exc_value: Optional[BaseException], _traceback: Optional[TracebackType], - ): + ) -> None: """Close the picamera connection when the Thing context manager is closed.""" self.stop_streaming() with self._streaming_picamera() as cam: diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 2882a216..80dadb55 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -54,7 +54,7 @@ class SimulatedCamera(BaseCamera): glyph_shape: tuple[int, int, int] = (91, 91, 3), canvas_shape: tuple[int, int, int] = (3000, 4000, 3), frame_interval: float = 0.1, - ): + ) -> None: """Initialise the simulated with settings for how images are generated. :param shape: The shape (size) of the generated image. @@ -208,7 +208,7 @@ class SimulatedCamera(BaseCamera): pos = {"x": 0, "y": 0, "z": 0} return self.generate_image((pos["y"], pos["x"], pos["z"])) - def __enter__(self): + def __enter__(self) -> None: """Start the capture thread when the Thing context manager is opened.""" self.start_streaming() return self @@ -218,7 +218,7 @@ class SimulatedCamera(BaseCamera): _exc_type: type[BaseException], _exc_value: Optional[BaseException], _traceback: Optional[TracebackType], - ): + ) -> None: """Close the capture thread when the Thing context manager is closed.""" if self.stream_active: self._capture_enabled = False diff --git a/src/openflexure_microscope_server/things/camera_stage_mapping.py b/src/openflexure_microscope_server/things/camera_stage_mapping.py index d9281e22..a05a3385 100644 --- a/src/openflexure_microscope_server/things/camera_stage_mapping.py +++ b/src/openflexure_microscope_server/things/camera_stage_mapping.py @@ -62,7 +62,7 @@ class RecordedMove: for calibrating the stage as it allows measuring how long moves take. """ - def __init__(self, stage: Stage): + def __init__(self, stage: Stage) -> None: """Set the stage client used for for movement. :param stage: the stage client to be used. ``stage.move_to_xyz_position`` will @@ -72,7 +72,7 @@ class RecordedMove: self._current_position: Optional[CoordinateType] = None self._history: List[Tuple[float, Optional[CoordinateType]]] = [] - def __call__(self, new_position: CoordinateType): + def __call__(self, new_position: CoordinateType) -> None: """Move to a new position, and record it.""" self._history.append((time.time(), self._current_position)) self._stage.move_to_xyz_position(xyz_pos=new_position) @@ -99,7 +99,7 @@ class CSMUncalibratedError(HTTPException): live preview to move, or when performing a scan. """ - def __init__(self): + def __init__(self) -> None: """Customise the default error code and message of HTTPException.""" HTTPException.__init__( self, diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 3e0badba..ae723880 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -85,7 +85,7 @@ class SmartScanThing(lt.Thing): past scans. """ - def __init__(self, scans_folder: str): + def __init__(self, scans_folder: str) -> None: """Initialise a SmartScanThing saving to and loading from the input directory. :param scans_folder: This is the path to the directory where all scans will be diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index f6a49590..4bbcacaf 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -45,7 +45,7 @@ class BaseStage(lt.Thing): _axis_names = ("x", "y", "z") - def __init__(self): + def __init__(self) -> None: """Initialise the stage. :raises RedefinedBaseMovementError: if ``move_relative`` and/or diff --git a/src/openflexure_microscope_server/things/stage/dummy.py b/src/openflexure_microscope_server/things/stage/dummy.py index b238c1c2..6758b37a 100644 --- a/src/openflexure_microscope_server/things/stage/dummy.py +++ b/src/openflexure_microscope_server/things/stage/dummy.py @@ -19,7 +19,7 @@ class DummyStage(BaseStage): hardware attached. """ - def __init__(self, step_time: float = 0.001, **kwargs: Any): + def __init__(self, step_time: float = 0.001, **kwargs: Any) -> None: """Initialise the Dummy stage, setting the step_time to adjust the speed. :param step_time: The time in seconds per "motor" step. The default of 0.001 @@ -31,7 +31,7 @@ class DummyStage(BaseStage): self.step_time = step_time self.instantaneous_position = self._hardware_position - def __enter__(self): + def __enter__(self) -> None: """Register the stage position when the Thing context manager is opened.""" self.instantaneous_position = self._hardware_position @@ -40,7 +40,7 @@ class DummyStage(BaseStage): _exc_type: type[BaseException], _exc_value: Optional[BaseException], _traceback: Optional[TracebackType], - ): + ) -> None: """Nothing to do when the Thing context manager is closed.""" axis_inverted = lt.ThingSetting( diff --git a/src/openflexure_microscope_server/things/stage/sangaboard.py b/src/openflexure_microscope_server/things/stage/sangaboard.py index 05fb55a9..932fa373 100644 --- a/src/openflexure_microscope_server/things/stage/sangaboard.py +++ b/src/openflexure_microscope_server/things/stage/sangaboard.py @@ -25,7 +25,7 @@ class SangaboardThing(BaseStage): functionality is accessed by directly querying the serial interface. """ - def __init__(self, port: str = None, **kwargs: Any): + def __init__(self, port: str = None, **kwargs: Any) -> None: """Initialise SangaboardThing. Initialise the "Thing", but do not initialise an underlying @@ -42,7 +42,7 @@ class SangaboardThing(BaseStage): self.sangaboard_kwargs["port"] = port super().__init__(**kwargs) - def __enter__(self): + def __enter__(self) -> None: """Connect to the sangaboard when the Thing context manager is opened.""" self._sangaboard = sangaboard.Sangaboard(**self.sangaboard_kwargs) self._sangaboard_lock = threading.RLock() @@ -59,7 +59,7 @@ class SangaboardThing(BaseStage): _exc_type: type[BaseException], _exc_value: Optional[BaseException], _traceback: Optional[TracebackType], - ): + ) -> None: """Close the sangaboard connection when the Thing context manager is closed.""" with self.sangaboard() as sb: sb.close() diff --git a/src/openflexure_microscope_server/utilities.py b/src/openflexure_microscope_server/utilities.py index 87503aaa..d9debd1e 100644 --- a/src/openflexure_microscope_server/utilities.py +++ b/src/openflexure_microscope_server/utilities.py @@ -78,7 +78,7 @@ class ErrorCapturingThread(Thread): kwargs: Optional[dict[str, Any]] = None, *, daemon: Optional[bool] = None, - ): + ) -> None: """Initialise with the same arguments as Thread.""" # As all inputs are keywords we need to set the default values for args and kwargs: if args is None: From 8f78369aaf3d470a5574d23e5717e7732a5d81b0 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 27 Aug 2025 14:28:14 +0100 Subject: [PATCH 5/7] fixup! Add ANN20X rules to ruff --- src/openflexure_microscope_server/things/system.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openflexure_microscope_server/things/system.py b/src/openflexure_microscope_server/things/system.py index 48417e31..d2b022bb 100644 --- a/src/openflexure_microscope_server/things/system.py +++ b/src/openflexure_microscope_server/things/system.py @@ -6,7 +6,7 @@ the microscope, server, and thing states to the web API. from collections.abc import Mapping import socket -from typing import Optional +from typing import Optional, Any from uuid import UUID, uuid4 import subprocess import os @@ -127,7 +127,7 @@ class OpenFlexureSystem(lt.Thing): return metadata_getter() @property - def thing_state(self) -> Mapping: + def thing_state(self) -> Mapping[str, Any]: """Summary metadata describing the current state of the Thing.""" return { "hostname": self.hostname, From 8500b479f562dacf9720dbf0155ef8a285ccb1ab Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 28 Aug 2025 09:13:44 +0100 Subject: [PATCH 6/7] No Never returns from actions in base Thing classes --- src/openflexure_microscope_server/things/stage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index 4bbcacaf..3643fc5d 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -194,7 +194,7 @@ class BaseStage(lt.Thing): ) @lt.thing_action - def set_zero_position(self) -> Never: + def set_zero_position(self) -> None: """Make the current position zero in all axes. This action does not move the stage, but resets the position to zero. From 51f0ac3d82b1f5e7c7936fbc52cf6b5716f9991f Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 28 Aug 2025 09:34:30 +0100 Subject: [PATCH 7/7] Update Picamera coverage data --- picamera_coverage.zip | Bin 53913 -> 53913 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/picamera_coverage.zip b/picamera_coverage.zip index eccadaccead003edc27c4e0edec8681f49e0739e..c0370980c9f1901309a9cf162c83d578d03d33c4 100644 GIT binary patch delta 520 zcmbQalzHY-W}yIYW)=|!5Qy}WiSB>C-eseZN`pW%1OHF{kNng4oB5;pF7Y*Qb`;3v zo4mcxM9=t#_92#n3ql`Q9^`W#VDV*4Xlh`{;If-jz}3jc#>&Xq#KXqK@bCThgfzyO zd6Omjbu=ExGBB|IdB1<(gEt>8oBfeuV0a)e&9I=2jo~mGgPkR;WW!;#R-xQ93Zb-Khan{+37;MdUC2|in&R$p+Ra&a*|n+MY5r#sbx}< znW3eHfkBFiMPibrX{x2+&^HBv1SlZ;Z0(#(x5EmJKmj4hH(ObiW-5)&;`(=05^ zQ;iK%4O7f4Ehb;MU^BV=qGY{^Nt%(FWty3Rfu*6jX;NZhvWaQ3fk~oSqDh*8p|P2f zkx8OOigB`%j)Ib2a(-EAQDSGfLz#x=a?hWP$K+z$H%r){~-8 delta 516 zcmbQalzHY-W}yIYW)=|!5Gd=Cj(+yIV9iD$l?H)!2L7M?ANi;AxAMpGUE^!t>?lyk zH+gfPiLUU!E(1oHEi5*SKcblo9D^BVC@3(@YT{R)!_>&a#>&Xq#KFeI@b7!H!RCh9 zlSTV=)Za5OXgsL@|CKepeg3wG*BKZX{@-9=Fg0P2Fk|>|li@>sg2N8RhTW4h`@M1m zSXdZ2l~_(~$i2PImx1SY8)pPFDu{r&td2>j99&ujp5sF zHhCUq9pTN26C@otK;AA7(9ECgbRk_mDK*t1&A=?xDB099$uuQ3$