Documentation of python magic class methods

This commit is contained in:
Julian Stirling 2025-07-10 16:44:18 +01:00
parent 864ca91e5c
commit e33fecaef0
13 changed files with 119 additions and 32 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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