From e33fecaef0c4dc0449c0436948f36ee28d007274 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 10 Jul 2025 16:44:18 +0100 Subject: [PATCH] Documentation of python magic class methods --- pyproject.toml | 2 - scripts/zenodo/zenodo.py | 6 +++ src/openflexure_microscope_server/logging.py | 7 ++++ .../scan_directories.py | 16 +++++--- .../things/autofocus.py | 37 +++++++++++-------- .../things/camera/__init__.py | 13 +++++-- .../things/camera/opencv.py | 11 +++++- .../things/camera/picamera.py | 20 +++++++++- .../things/camera/simulation.py | 14 ++++++- .../things/camera_stage_mapping.py | 5 +++ .../things/smart_scan.py | 8 +++- .../things/stage/dummy.py | 10 ++++- .../things/stage/sangaboard.py | 2 + 13 files changed, 119 insertions(+), 32 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7b09fcf6..5be129f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,8 +129,6 @@ ignore = [ "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. - "D105", - "D107", ] [tool.ruff.lint.pydocstyle] diff --git a/scripts/zenodo/zenodo.py b/scripts/zenodo/zenodo.py index fa5de077..fdf47777 100644 --- a/scripts/zenodo/zenodo.py +++ b/scripts/zenodo/zenodo.py @@ -16,6 +16,12 @@ class Zenodo: """A class that packages data for Zenodo.""" def __init__(self, api_token, use_sandbox=True): + """Initialise the zenodo packager with the API token. + + :param api_token: Your Zenodo API key. + :param use_sandbox: True to pushing to ``sandbox.zenodo.org`` rather than + ``zenodo.org`` for testing. + """ self._api_token = api_token self._use_sandbox = use_sandbox if use_sandbox: diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index 0530ecf0..1c1d31d3 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -92,6 +92,13 @@ 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): + """Inialise 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 + are captured. + :param max_logs: The maximum number of logs to hold in memory. This sets + how many can be returned over HTTP. + """ super().__init__(level=level) self._log = [] self._max_logs = max_logs diff --git a/src/openflexure_microscope_server/scan_directories.py b/src/openflexure_microscope_server/scan_directories.py index 224991bc..0fa96b48 100644 --- a/src/openflexure_microscope_server/scan_directories.py +++ b/src/openflexure_microscope_server/scan_directories.py @@ -36,6 +36,10 @@ class ScanDirectoryManager: _base_scan_dir: str def __init__(self, base_scan_dir: str): + """Initialise the scan directory manager. + + :param base_scan_dir: Path of the directory that holds all scans. + """ self._base_scan_dir = base_scan_dir if not os.path.exists(self._base_scan_dir): os.makedirs(self._base_scan_dir) @@ -194,17 +198,17 @@ class ScanDirectoryManager: class ScanDirectory: - """A class for handling interactions with scan directories. - - Initalisation parameters: - name: the name of the scan (the scan directory basename) - base_scan_dir: Path of the directory that holds all scans - """ + """A class for handling interactions with scan directories.""" _name: str _base_scan_dir: str def __init__(self, name: str, base_scan_dir: str): + """Initialise the scan directory. + + :param name: the name of the scan (the scan directory basename). + :param base_scan_dir: Path of the directory that holds all scans. + """ self._name = name self._base_scan_dir = base_scan_dir if not os.path.isdir(self.dir_path): diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 2ab58750..7c1b39c7 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -26,21 +26,7 @@ from .stage import StageDependency as Stage class StackParams: - """A class for holding for scan parameters. - - All arguments are keyword only - - :param stack_dz: The number of motor steps between images - :param images_to_save: The number of images to save to disk - :param min_images_to_test: The minimum number of images in the stack before, the - stack is evaluated for focus. As more images are captured evaluation of the - focus is always evaluated with the same number of images. i.e. if - ``min_images_to_test=9``, then 9 images are captured, if the stack is not well - focused, a 10th image is captured and images 2 to 10 are evaluated for focus - :param autofocus_dz: The number of steps in a full autofocus (when required) - :param images_dir: The directory to save images to disk - :param save_resolution: The resolution to save the captures to disk with - """ + """A class for holding for stack parameters, and returning computed ones.""" def __init__( self, @@ -52,6 +38,20 @@ class StackParams: images_dir: str, save_resolution: tuple[int, int], ) -> None: + """Initalise the parameters. All arguments are keyword only. + + :param stack_dz: The number of motor steps between images + :param images_to_save: The number of images to save to disk + :param min_images_to_test: The minimum number of images in the stack before, + the stack is evaluated for focus. As more images are captured evaluation + of the focus is always evaluated with the same number of images. i.e. if + ``min_images_to_test=9``, then 9 images are captured, if the stack is not + well focused, a 10th image is captured and images 2 to 10 are evaluated + for focus + :param autofocus_dz: The number of steps in a full autofocus (when required) + :param images_dir: The directory to save images to disk + :param save_resolution: The resolution to save the captures to disk with + """ if min_images_to_test < images_to_save: raise ValueError("Can't save more images than the minimum number tested") if min_images_to_test % 2 == 0 or min_images_to_test <= 0: @@ -205,6 +205,13 @@ class JPEGSharpnessMonitor: """ def __init__(self, stage: Stage, camera: Camera, portal: lt.deps.BlockingPortal): + """Initalise a new JPEGSharpnessMonitor. The args are injected automatically. + + :param stage: A direct_thing_client dependency for the the microscope stage. + :param camera: A raw_thing_client depeendency for the camera. This is a raw + dependency as the underlying class needs to be + :param portal: The asyncio blocking portal for asynchronous task scheduling. + """ self.camera = camera self.stage = stage self.portal = portal diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index ed0d4e96..8bcf25eb 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -41,18 +41,19 @@ class CaptureError(RuntimeError): class NoImageInMemoryError(RuntimeError): - """An error called if no image in in memory when an method is called to use that image.""" + """An error called if no image is in memory when accessed.""" class CameraMemoryBuffer: """A class that holds images in memory. The images are by default PIL images. - However subclasses of BaseCamera can use this class to store other object types + However subclasses of BaseCamera can use this class to store other object types. """ _storage: dict[int, tuple[Any, Optional[dict]]] def __init__(self): + """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 self._storage = {} @@ -142,16 +143,22 @@ class CameraMemoryBuffer: class BaseCamera(lt.Thing): - """The base class for all cameras. All cameras must directly inherit from this class.""" + """The base class for all cameras. All cameras must directly inherit from this class. + + The connection to the camera hardware should be added to the ``__enter__`` method not + ``__init__`` method of the subclass. + """ mjpeg_stream = lt.outputs.MJPEGStreamDescriptor() lores_mjpeg_stream = lt.outputs.MJPEGStreamDescriptor() _memory_buffer = CameraMemoryBuffer() def __enter__(self) -> None: + """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: + """Close hardware connection when the Thing context manager is closed.""" raise NotImplementedError("CameraThings must define their own __exit__ method") @lt.thing_action diff --git a/src/openflexure_microscope_server/things/camera/opencv.py b/src/openflexure_microscope_server/things/camera/opencv.py index 89bc132a..4c39191b 100644 --- a/src/openflexure_microscope_server/things/camera/opencv.py +++ b/src/openflexure_microscope_server/things/camera/opencv.py @@ -23,14 +23,19 @@ from . import BaseCamera, JPEGBlob class OpenCVCamera(BaseCamera): - """A Thing representing an OpenCV camera.""" + """A Thing that provides and interface to an OpenCV Camera.""" def __init__(self, camera_index: int = 0): + """Iniatilise the thing storing the index of the camera to use. + + :param camera_index: The index of the camera to use for the microscope. + """ self.camera_index = camera_index self._capture_thread: Optional[Thread] = None self._capture_enabled = False def __enter__(self): + """Start the capture thread when the Thing context manager is opened.""" self.cap = cv2.VideoCapture(self.camera_index) self._capture_enabled = True self._capture_thread = Thread(target=self._capture_frames) @@ -38,6 +43,10 @@ class OpenCVCamera(BaseCamera): return self def __exit__(self, _exc_type, _exc_value, _traceback): + """Release the camera when the Thing context manager is closed. + + Before releasing the camera the capture thread is closed. + """ if self.stream_active: self._capture_enabled = False self._capture_thread.join() diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index c1524ea4..d1990f8d 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -104,9 +104,20 @@ class LensShading(BaseModel): class StreamingPiCamera2(BaseCamera): - """A Thing that represents an OpenCV camera.""" + """A Thing that provides and interface to the Raspberry Pi Camera. + + Currently the Thing only supports the PiCamera v2 board. This needs + generalisation. + """ def __init__(self, camera_num: int = 0): + """Initialise the camera with the given camera number. + + This makes no connection to the camera (except to get the default tuning file). + + :param camera_num: The number of the camera. This should generally be left as 0 + as most Raspberry Pi boards only support 1 camera. + """ self._setting_save_in_progress = False self.camera_num = camera_num self.camera_configs: dict[str, dict] = {} @@ -324,6 +335,11 @@ class StreamingPiCamera2(BaseCamera): self._picamera_lock = RLock() def __enter__(self): + """Start streaming when the Thing context manager is opened. + + This opens the picamera connection, initialises the camera, sets the + sensor_modes property, and then starts the streams. + """ self._initialise_picamera() # populate sensor modes by reading the property self.sensor_modes @@ -360,7 +376,7 @@ class StreamingPiCamera2(BaseCamera): self.start_streaming() def __exit__(self, exc_type, exc_value, traceback): - # Shut down the camera + """Close the picamera connection when the Thing context manager is closed.""" self.stop_streaming() with self._streaming_picamera() as cam: cam.close() diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index a5a7bc13..58675f4b 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -30,7 +30,7 @@ RATIO = 0.2 class SimulatedCamera(BaseCamera): - """A Thing representing an OpenCV camera.""" + """A Thing that simulates a camera for testing.""" _stage: Optional[BaseStage] = None _server: Optional[lt.ThingServer] = None @@ -42,6 +42,16 @@ class SimulatedCamera(BaseCamera): canvas_shape: tuple[int, int, int] = (3000, 4000, 3), frame_interval: float = 0.1, ): + """Initalise the simulated with settings for how images are generated. + + :param shape: The shape (size) of the generated image. + :param glyph_shape: The size randomly positioned glyphs. + :param canvas_shape: The shape (size) of the canvas generated on initialisation + that images are cropped from. If this is too large the it uses resources, + but its size limits the range of motion of the simulation. + :param frame_interval: Nominally the time between frames on the MJPEG stream, + however the rate may be slower due to calculation time for focus. + """ self.shape = shape self.glyph_shape = glyph_shape self.canvas_shape = canvas_shape @@ -143,12 +153,14 @@ class SimulatedCamera(BaseCamera): return self.generate_image((pos["y"], pos["x"], pos["z"])) def __enter__(self): + """Start the capture thread when the Thing context manager is opened.""" self._capture_enabled = True self._capture_thread = Thread(target=self._capture_frames) self._capture_thread.start() return self def __exit__(self, _exc_type, _exc_value, _traceback): + """Close the capture thread when the Thing context manager is closed.""" if self.stream_active: self._capture_enabled = False self._capture_thread.join() diff --git a/src/openflexure_microscope_server/things/camera_stage_mapping.py b/src/openflexure_microscope_server/things/camera_stage_mapping.py index cd25123d..0e0ace6f 100644 --- a/src/openflexure_microscope_server/things/camera_stage_mapping.py +++ b/src/openflexure_microscope_server/things/camera_stage_mapping.py @@ -155,6 +155,10 @@ class LoggingMoveWrapper: """ def __init__(self, move_function: Callable): + """Set the movement function to be wrapped. + + :param move_function: the movement function to be wrapped + """ self._move_function: Callable = move_function self._current_position: Optional[CoordinateType] = None self.clear_history() @@ -187,6 +191,7 @@ class CSMUncalibratedError(HTTPException): """ def __init__(self): + """Customise the default error code and message of HTTPException.""" HTTPException.__init__( self, 503, diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 3755ea07..4401520b 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -82,7 +82,13 @@ class SmartScanThing(lt.Thing): past scans. """ - def __init__(self, scans_folder): + def __init__(self, scans_folder: str): + """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 + saved. Any scans already in this directory will be accessible through the + HTTP interface. + """ self._scan_dir_manager = scan_directories.ScanDirectoryManager(scans_folder) self._preview_stitch_popen = None self._preview_stitch_popen_lock = threading.Lock() diff --git a/src/openflexure_microscope_server/things/stage/dummy.py b/src/openflexure_microscope_server/things/stage/dummy.py index 44d031a4..87109e4f 100644 --- a/src/openflexure_microscope_server/things/stage/dummy.py +++ b/src/openflexure_microscope_server/things/stage/dummy.py @@ -18,14 +18,22 @@ class DummyStage(BaseStage): """ def __init__(self, step_time: float = 0.001, **kwargs): + """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 + works well for the live simulation. For unit testing it is very slow + so the speed can be increased. Increasing it too far is problematic if + also doing computationally heavy tasks like simulated image blurring. + """ super().__init__(**kwargs) self.step_time = step_time def __enter__(self): + """Register the stage position when the Thing context manager is opened.""" self.instantaneous_position = self.position def __exit__(self, _exc_type, _exc_value, _traceback): - pass + """Nothing to do when the Thing context manager is closed.""" @lt.thing_action def move_relative( diff --git a/src/openflexure_microscope_server/things/stage/sangaboard.py b/src/openflexure_microscope_server/things/stage/sangaboard.py index cd3e4f3a..e78fa75c 100644 --- a/src/openflexure_microscope_server/things/stage/sangaboard.py +++ b/src/openflexure_microscope_server/things/stage/sangaboard.py @@ -40,6 +40,7 @@ class SangaboardThing(BaseStage): self.sangaboard_kwargs["port"] = port def __enter__(self): + """Connect to the sangaboard when the Thing context manager is opened.""" self._sangaboard = sangaboard.Sangaboard(**self.sangaboard_kwargs) self._sangaboard_lock = threading.RLock() with self.sangaboard() as sb: @@ -51,6 +52,7 @@ class SangaboardThing(BaseStage): self.update_position() def __exit__(self, _exc_type, _exc_value, _traceback): + """Close the sangaboard connection when the Thing context manager is closed.""" with self.sangaboard() as sb: sb.close()