From 4c46330959c2a7f1adb9fb9a571f7ad687b7c9c1 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 27 Aug 2025 14:00:49 +0100 Subject: [PATCH] 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.