Merge branch 'ANN-rules' into 'v3'

Enable ruff checks for type annotations

See merge request openflexure/openflexure-microscope-server!382
This commit is contained in:
Julian Stirling 2025-08-28 17:30:47 +00:00
commit cb62bc3cc6
26 changed files with 252 additions and 156 deletions

View file

@ -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") -> None:
"""Create a list of the MRs into a given branch after a tag.
:param tag: The tag the mrs should be since.

Binary file not shown.

View file

@ -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.

View file

@ -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!
"ANN",
]
ignore = [
@ -133,13 +134,19 @@ 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.
"ANN401", # Disalows Any, Any is needed at times, Once MyPy is running Any will be
# handled appropriately
]
[tool.ruff.lint.per-file-ignores]
# 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]

View file

@ -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()

View file

@ -21,7 +21,7 @@ from fastapi import HTTPException
OFM_LOG_FILE = None
def configure_logging(log_folder):
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
@ -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) -> 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
@ -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")

View file

@ -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.
@ -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).
@ -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):
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()]

View file

@ -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)
@ -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) -> tuple[float, float, float]:
return (
self.moves_between(current_pos, pos),
self.moves_between(self._initial_position, pos),

View file

@ -2,20 +2,20 @@
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
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, **kwargs):
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()

View file

@ -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(

View file

@ -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
@ -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.
@ -185,14 +189,14 @@ class FinalStitcher(BaseStitcher):
def __init__(
self,
images_dir,
images_dir: str,
*,
logger: lt.deps.InvocationLogger,
overlap: Optional[float] = None,
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.
@ -340,7 +344,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)

View file

@ -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) -> 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(
@ -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.
@ -234,7 +236,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 +246,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:
@ -252,7 +254,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 +270,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())
@ -401,9 +403,9 @@ class AutofocusThing(lt.Thing):
self,
stage: Stage,
sharpness_monitor: SharpnessMonitorDep,
dz=2000,
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

View file

@ -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
@ -68,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
@ -132,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()
@ -169,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.
@ -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")
@ -202,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
@ -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

View file

@ -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
@ -24,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.
@ -34,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
@ -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],
) -> None:
"""Release the camera when the Thing context manager is closed.
Before releasing the camera the capture thread is closed.
@ -59,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()

View file

@ -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
@ -52,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
@ -65,8 +68,13 @@ 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,
) -> None:
"""Add a frame to the stream's ringbuffer."""
self.stream.add_frame(frame, self.portal)
@ -120,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).
@ -167,7 +175,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
@ -198,7 +206,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:
@ -218,7 +226,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:
@ -242,7 +250,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:
@ -286,7 +294,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
@ -346,7 +354,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
@ -378,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
@ -396,7 +404,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 +427,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],
) -> None:
"""Close the picamera connection when the Thing context manager is closed."""
self.stop_streaming()
with self._streaming_picamera() as cam:
@ -635,7 +648,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
@ -661,7 +674,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
@ -725,7 +738,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:
@ -733,7 +749,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

View file

@ -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...

View file

@ -8,7 +8,8 @@ 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
@ -53,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.
@ -75,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 = []
@ -108,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
@ -124,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
@ -144,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
@ -179,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
@ -198,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()
@ -207,12 +208,17 @@ 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
def __exit__(self, _exc_type, _exc_value, _traceback):
def __exit__(
self,
_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
@ -250,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)
@ -306,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.")

View file

@ -18,6 +18,7 @@ from typing import (
NamedTuple,
Optional,
Tuple,
Mapping,
)
from fastapi import HTTPException
@ -61,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
@ -71,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)
@ -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 = []
@ -98,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,
@ -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)

View file

@ -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,20 +30,34 @@ 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/"
)
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):
"""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 +66,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)
@ -71,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
@ -111,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,
@ -166,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:
@ -306,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",
@ -316,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.
@ -325,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
@ -377,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
@ -444,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:
@ -453,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")
@ -623,7 +637,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
@ -724,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.
@ -735,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

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 collections.abc import Sequence, Mapping
from typing import Literal
from typing import Literal, Never, Any
import labthings_fastapi as lt
@ -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
@ -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.
@ -142,8 +142,8 @@ class BaseStage(lt.Thing):
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
):
**kwargs: int,
) -> None:
"""Make a relative move. Keyword arguments should be axis names."""
self._hardware_move_relative(
cancel=cancel,
@ -155,8 +155,8 @@ class BaseStage(lt.Thing):
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
):
**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``.
@ -170,8 +170,8 @@ class BaseStage(lt.Thing):
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
):
**kwargs: int,
) -> None:
"""Make an absolute move. Keyword arguments should be axis names."""
self._hardware_move_absolute(
cancel=cancel,
@ -183,8 +183,8 @@ class BaseStage(lt.Thing):
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
):
**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) -> None:
"""Make the current position zero in all axes.
This action does not move the stage, but resets the position to zero.

View file

@ -2,6 +2,8 @@
from __future__ import annotations
from typing import Optional, Any
from types import TracebackType
from collections.abc import Mapping
import time
@ -17,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) -> 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
@ -29,11 +31,16 @@ 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
def __exit__(self, _exc_type, _exc_value, _traceback):
def __exit__(
self,
_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(
@ -47,8 +54,8 @@ class DummyStage(BaseStage):
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
):
**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
@ -85,8 +92,8 @@ class DummyStage(BaseStage):
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
):
**kwargs: int,
) -> None:
"""Make an absolute move. Keyword arguments should be axis names."""
displacement = {
axis: int(pos) - self._hardware_position[axis]
@ -98,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.

View file

@ -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, Any
from types import TracebackType
from contextlib import contextmanager
from collections.abc import Mapping
@ -24,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) -> None:
"""Initialise SangaboardThing.
Initialise the "Thing", but do not initialise an underlying
@ -41,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()
@ -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],
) -> None:
"""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():

View file

@ -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
@ -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
@ -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,

View file

@ -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.

View file

@ -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,
) -> 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) -> 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: Any, **kwargs: Any
) -> None:
"""Run target function in a try-except block.
This function is designed only to be used by ErrorCapturingThread.

View file

@ -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