From a30b726b914e668dc06e540a5e7b1bad1cc7bf89 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 10 Jul 2025 13:26:23 +0100 Subject: [PATCH] Class docstrings --- pyproject.toml | 1 - scripts/zenodo/zenodo.py | 2 ++ .../scan_directories.py | 2 +- .../things/auto_recentre_stage.py | 6 ++++ .../things/autofocus.py | 26 +++++++++++++++-- .../things/background_detect.py | 12 ++++++++ .../things/camera/__init__.py | 2 ++ .../things/camera_stage_mapping.py | 28 +++++++++++++++++-- .../things/settings_manager.py | 6 ++++ .../things/smart_scan.py | 7 +++++ .../things/system_control.py | 2 ++ tests/mock_things/mock_autofocus.py | 7 +++++ tests/mock_things/mock_background_detect.py | 9 +++++- tests/mock_things/mock_csm.py | 7 +++++ tests/mock_things/mock_stage.py | 7 +++++ tests/test_smart_scan.py | 4 +-- 16 files changed, 118 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e55e518..53d93bdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,7 +130,6 @@ ignore = [ "D400", # A stricter version of #415 that doesn't allow ! # The checkers below should be turned on as they complain about missing docstrings. "D102", - "D101", "D103", "D104", "D105", diff --git a/scripts/zenodo/zenodo.py b/scripts/zenodo/zenodo.py index 6a0a0380..fa5de077 100644 --- a/scripts/zenodo/zenodo.py +++ b/scripts/zenodo/zenodo.py @@ -13,6 +13,8 @@ import requests class Zenodo: + """A class that packages data for Zenodo.""" + def __init__(self, api_token, use_sandbox=True): self._api_token = api_token self._use_sandbox = use_sandbox diff --git a/src/openflexure_microscope_server/scan_directories.py b/src/openflexure_microscope_server/scan_directories.py index 1a47262d..224991bc 100644 --- a/src/openflexure_microscope_server/scan_directories.py +++ b/src/openflexure_microscope_server/scan_directories.py @@ -16,7 +16,7 @@ IMAGE_REGEX = re.compile(r"-?[0-9]+_-?[0-9]+\.jpe?g$") class NotEnoughFreeSpaceError(IOError): - pass + """An exception raised if there is not enough free space on disk to scan.""" class ScanInfo(BaseModel): diff --git a/src/openflexure_microscope_server/things/auto_recentre_stage.py b/src/openflexure_microscope_server/things/auto_recentre_stage.py index 956e415e..c54270be 100644 --- a/src/openflexure_microscope_server/things/auto_recentre_stage.py +++ b/src/openflexure_microscope_server/things/auto_recentre_stage.py @@ -16,6 +16,12 @@ AutofocusDep = lt.deps.direct_thing_client_dependency(AutofocusThing, "/autofocu class RecentringThing(lt.Thing): + """A Thing for recentring the stage by monitoring parasitic z motion of the stage. + + As the stage moves over a sphere-cap there is parasitic motion in z during xy + movement. The highest z position is the centre of motion. + """ + @lt.thing_action def recentre( self, diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index d7a56700..ba3dd5b3 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -175,6 +175,13 @@ def _get_capture_index_by_id(captures: list[CaptureInfo], buffer_id: int) -> int class SharpnessDataArrays(BaseModel): + """A BaseModel with the position and sharpness data from JPEGSharpnessMonitor. + + Each JPEG Size (representing a sharpness metric) has an associated timestamp, + as does each stage position. The stage positions need to be interpolated so + they correspond with the image timestamps. + """ + jpeg_times: NDArray jpeg_sizes: NDArray stage_times: NDArray @@ -182,6 +189,21 @@ class SharpnessDataArrays(BaseModel): class JPEGSharpnessMonitor: + """A class with direct access to the CameraThing for monitoring the MJPEG stream. + + The autofocus algorithm uses sharpness calculated from the file size of the + images in the MJPEG stream. This class monitors both the stage position and the + jpeg sharpness over time. + + The ``run`` context manager is used to start monitoring the camera stream. Position + monitoring happens during ``focus_rel``. Raw data can be retrieved with + ``data_dict`` and data with interpolate ``z`` positions can be retrieved with + move_data. + + A new JPEGSharpnessMonitor instance is created each time an action with the + SharpnessMonitorDep as an argument is called. + """ + def __init__(self, stage: Stage, camera: Camera, portal: lt.deps.BlockingPortal): self.camera = camera self.stage = stage @@ -564,7 +586,7 @@ class AutofocusThing(lt.Thing): ) -> tuple[bool, list[CaptureInfo], Optional[int]]: """Capture a series of images checking that sharpest image central. - The images are seperated in z offset by stack_parameters.stack_dz, as they + The images are separated in z offset by stack_parameters.stack_dz, as they are captured the last stack_parameters.min_images_to_test images are checked to see if the sharpest image is central enough in the stack. If it is the stack completes. @@ -577,7 +599,7 @@ class AutofocusThing(lt.Thing): * the stack result (True for successful stack, False for failed stack), * a list of CaptureInfo objects, - * the buffer_id of the shapest image (or None if the stack failed) + * the buffer_id of the sharpest image (or None if the stack failed). """ # Move down by the height of the z stack, plus an overshoot # Better to start too low and take too many images than too high and need to refocus diff --git a/src/openflexure_microscope_server/things/background_detect.py b/src/openflexure_microscope_server/things/background_detect.py index 941aef30..219e2ed5 100644 --- a/src/openflexure_microscope_server/things/background_detect.py +++ b/src/openflexure_microscope_server/things/background_detect.py @@ -17,12 +17,24 @@ from .camera import CameraDependency as CamDep class ChannelDistributions(BaseModel): + """A BaseModel for storing the channel distribution of a background image.""" + means: list[float] + """The mean of each channel in the colourspace.""" standard_deviations: list[float] + """The standard deviation of each channel in the colourspace.""" colorspace: str = "LUV" + """The colourspace used.""" class BackgroundDetectThing(lt.Thing): + """Thing for setting a background image and detecting sample in the field of view. + + This uses an LUV colour space checking only the mean and standard deviation of the + UV channels. Over time different, selectable, background detection methods will be + added. + """ + # Requires a getter and a setter to support being a BaseModel but being # saved to file as a dict _background_distributions: Optional[ChannelDistributions] = None diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 25162ff8..23fb1d40 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -19,6 +19,8 @@ from labthings_fastapi.types.numpy import NDArray class JPEGBlob(lt.blob.Blob): + """A class representing a JPEG image as a LabThings FastAPI Blob.""" + media_type: str = "image/jpeg" diff --git a/src/openflexure_microscope_server/things/camera_stage_mapping.py b/src/openflexure_microscope_server/things/camera_stage_mapping.py index 7c028a53..cd25123d 100644 --- a/src/openflexure_microscope_server/things/camera_stage_mapping.py +++ b/src/openflexure_microscope_server/things/camera_stage_mapping.py @@ -45,6 +45,14 @@ XYCoordinateType = Tuple[float, float] class HardwareInterfaceModel(BaseModel): + """A pydantic base model for a stage and camera interface. + + This class provides access to both the stage and the camera, but + it is confusing both because it is a BaseModel of callables rather + than a class with methods, and because it uses names from the underlying + Thing actions, but performs different actions. + """ + move: Callable[[NDArray], None] get_position: Callable[[], NDArray] grab_image: Callable[[], NDArray] @@ -123,6 +131,14 @@ HardwareInterfaceDep = Annotated[ class MoveHistory(NamedTuple): + """A named tuple containing the position over time for a single move. + + This a named tuple with elements: + + * ``times`` + * ``stage_positions`` + """ + times: List[float] stage_positions: List[CoordinateType] @@ -163,6 +179,13 @@ class LoggingMoveWrapper: class CSMUncalibratedError(HTTPException): + """An HTTP Exception raised if camera stage mapping data is needed but unavailable. + + Camera Stage Mapping data is needed to convert from distances specified in fractions + of the feild of view to distances in motor steps. This is used when clicking on the + live preview to move, or when performing a scan. + """ + def __init__(self): HTTPException.__init__( self, @@ -186,9 +209,8 @@ class CameraStageMapper(lt.Thing): direction: Tuple[float, float, float], ) -> DenumpifyingDict: """Move a microscope's stage in 1D, and figure out the relationship with the camera.""" - move = LoggingMoveWrapper( - hw.move - ) # log positions and times for stage calibration + # log positions and times for stage calibration + move = LoggingMoveWrapper(hw.move) tracker = Tracker(hw.grab_image, hw.get_position, settle=hw.settle) direction_array: np.ndarray = np.array(direction) diff --git a/src/openflexure_microscope_server/things/settings_manager.py b/src/openflexure_microscope_server/things/settings_manager.py index 3ad6d222..5aaab5bf 100644 --- a/src/openflexure_microscope_server/things/settings_manager.py +++ b/src/openflexure_microscope_server/things/settings_manager.py @@ -43,6 +43,12 @@ def nested_dict_get(data: dict, key: Sequence[str], create=False) -> Any: class SettingsManager(lt.Thing): + """Provides functionality to other Things about the current server state. + + The SettingsManager is used to get information the microscope ID, the hostname + and the state of other Things. + """ + external_metadata = lt.ThingSetting( initial_value={}, model=Mapping, diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 75bc943d..3755ea07 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -75,6 +75,13 @@ def _scan_running(method): class SmartScanThing(lt.Thing): + """A Thing for scanning samples and interacting with past scans. + + SmartScanThing exposes all functionality for automatically scanning samples, + previewing live stitching, retrieving data from past scans, and for deleting + past scans. + """ + def __init__(self, scans_folder): self._scan_dir_manager = scan_directories.ScanDirectoryManager(scans_folder) self._preview_stitch_popen = None diff --git a/src/openflexure_microscope_server/things/system_control.py b/src/openflexure_microscope_server/things/system_control.py index ca41c22c..82dc276a 100644 --- a/src/openflexure_microscope_server/things/system_control.py +++ b/src/openflexure_microscope_server/things/system_control.py @@ -10,6 +10,8 @@ from pydantic import BaseModel class CommandOutput(BaseModel): + """A pydantic model passing the STDOUT and STDERR from a subprocess over HTTP.""" + output: str error: str diff --git a/tests/mock_things/mock_autofocus.py b/tests/mock_things/mock_autofocus.py index a6c2bd58..cf7de4f3 100644 --- a/tests/mock_things/mock_autofocus.py +++ b/tests/mock_things/mock_autofocus.py @@ -9,6 +9,13 @@ answers to functions. class MockAutoFocusThing: + """A mock autofocus Thing that imports no code from AutofocusThing. + + The class needs functionality added to it over time as more complex + mocking is needed. It imports no code from AutofocusThing so that coverage + is not artificially inflated. + """ + # Counter for checking functions were called mock_call_count = {"looping_autofocus": 0} diff --git a/tests/mock_things/mock_background_detect.py b/tests/mock_things/mock_background_detect.py index 67011523..80419030 100644 --- a/tests/mock_things/mock_background_detect.py +++ b/tests/mock_things/mock_background_detect.py @@ -10,7 +10,14 @@ answers to functions. from openflexure_microscope_server.things.background_detect import ChannelDistributions -class MockBackgoundDetectThing: +class MockBackgroundDetectThing: + """A mock background detect Thing that imports no code from BackgroundDetectThing. + + The class needs functionality added to it over time as more complex + mocking is needed. It imports no code from BackgroundDetectThing so that coverage + is not artificially inflated. + """ + background_distributions = ChannelDistributions( means=[128.0, 128.0, 128.0], standard_deviations=[3.0, 3.0, 3.0], diff --git a/tests/mock_things/mock_csm.py b/tests/mock_things/mock_csm.py index 7123cdf4..20178a8b 100644 --- a/tests/mock_things/mock_csm.py +++ b/tests/mock_things/mock_csm.py @@ -9,4 +9,11 @@ answers to functions. class MockCSMThing: + """A mock CSM Thing that imports no code from CameraStageMapper. + + The class needs functionality added to it over time as more complex + mocking is needed. It imports no code from CameraStageMapper so that coverage + is not artificially inflated. + """ + image_resolution = (123, 456) diff --git a/tests/mock_things/mock_stage.py b/tests/mock_things/mock_stage.py index 2a974ee2..9a36d044 100644 --- a/tests/mock_things/mock_stage.py +++ b/tests/mock_things/mock_stage.py @@ -9,4 +9,11 @@ answers to functions. class MockStageThing: + """A mock Thing for a stage that imports no code from BaseStage. + + The class needs functionality added to it over time as more complex + mocking is needed. It imports no code from BaseStage so that coverage + is not artificially inflated. + """ + position = (111, 222, 333) diff --git a/tests/test_smart_scan.py b/tests/test_smart_scan.py index fb14a17a..a3271330 100644 --- a/tests/test_smart_scan.py +++ b/tests/test_smart_scan.py @@ -30,7 +30,7 @@ from openflexure_microscope_server.things.smart_scan import ( from .mock_things.mock_csm import MockCSMThing from .mock_things.mock_autofocus import MockAutoFocusThing from .mock_things.mock_stage import MockStageThing -from .mock_things.mock_background_detect import MockBackgoundDetectThing +from .mock_things.mock_background_detect import MockBackgroundDetectThing # A global logger to pass in as an Invocation Logger LOGGER = logging.getLogger("mock-invocation_logger") @@ -172,7 +172,7 @@ def _run_only_outer_scan(adjust_inital_state: Optional[Callable] = None): cam_mock = 4 # not called meta_mock = 5 # not called csm_mock = MockCSMThing() - bkgrnd_det_mock = MockBackgoundDetectThing() + bkgrnd_det_mock = MockBackgroundDetectThing() class MockedSmartScanThing(SmartScanThing): """Mocked version of SmartScanThing with a patched _run_scan method."""