Add ANN20X rules to ruff

This commit is contained in:
Julian Stirling 2025-08-27 14:00:49 +01:00
parent d673196671
commit 4c46330959
20 changed files with 98 additions and 86 deletions

View file

@ -12,7 +12,7 @@ from gitlab.exceptions import GitlabGetError
PROJECT_ID = 9238334 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. """Create a list of the MRs into a given branch after a tag.
:param tag: The tag the mrs should be since. :param tag: The tag the mrs should be since.

View file

@ -43,7 +43,7 @@ def extract_zip(zip_data: bytes) -> None:
print(f"Extracted zip to: {THIS_DIR}") 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. """Fetch build artifacts from GitLab for a given branch and unzip.
Clear any existing static directory before extract the new files. Clear any existing static directory before extract the new files.

View file

@ -126,7 +126,7 @@ select = [
"N", # PEP8 naming "N", # PEP8 naming
"D", # Docstring checks "D", # Docstring checks
"ERA001", # Commented out code! "ERA001", # Commented out code!
"ANN001", "ANN002", "ANN003", "ANN",
] ]
ignore = [ ignore = [
@ -134,7 +134,9 @@ ignore = [
"D203", # incompatible with D204 "D203", # incompatible with D204
"D213", # incompatible with D212 "D213", # incompatible with D212
"D400", # A stricter version of #415 that doesn't allow ! "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] [tool.ruff.lint.per-file-ignores]

View file

@ -21,7 +21,7 @@ from fastapi import HTTPException
OFM_LOG_FILE = None 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. """Configure logging for the server while it is running.
This modifies the root logger to have a rotating file handler and 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): class OFMLogFileFormatter(logging.Formatter):
"""The formatter used for the OpenFlexure Microscope Server log file.""" """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. """Adjust the logging formatting for uvicorn logs.
uvicorn has two loggers. Each API access is ``uvicorn.access`` which we filter uvicorn has two loggers. Each API access is ``uvicorn.access`` which we filter
@ -132,7 +132,7 @@ class OFMHandler(logging.Handler):
self._log = [] self._log = []
self._max_logs = max_logs 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. """Format message and append it to a list of records.
The built in formatter is used to format the record. 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: while len(self._log) > self._max_logs:
self._log.pop(0) 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.""" """Emit will save the logged record to the log."""
try: try:
if record.levelno >= self.level: if record.levelno >= self.level:
@ -156,7 +156,7 @@ class OFMHandler(logging.Handler):
self.handleError(record) self.handleError(record)
@property @property
def log_history(self): def log_history(self) -> str:
"""Return the log history up to the maximum number of logs.""" """Return the log history up to the maximum number of logs."""
return "\n".join(self._log) return "\n".join(self._log)
@ -164,7 +164,7 @@ class OFMHandler(logging.Handler):
class UvicornAccessFilter(logging.Filter): class UvicornAccessFilter(logging.Filter):
"""A logging filter to filter out "uvicorn.access" messages.""" """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 False if record is from "uvicorn.access"."""
return not record.name.startswith("uvicorn.access") return not record.name.startswith("uvicorn.access")

View file

@ -105,7 +105,7 @@ class ScanData(BaseModel):
This should be set with ``set_final_data()`` to ensure duration is set. 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. """Set the final data for the scan, scan duration is automatically calculated.
:param result: A string describing the result. :param result: A string describing the result.
@ -423,13 +423,13 @@ class ScanDirectory:
"""The time the directory was created on disk.""" """The time the directory was created on disk."""
return os.path.getctime(self.dir_path) 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.""" """Return a list of the files in the images dir."""
if self.images_dir is None: if self.images_dir is None:
return [] return []
return os.listdir(self.images_dir) 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. """Extract files which match the naming convention for scan images.
:param file_list: The list of files to search. Normally this would be :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)] 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. """Extract files which match the naming convention for final stitches.
:param file_list: The list of files to search. :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)] 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. """Extract files which match the naming convention for dzi_files.
:param file_list: The list of files to search. :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)) files.append(os.path.relpath(full_path, self.dir_path))
return files 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.""" """Save the scan data for this scan to disk."""
if self.scan_data_path is None: if self.scan_data_path is None:
raise FileNotFoundError( raise FileNotFoundError(
@ -551,7 +551,7 @@ class ScanDirectory:
return zip_fname 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.""" """List the relative paths of all files and folders in the zip folder specified."""
scan_zip = zipfile.ZipFile(zip_path) scan_zip = zipfile.ZipFile(zip_path)
return [os.path.normpath(i) for i in scan_zip.namelist()] return [os.path.normpath(i) for i in scan_zip.namelist()]

View file

@ -332,7 +332,7 @@ class SmartSpiral(ScanPlanner):
"""Sort the remaining positions based on the current location.""" """Sort the remaining positions based on the current location."""
# Defined rather than use a lambda for readability # Defined rather than use a lambda for readability
def sort_key(pos: XYPos): def sort_key(pos: XYPos) -> tuple[float, float, float]:
return ( return (
self.moves_between(current_pos, pos), self.moves_between(current_pos, pos),
self.moves_between(self._initial_position, pos), self.moves_between(self._initial_position, pos),

View file

@ -15,7 +15,7 @@ from .legacy_api import add_v2_endpoints
from ..logging import configure_logging, retrieve_log, retrieve_log_from_file 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. """Ensure a function is called before the shutdown.
This monkey patches the Uvicorn Server's handle_exit. This is needed because 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 original_handler = Server.handle_exit
@wraps(Server.handle_exit) @wraps(Server.handle_exit)
def handle_exit(*args: Any, **kwargs: Any): def handle_exit(*args: Any, **kwargs: Any) -> None:
shutdown_function() shutdown_function()
original_handler(*args, **kwargs) original_handler(*args, **kwargs)
@ -41,7 +41,7 @@ def set_shutdown_function(shutdown_function: Callable[[], None]):
def customise_server( def customise_server(
server: lt.ThingServer, log_folder: str, scans_folder: Optional[str] server: lt.ThingServer, log_folder: str, scans_folder: Optional[str]
): ) -> None:
"""Customise the server with additional endpoints, etc.""" """Customise the server with additional endpoints, etc."""
configure_logging(log_folder) configure_logging(log_folder)
add_v2_endpoints(server) add_v2_endpoints(server)
@ -66,7 +66,7 @@ def _get_scans_dir(config: dict) -> Optional[str]:
return None 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.""" """Start the server from the command line."""
args = lt.cli.parse_args(argv) 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) server = lt.cli.server_from_config(config)
customise_server(server, log_folder, scans_folder) customise_server(server, log_folder, scans_folder)
def shutdown_call(): def shutdown_call() -> None:
try: try:
# Kill any mjpeg streams so that StreamingResponses close. # Kill any mjpeg streams so that StreamingResponses close.
server.things["/camera/"].kill_mjpeg_streams() server.things["/camera/"].kill_mjpeg_streams()

View file

@ -50,7 +50,7 @@ def add_static_files(app: FastAPI, scans_folder: Optional[str]) -> None:
check_static_dir() check_static_dir()
@app.get("/", response_class=RedirectResponse) @app.get("/", response_class=RedirectResponse)
async def redirect_fastapi(): async def redirect_fastapi() -> str:
return "/index.html" return "/index.html"
# Mounting the webapp at / file by file to allow other endpoints to be created # 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.""" """Check that the static dir exists and contains expected files and dirs."""
if not os.path.isdir(STATIC_PATH): if not os.path.isdir(STATIC_PATH):
raise FileNotFoundError( raise FileNotFoundError(

View file

@ -34,7 +34,7 @@ class StitcherValidationError(RuntimeError):
"""The stitcher received values that it deems unsafe to create a command from.""" """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. """Validate that the command only characters that are allowed in a path.
The values in the commands should be numbers, commandline flags, paths, and 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 # Print everything in the buffer when program finishes
self.log_buffer(process.stdout) 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.""" """Log everything in the buffer at INFO level."""
while line := buffer.readline(): while line := buffer.readline():
self.logger.info(line) self.logger.info(line)

View file

@ -127,7 +127,7 @@ class StackParams:
""" """
return self.min_images_to_test + 15 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.""" """Return the slice of images to save given the index of the sharpest image."""
images_each_side = (self.images_to_save - 1) // 2 images_each_side = (self.images_to_save - 1) // 2
return slice( return slice(
@ -234,7 +234,7 @@ class JPEGSharpnessMonitor:
running = False running = False
async def monitor_sharpness(self): async def monitor_sharpness(self) -> None:
"""Start monitoring the frame sizes.""" """Start monitoring the frame sizes."""
self.running = True self.running = True
async for frame in self.camera.lores_mjpeg_stream.frame_async_generator(): async for frame in self.camera.lores_mjpeg_stream.frame_async_generator():
@ -244,7 +244,7 @@ class JPEGSharpnessMonitor:
break break
@contextmanager @contextmanager
def run(self): def run(self) -> None:
"""Context manager, during which we will monitor sharpness from the camera.""" """Context manager, during which we will monitor sharpness from the camera."""
self.portal.start_task_soon(self.monitor_sharpness) self.portal.start_task_soon(self.monitor_sharpness)
try: try:
@ -403,7 +403,7 @@ class AutofocusThing(lt.Thing):
sharpness_monitor: SharpnessMonitorDep, sharpness_monitor: SharpnessMonitorDep,
dz: int = 2000, dz: int = 2000,
start: Literal["centre", "base"] = "centre", start: Literal["centre", "base"] = "centre",
): ) -> tuple[list[float], list[float]]:
"""Repeatedly autofocus the stage until it looks focused. """Repeatedly autofocus the stage until it looks focused.
This action will run the ``fast_autofocus`` action until it settles on a point This action will run the ``fast_autofocus`` action until it settles on a point

View file

@ -133,7 +133,7 @@ class CameraMemoryBuffer:
"No image with matching id in memory to retrieve." "No image with matching id in memory to retrieve."
) from e ) from e
def clear(self): def clear(self) -> None:
"""Clear all images from memory.""" """Clear all images from memory."""
self._storage.clear() self._storage.clear()
@ -208,7 +208,7 @@ class BaseCamera(lt.Thing):
"CameraThings must define their own start_streaming method" "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. """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 This is called when uvicorn gets the a shutdown signal. As this is called from

View file

@ -65,7 +65,7 @@ class OpenCVCamera(BaseCamera):
return self._capture_thread.is_alive() return self._capture_thread.is_alive()
return False return False
def _capture_frames(self): def _capture_frames(self) -> None:
portal = lt.get_blocking_portal(self) portal = lt.get_blocking_portal(self)
while self._capture_enabled: while self._capture_enabled:
ret, frame = self.cap.read() ret, frame = self.cap.read()

View file

@ -72,7 +72,7 @@ class PicameraStreamOutput(Output):
_timestamp: Optional[int] = None, _timestamp: Optional[int] = None,
_packet: Any = None, _packet: Any = None,
_audio: bool = False, _audio: bool = False,
): ) -> None:
"""Add a frame to the stream's ringbuffer.""" """Add a frame to the stream's ringbuffer."""
self.stream.add_frame(frame, self.portal) self.stream.add_frame(frame, self.portal)
@ -173,7 +173,7 @@ class StreamingPiCamera2(BaseCamera):
) )
"""Whether the MJPEG stream is active.""" """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. """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 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 return self._analogue_gain
@analogue_gain.setter @analogue_gain.setter
def analogue_gain(self, value: float): def analogue_gain(self, value: float) -> None:
self._analogue_gain = value self._analogue_gain = value
if self.streaming: if self.streaming:
with self._streaming_picamera() as cam: with self._streaming_picamera() as cam:
@ -224,7 +224,7 @@ class StreamingPiCamera2(BaseCamera):
return self._colour_gains return self._colour_gains
@colour_gains.setter @colour_gains.setter
def colour_gains(self, value: tuple[float, float]): def colour_gains(self, value: tuple[float, float]) -> None:
self._colour_gains = value self._colour_gains = value
if self.streaming: if self.streaming:
with self._streaming_picamera() as cam: with self._streaming_picamera() as cam:
@ -248,7 +248,7 @@ class StreamingPiCamera2(BaseCamera):
return self._exposure_time return self._exposure_time
@exposure_time.setter @exposure_time.setter
def exposure_time(self, value: int): def exposure_time(self, value: int) -> None:
self._exposure_time = value self._exposure_time = value
if self.streaming: if self.streaming:
with self._streaming_picamera() as cam: with self._streaming_picamera() as cam:
@ -292,7 +292,7 @@ class StreamingPiCamera2(BaseCamera):
return SensorModeSelector(**self._sensor_mode) return SensorModeSelector(**self._sensor_mode)
@sensor_mode.setter @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.""" """Change the sensor mode used."""
if new_mode is None: if new_mode is None:
self._sensor_mode = None self._sensor_mode = None
@ -352,7 +352,7 @@ class StreamingPiCamera2(BaseCamera):
) from e ) from e
return None return None
def _initialise_picamera(self): def _initialise_picamera(self) -> None:
"""Acquire the picamera device and store it as ``self._picamera``. """Acquire the picamera device and store it as ``self._picamera``.
This duplicates logic in ``Picamera2.__init__`` to provide a tuning file that This duplicates logic in ``Picamera2.__init__`` to provide a tuning file that
@ -646,7 +646,7 @@ class StreamingPiCamera2(BaseCamera):
self, self,
target_white_level: int = 700, target_white_level: int = 700,
percentile: float = 99.9, percentile: float = 99.9,
): ) -> None:
"""Adjust exposure until a the target white level is reached. """Adjust exposure until a the target white level is reached.
Starting from the minimum exposure, gradually increase exposure until Starting from the minimum exposure, gradually increase exposure until
@ -672,7 +672,7 @@ class StreamingPiCamera2(BaseCamera):
self, self,
method: Literal["percentile", "centre"] = "centre", method: Literal["percentile", "centre"] = "centre",
luminance_power: float = 1.0, luminance_power: float = 1.0,
): ) -> None:
"""Correct the white balance of the image. """Correct the white balance of the image.
This calibration requires a neutral image, such that the 99th centile This calibration requires a neutral image, such that the 99th centile
@ -747,7 +747,7 @@ class StreamingPiCamera2(BaseCamera):
self._initialise_picamera() self._initialise_picamera()
@lt.thing_action @lt.thing_action
def reset_ccm(self): def reset_ccm(self) -> None:
"""Overwrite the colour correction matrix in camera tuning with default values. """Overwrite the colour correction matrix in camera tuning with default values.
These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page

View file

@ -8,7 +8,7 @@ See repository root for licensing information.
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Literal, Optional from typing import Literal, Optional, Mapping
from types import TracebackType from types import TracebackType
from threading import Thread from threading import Thread
import time import time
@ -76,7 +76,7 @@ class SimulatedCamera(BaseCamera):
self.generate_blobs() self.generate_blobs()
self.generate_canvas() self.generate_canvas()
def generate_sprites(self): def generate_sprites(self) -> None:
"""Generate sprites to populate the image.""" """Generate sprites to populate the image."""
sprite_sizes = [5, 7, 10, 21, 36, 40] sprite_sizes = [5, 7, 10, 21, 36, 40]
self.sprites = [] self.sprites = []
@ -109,7 +109,7 @@ class SimulatedCamera(BaseCamera):
# Convert to uint8 and append to the list # Convert to uint8 and append to the list
self.sprites.append(sprite.astype(np.uint8)) 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. """Generate coordinates of blobs and their sizes.
A 1000x3 array is returned. Each row represents (x,y) coordinate 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[:, 1] = RNG.uniform(w / 2, self.canvas_shape[1] - w / 2, n_blobs)
self.blobs[:, 2] = RNG.choice(len(self.sprites), n_blobs) self.blobs[:, 2] = RNG.choice(len(self.sprites), n_blobs)
def generate_canvas(self): def generate_canvas(self) -> None:
"""Generate a canvas. """Generate a canvas.
Canvas is int16 so that random noise can be added to simulation image before 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 < 0] = 0
self.canvas[self.canvas > 255] = 255 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.""" """Generate an image with blobs based on supplied coordinates."""
canvas_width, canvas_height, _ = self.canvas_shape canvas_width, canvas_height, _ = self.canvas_shape
image_width, image_height, _ = self.shape image_width, image_height, _ = self.shape
@ -180,16 +180,16 @@ class SimulatedCamera(BaseCamera):
def attach_to_server( def attach_to_server(
self, server: lt.ThingServer, path: str, setting_storage_path: str self, server: lt.ThingServer, path: str, setting_storage_path: str
): ) -> None:
"""Wrap the attach_to_server method so the server instance can be stored. """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 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. maintaining the same public API as a real camera that doesn't need this access.
""" """
self._server = server 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. """Return the stage position.
The simulation camera has access to the stage position so it can generate a 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/"] self._stage = self._server.things["/stage/"]
return self._stage.instantaneous_position 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.""" """Generate a frame with blobs based on the stage coordinates."""
try: try:
pos = self.get_stage_position() pos = self.get_stage_position()
@ -256,7 +256,7 @@ class SimulatedCamera(BaseCamera):
noise_level = lt.ThingProperty(float, 2.0) noise_level = lt.ThingProperty(float, 2.0)
def _capture_frames(self): def _capture_frames(self) -> None:
portal = lt.get_blocking_portal(self) portal = lt.get_blocking_portal(self)
while self._capture_enabled: while self._capture_enabled:
time.sleep(self.frame_interval) time.sleep(self.frame_interval)
@ -312,14 +312,14 @@ class SimulatedCamera(BaseCamera):
return Image.fromarray(self.generate_frame()) return Image.fromarray(self.generate_frame())
@lt.thing_action @lt.thing_action
def remove_sample(self): def remove_sample(self) -> None:
"""Show the simulated background with no sample.""" """Show the simulated background with no sample."""
if not self._show_sample: if not self._show_sample:
raise RuntimeError("Sample is already removed.") raise RuntimeError("Sample is already removed.")
self._show_sample = False self._show_sample = False
@lt.thing_action @lt.thing_action
def load_sample(self): def load_sample(self) -> None:
"""Show the simulated sample.""" """Show the simulated sample."""
if self._show_sample: if self._show_sample:
raise RuntimeError("Sample is already in place.") raise RuntimeError("Sample is already in place.")

View file

@ -18,6 +18,7 @@ from typing import (
NamedTuple, NamedTuple,
Optional, Optional,
Tuple, Tuple,
Mapping,
) )
from fastapi import HTTPException 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] positions: List[CoordinateType] = [p for t, p in self._history if p is not None]
return MoveHistory(times, positions) return MoveHistory(times, positions)
def clear_history(self): def clear_history(self) -> None:
"""Reset our history to be an empty list.""" """Reset our history to be an empty list."""
self._history = [] self._history = []
@ -241,7 +242,7 @@ class CameraStageMapper(lt.Thing):
return None return None
return self.last_calibration["image_resolution"] 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.""" """Raise an exception if the image_to_stage_displacement matrix is not set."""
if self.image_to_stage_displacement_matrix is None: if self.image_to_stage_displacement_matrix is None:
# Disable check of no message in raised exception as the message is explicitly # Disable check of no message in raised exception as the message is explicitly
@ -254,7 +255,7 @@ class CameraStageMapper(lt.Thing):
stage: Stage, stage: Stage,
x: float, x: float,
y: float, y: float,
): ) -> None:
"""Move by a given number of pixels on the camera. """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 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]) stage.move_relative(x=relative_move[0], y=relative_move[1])
@lt.thing_property @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.""" """Summary metadata describing the current state of the Thing."""
return { return {
k: getattr(self, k) k: getattr(self, k)

View file

@ -38,8 +38,17 @@ CSMDep = lt.deps.direct_thing_client_dependency(
) )
AutofocusDep = lt.deps.direct_thing_client_dependency(AutofocusThing, "/autofocus/") 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): class ScanNotRunningError(RuntimeError):
@ -116,7 +125,7 @@ class SmartScanThing(lt.Thing):
cam: CameraClient, cam: CameraClient,
csm: CSMDep, csm: CSMDep,
scan_name: str = "", scan_name: str = "",
): ) -> None:
"""Move the stage to cover an area, taking images that can be tiled together. """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, 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 self._preview_stitcher = None
@_scan_running @_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. """Before starting a scan, check that background and camera-stage-mapping are set.
Raise error if: Raise error if:
@ -311,7 +320,7 @@ class SmartScanThing(lt.Thing):
) )
@_scan_running @_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. """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", 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) self._ongoing_scan.save_scan_data(self._scan_data)
@_scan_running @_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.""" """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 # Assume 4 images means at least one offset in x and y, making the stitching
# well constrained. # well constrained.
@ -330,7 +339,7 @@ class SmartScanThing(lt.Thing):
self._preview_stitcher.start() self._preview_stitcher.start()
@_scan_running @_scan_running
def _run_scan(self): def _run_scan(self) -> None:
"""Prepare and run the main scan, and perform final actions on completion. """Prepare and run the main scan, and perform final actions on completion.
The result (or exception) from the main scan loop determines whether the 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) self.purge_empty_scans(logger=self._scan_logger)
@_scan_running @_scan_running
def _main_scan_loop(self): def _main_scan_loop(self) -> None:
"""Run the main loop of the scan. """Run the main loop of the scan.
This loop runs during a scan, until no more scan x,y positions 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() self._ongoing_scan.zip_files()
@_scan_running @_scan_running
def _return_to_starting_position(self): def _return_to_starting_position(self) -> None:
"""Return to the initial scan position, if set.""" """Return to the initial scan position, if set."""
self._scan_logger.info("Returning to starting position.") self._scan_logger.info("Returning to starting position.")
if self._scan_data is not None: if self._scan_data is not None:
@ -458,7 +467,7 @@ class SmartScanThing(lt.Thing):
) )
@_scan_running @_scan_running
def _perform_final_stitch(self): def _perform_final_stitch(self) -> None:
"""Update the scan zip and perform final stitch of the data.""" """Update the scan zip and perform final stitch of the data."""
if self._scan_data.image_count <= 3: if self._scan_data.image_count <= 3:
self._scan_logger.info("Not performing a stitch as 3 or fewer images taken") 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( def download_zip(
self, self,
scan_name: str, scan_name: str,
): ) -> ZipBlob:
"""Return zip after including any files left until the end. """Return zip after including any files left until the end.
The zipfile is returned as a Blob. The zipfile is returned as a Blob.
@ -740,7 +749,7 @@ class SmartScanThing(lt.Thing):
@lt.thing_action @lt.thing_action
def stitch_all_scans( def stitch_all_scans(
self, logger: lt.deps.InvocationLogger, cancel: lt.deps.CancelHook 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. """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 :raises RuntimeError: if the microscope is currently running a scan

View file

@ -11,7 +11,7 @@ As the object will be used as a context manager create the hardware connection i
from __future__ import annotations from __future__ import annotations
from collections.abc import Sequence, Mapping from collections.abc import Sequence, Mapping
from typing import Literal from typing import Literal, Never, Any
import labthings_fastapi as lt import labthings_fastapi as lt
@ -119,12 +119,12 @@ class BaseStage(lt.Thing):
) )
@property @property
def thing_state(self): def thing_state(self) -> Mapping[str, Any]:
"""Summary metadata describing the current state of the stage.""" """Summary metadata describing the current state of the stage."""
return {"position": self.position} return {"position": self.position}
@lt.thing_action @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. """Invert the direction setting of the given axis.
:param axis: The axis name (x, y or z) to invert. :param axis: The axis name (x, y or z) to invert.
@ -143,7 +143,7 @@ class BaseStage(lt.Thing):
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
block_cancellation: bool = False, block_cancellation: bool = False,
**kwargs: int, **kwargs: int,
): ) -> None:
"""Make a relative move. Keyword arguments should be axis names.""" """Make a relative move. Keyword arguments should be axis names."""
self._hardware_move_relative( self._hardware_move_relative(
cancel=cancel, cancel=cancel,
@ -156,7 +156,7 @@ class BaseStage(lt.Thing):
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
block_cancellation: bool = False, block_cancellation: bool = False,
**kwargs: int, **kwargs: int,
): ) -> Never:
"""Make a relative move in the coordinate system used by the physical hardware. """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``. Make sure to use and update ``self._hardware_position`` not ``self.position``.
@ -171,7 +171,7 @@ class BaseStage(lt.Thing):
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
block_cancellation: bool = False, block_cancellation: bool = False,
**kwargs: int, **kwargs: int,
): ) -> None:
"""Make an absolute move. Keyword arguments should be axis names.""" """Make an absolute move. Keyword arguments should be axis names."""
self._hardware_move_absolute( self._hardware_move_absolute(
cancel=cancel, cancel=cancel,
@ -184,7 +184,7 @@ class BaseStage(lt.Thing):
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
block_cancellation: bool = False, block_cancellation: bool = False,
**kwargs: int, **kwargs: int,
): ) -> Never:
"""Make a absolute move in the coordinate system used by the physical hardware. """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``. Make sure to use and update ``self._hardware_position`` not ``self.position``.
@ -194,7 +194,7 @@ class BaseStage(lt.Thing):
) )
@lt.thing_action @lt.thing_action
def set_zero_position(self): def set_zero_position(self) -> Never:
"""Make the current position zero in all axes. """Make the current position zero in all axes.
This action does not move the stage, but resets the position to zero. This action does not move the stage, but resets the position to zero.

View file

@ -55,7 +55,7 @@ class DummyStage(BaseStage):
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
block_cancellation: bool = False, block_cancellation: bool = False,
**kwargs: int, **kwargs: int,
): ) -> None:
"""Make a relative move. Keyword arguments should be axis names.""" """Make a relative move. Keyword arguments should be axis names."""
displacement = [kwargs.get(k, 0) for k in self.axis_names] displacement = [kwargs.get(k, 0) for k in self.axis_names]
self.moving = True self.moving = True
@ -93,7 +93,7 @@ class DummyStage(BaseStage):
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
block_cancellation: bool = False, block_cancellation: bool = False,
**kwargs: int, **kwargs: int,
): ) -> None:
"""Make an absolute move. Keyword arguments should be axis names.""" """Make an absolute move. Keyword arguments should be axis names."""
displacement = { displacement = {
axis: int(pos) - self._hardware_position[axis] axis: int(pos) - self._hardware_position[axis]
@ -105,7 +105,7 @@ class DummyStage(BaseStage):
) )
@lt.thing_action @lt.thing_action
def set_zero_position(self): def set_zero_position(self) -> None:
"""Make the current position zero in all axes. """Make the current position zero in all axes.
This action does not move the stage, but resets the position to zero. This action does not move the stage, but resets the position to zero.

View file

@ -51,7 +51,7 @@ class OpenFlexureSystem(lt.Thing):
return UUID(self._microscope_id) return UUID(self._microscope_id)
@microscope_id.setter @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 # TODO make read only but still settable from disk
self._microscope_id = uuid self._microscope_id = uuid

View file

@ -105,7 +105,7 @@ class ErrorCapturingThread(Thread):
daemon=daemon, daemon=daemon,
) )
def join(self, timeout: Optional[float] = None): def join(self, timeout: Optional[float] = None) -> None:
"""Join when the thread is complete. """Join when the thread is complete.
If the thread ended due to an unhandled exception, the exception will be raised 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( def _wrap_and_catch_errors(
target: Callable[..., Any], error_buffer: list, *args: Any, **kwargs: Any target: Callable[..., Any], error_buffer: list, *args: Any, **kwargs: Any
): ) -> None:
"""Run target function in a try-except block. """Run target function in a try-except block.
This function is designed only to be used by ErrorCapturingThread. This function is designed only to be used by ErrorCapturingThread.