Starting docstring on the same lines as the quotations

This commit is contained in:
Julian Stirling 2025-07-10 01:04:26 +01:00
parent be6a6ca6fe
commit 35d47fe3ed
28 changed files with 92 additions and 214 deletions

View file

@ -1,7 +1,6 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
""" """A script to check that the npm version string matches the python
A script to check that the npm version string matches the python
version string exactly. version string exactly.
""" """

View file

@ -32,8 +32,7 @@ def test_calibration(client):
def test_jpeg_and_array(client): def test_jpeg_and_array(client):
""" """Check that grabbing a jpeg from the stream results in the same size
Check that grabbing a jpeg from the stream results in the same size
image as a array capture or a jpeg capture. image as a array capture or a jpeg capture.
""" """
# Grab a jpeg from the stream # Grab a jpeg from the stream

View file

@ -1,5 +1,4 @@
""" """Check exposure times do not drift.
Check exposure times do not drift.
This can get very tedious. Recommend running pytest with -s option This can get very tedious. Recommend running pytest with -s option
to monitor progress. to monitor progress.
@ -19,8 +18,7 @@ logging.basicConfig(level=logging.DEBUG)
def _test_exposure_time_drift(desired_time): def _test_exposure_time_drift(desired_time):
""" """Capture 10 full resolution images and check that the exposure time remains constant
Capture 10 full resolution images and check that the exposure time remains constant
This confirms that automatic exposure time adjustment is fully turned off This confirms that automatic exposure time adjustment is fully turned off
""" """
@ -64,8 +62,6 @@ def _test_exposure_time_drift(desired_time):
def test_exposure_time_drift(): def test_exposure_time_drift():
""" """Performs the exposure time test for a range of exposure time values."""
Performs the exposure time test for a range of exposure time values.
"""
for desired_time in [100, 1000, 10000]: for desired_time in [100, 1000, 10000]:
_test_exposure_time_drift(desired_time) _test_exposure_time_drift(desired_time)

View file

@ -12,9 +12,7 @@ logging.basicConfig(level=logging.DEBUG)
def test_sensor_mode(): def test_sensor_mode():
""" """Test capturing raw arrays in two different sensor modes"""
Test capturing raw arrays in two different sensor modes
"""
cam = StreamingPiCamera2() cam = StreamingPiCamera2()
server = ThingServer() server = ThingServer()
server.add_thing(cam, "/camera/") server.add_thing(cam, "/camera/")

View file

@ -1,6 +1,4 @@
""" """Tests that check that a tuning file can be reloaded"""
Tests that check that a tuning file can be reloaded
"""
import os import os
@ -16,17 +14,13 @@ MODEL = Picamera2.global_camera_info()[0]["Model"]
def load_default_tuning(): def load_default_tuning():
""" """Return the default tuning file for the connected camera."""
Return the default tuning file for the connected camera.
"""
fname = f"{MODEL}.json" fname = f"{MODEL}.json"
return Picamera2.load_tuning_file(fname) return Picamera2.load_tuning_file(fname)
def generate_bad_tuning(): def generate_bad_tuning():
""" """Return a tuning file with an invalid version number to force an error when loaded."""
Return a tuning file with an invalid version number to force an error when loaded.
"""
default_tuning = load_default_tuning() default_tuning = load_default_tuning()
bad_tuning = default_tuning.copy() bad_tuning = default_tuning.copy()
bad_tuning["version"] = 999 bad_tuning["version"] = 999
@ -34,8 +28,7 @@ def generate_bad_tuning():
def print_tuning(read_file: bool = False): def print_tuning(read_file: bool = False):
""" """Print the path of the default tuning file from the the environment variable.
Print the path of the default tuning file from the the environment variable.
:param read_file: Boolean, set true to also print the file contents. :param read_file: Boolean, set true to also print the file contents.
@ -53,8 +46,7 @@ def print_tuning(read_file: bool = False):
def _test_bad_tuning_after_good_tuning(configure: bool = False): def _test_bad_tuning_after_good_tuning(configure: bool = False):
""" """Load the default tuning file into the camera, re-load with a broken tuning file,
Load the default tuning file into the camera, re-load with a broken tuning file,
check it errors. Finally check the default tuning file will load again afterwards. check it errors. Finally check the default tuning file will load again afterwards.
:param configure: Boolean, set true to configure the camera on initial loading :param configure: Boolean, set true to configure the camera on initial loading

View file

@ -124,8 +124,7 @@ def set_up_working_dir() -> None:
def start_server() -> subprocess.Popen: def start_server() -> subprocess.Popen:
""" """Start the server in a subprocess.
Start the server in a subprocess.
The server is started in a subprocess and all outputs are buffered. The server is started in a subprocess and all outputs are buffered.

View file

@ -128,8 +128,7 @@ ignore = [
"D203", # incompatible with D204 "D203", # incompatible with D204
"D213", # incompatible with D212 "D213", # incompatible with D212
# Below are checkers to be turned on gradually # Below are checkers to be turned on gradually
"D212", "D205", # Force single line summary!
"D205",
"D415", "D415",
"D400", "D400",
"D200", "D200",

View file

@ -1,5 +1,4 @@
""" """Copyright (c) 2020 Bath Open Instrumentation Group
Copyright (c) 2020 Bath Open Instrumentation Group
Adapted from: https://gitlab.com/schlauch/zenodo-api-test/ Adapted from: https://gitlab.com/schlauch/zenodo-api-test/
Copyright (c) 2018 German Aerospace Center (DLR). All rights reserved. Copyright (c) 2018 German Aerospace Center (DLR). All rights reserved.
SPDX-License-Identifier: MIT-DLR SPDX-License-Identifier: MIT-DLR

View file

@ -68,8 +68,7 @@ def retrieve_log_from_file() -> PlainTextResponse:
class OFMHandler(logging.Handler): class OFMHandler(logging.Handler):
""" """A child class of logging.Handler. This class handles storing the most recent
A child class of logging.Handler. This class handles storing the most recent
logs for access by the server. logs for access by the server.
""" """
@ -79,8 +78,7 @@ class OFMHandler(logging.Handler):
self._max_logs = max_logs self._max_logs = max_logs
def append_record(self, record): def append_record(self, record):
""" """Use the built in formatter to format the record, then save
Use the built in formatter to format the record, then save
it to an array. Pop any in excess of the maximum number of logs it to an array. Pop any in excess of the maximum number of logs
""" """
self._log.append(self.format(record)) self._log.append(self.format(record))
@ -88,9 +86,7 @@ class OFMHandler(logging.Handler):
self._log.pop(0) self._log.pop(0)
def emit(self, record): def emit(self, record):
""" """Emit will save the logged record to the log"""
Emit will save the logged record to the log
"""
# Basic filter for now that simply stops uvicorn.access logs # Basic filter for now that simply stops uvicorn.access logs
# These are the logs each time an API endpoint is accessed # These are the logs each time an API endpoint is accessed
# This is only the log for the UI. # This is only the log for the UI.
@ -108,9 +104,7 @@ class OFMHandler(logging.Handler):
@property @property
def log_history(self): def log_history(self):
""" """Return the log history up to the maximum number of logs"""
Return the log history up to the maximum number of logs
"""
return "\n".join(self._log) return "\n".join(self._log)

View file

@ -31,9 +31,7 @@ class ScanInfo(BaseModel):
class ScanDirectoryManager: class ScanDirectoryManager:
""" """A class for managing interactions with scan directories"""
A class for managing interactions with scan directories
"""
_base_scan_dir: str _base_scan_dir: str
@ -180,8 +178,7 @@ class ScanDirectoryManager:
return ScanDirectory(scan_name, self.base_dir).zip_files(final_version) return ScanDirectory(scan_name, self.base_dir).zip_files(final_version)
def check_free_disk_space(self, min_space: int = 500000000) -> None: def check_free_disk_space(self, min_space: int = 500000000) -> None:
""" """Raise an exception if there is not enough free disk space to continue scanning
Raise an exception if there is not enough free disk space to continue scanning
:param min_space: the minimum space required in bytes. :param min_space: the minimum space required in bytes.
Default = 500,000,000 (500MB) Default = 500,000,000 (500MB)
@ -227,8 +224,7 @@ class ScanDirectory:
@property @property
def images_dir(self) -> Optional[str]: def images_dir(self) -> Optional[str]:
""" """The path to the images directory. None is returned if no images directory
The path to the images directory. None is returned if no images directory
was created was created
""" """
im_path = os.path.join(self.dir_path, IMG_DIR_NAME) im_path = os.path.join(self.dir_path, IMG_DIR_NAME)

View file

@ -58,8 +58,7 @@ def enforce_xyz_tuple(value: XYZPos) -> XYZPos:
class ScanPlanner: class ScanPlanner:
""" """A base class for a scan planner.
A base class for a scan planner.
This should never be used directly for a scan, it should be subclassed. This should never be used directly for a scan, it should be subclassed.
@ -116,9 +115,7 @@ class ScanPlanner:
@property @property
def imaged_locations(self) -> XYZPosList: def imaged_locations(self) -> XYZPosList:
""" """Property to access a copy of the imaged_locations"""
Property to access a copy of the imaged_locations
"""
return copy(self._imaged_locations) return copy(self._imaged_locations)
@property @property
@ -148,22 +145,17 @@ class ScanPlanner:
raise NotImplementedError("Did you call the ScanPlanner base class?") raise NotImplementedError("Did you call the ScanPlanner base class?")
def position_visited(self, position: XYPos) -> bool: def position_visited(self, position: XYPos) -> bool:
""" """Return True if input xy position has been visited before"""
Return True if input xy position has been visited before
"""
# Ensure tuple for correct matching! # Ensure tuple for correct matching!
return tuple(position) in self._path_history return tuple(position) in self._path_history
def position_planned(self, position: XYPos) -> bool: def position_planned(self, position: XYPos) -> bool:
""" """Return True if input xy position is planned"""
Return True if input xy position is planned
"""
# Ensure tuple for correct matching! # Ensure tuple for correct matching!
return tuple(position) in self._remaining_locations return tuple(position) in self._remaining_locations
def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]: def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]:
""" """Return the next location to scan, and the estimated z-position
Return the next location to scan, and the estimated z-position
for this location. for this location.
Note z-position may be None! This indicates that the current z, position Note z-position may be None! This indicates that the current z, position
@ -184,8 +176,7 @@ class ScanPlanner:
return next_location, z return next_location, z
def closest_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]: def closest_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]:
""" """Return the xyz position of the closest site where focus was achieved
Return the xyz position of the closest site where focus was achieved
to the input xy_position, with the most recently taken image returned in to the input xy_position, with the most recently taken image returned in
the case of a tie the case of a tie
@ -212,8 +203,7 @@ class ScanPlanner:
def mark_location_visited( def mark_location_visited(
self, xyz_pos: XYZPos, imaged: bool, focused: bool self, xyz_pos: XYZPos, imaged: bool, focused: bool
) -> None: ) -> None:
""" """Mark the location as visited
Mark the location as visited
Args: Args:
xyz_pos: the x_y_z position xyz_pos: the x_y_z position
@ -290,8 +280,7 @@ class SmartSpiral(ScanPlanner):
def mark_location_visited( def mark_location_visited(
self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True
) -> None: ) -> None:
""" """Mark the location as visited. Adjust extra positions accordingly
Mark the location as visited. Adjust extra positions accordingly
Args: Args:
xyz_pos: the x_y_z position xyz_pos: the x_y_z position
@ -336,9 +325,7 @@ class SmartSpiral(ScanPlanner):
self._remaining_locations.append(new_pos) self._remaining_locations.append(new_pos)
def _re_sort_remaining_locations(self, current_pos: XYPos) -> None: def _re_sort_remaining_locations(self, current_pos: XYPos) -> None:
""" """Sort the remaining positions besed on the current location"""
Sort the remaining positions besed on the current location
"""
# Defined rather than use a lambda for readability # Defined rather than use a lambda for readability
def sort_key(pos): def sort_key(pos):
@ -351,8 +338,7 @@ class SmartSpiral(ScanPlanner):
self._remaining_locations.sort(key=sort_key) self._remaining_locations.sort(key=sort_key)
def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]: def get_next_location_and_z_estimate(self) -> tuple[XYPos, Optional[int]]:
""" """Return the next location to scan, and the estimated z-position
Return the next location to scan, and the estimated z-position
for this location. This overrides the default behaviour of ScanPlanner for this location. This overrides the default behaviour of ScanPlanner
to take the lowest value of nearest neighbours as this works best for smart stack to take the lowest value of nearest neighbours as this works best for smart stack
@ -374,8 +360,7 @@ class SmartSpiral(ScanPlanner):
return next_location, z return next_location, z
def select_nearby_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]: def select_nearby_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]:
""" """Return the xyz position of the nearby site with the lowest z position.
Return the xyz position of the nearby site with the lowest z position.
Lowest position is best, as starting too high causes smart stacking to Lowest position is best, as starting too high causes smart stacking to
autofocus and restart. Starting too low just requires extra movements in +z. autofocus and restart. Starting too low just requires extra movements in +z.
Nearby is defined as within NEIGHBOUR_CUTOFF times the distance to the closest neighbour. Nearby is defined as within NEIGHBOUR_CUTOFF times the distance to the closest neighbour.
@ -412,8 +397,7 @@ class SmartSpiral(ScanPlanner):
starting_pos: XYPos | np.ndarray, starting_pos: XYPos | np.ndarray,
ending_pos: XYPos | np.ndarray, ending_pos: XYPos | np.ndarray,
) -> float: ) -> float:
""" """Return the number of moves between two xy positions in the x or y direction
Return the number of moves between two xy positions in the x or y direction
whichever is largest whichever is largest
Args: Args:
@ -434,8 +418,7 @@ class SmartSpiral(ScanPlanner):
def distance_between( def distance_between(
current_pos: XYPos | np.ndarray, next_pos: XYPos | np.ndarray current_pos: XYPos | np.ndarray, next_pos: XYPos | np.ndarray
) -> float: ) -> float:
""" """Calculate the distance between the two xy positions
Calculate the distance between the two xy positions
This was previously called ``distance_to_site`` This was previously called ``distance_to_site``
""" """

View file

@ -14,8 +14,7 @@ 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]):
""" """Ensure a function is called before the shutdown
Ensure a function is called before the shutdown
This monkey patches the Uvicorn Server's handle_exit. This is needed because This monkey patches the Uvicorn Server's handle_exit. This is needed because
the uvicorn ``lifecycle`` events and FastAPI ``shutdown`` events only fire once the uvicorn ``lifecycle`` events and FastAPI ``shutdown`` events only fire once
@ -52,8 +51,7 @@ def customise_server(
def _get_scans_dir(config: dict) -> Optional[str]: def _get_scans_dir(config: dict) -> Optional[str]:
""" """Read the config and return the scans directory.
Read the config and return the scans directory.
Return is None if there is no /smart_scan/ thing loaded. Return is None if there is no /smart_scan/ thing loaded.
""" """

View file

@ -107,9 +107,7 @@ class StackParams:
@property @property
def steps_undershoot(self) -> int: def steps_undershoot(self) -> int:
""" """The distance to deliberately undershoot the estimated optimal starting point"""
The distance to deliberately undershoot the estimated optimal starting point
"""
# Starting too low by "steps_undershoot" makes smart stacking faster. # Starting too low by "steps_undershoot" makes smart stacking faster.
# Starting a stack too high requires it to move to the start, # Starting a stack too high requires it to move to the start,
# autofocus and then re-stack. Starting slightly too low only # autofocus and then re-stack. Starting slightly too low only
@ -135,9 +133,7 @@ class StackParams:
@dataclass @dataclass
class CaptureInfo: class CaptureInfo:
""" """The information from a capture in a z_stack"""
The information from a capture in a z_stack
"""
buffer_id: int buffer_id: int
position: dict[str, int] position: dict[str, int]

View file

@ -43,8 +43,7 @@ class NoImageInMemoryError(RuntimeError):
class CameraMemoryBuffer: class CameraMemoryBuffer:
""" """A class that holds images in memory. The images are by default PIL images.
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
""" """
@ -62,8 +61,7 @@ class CameraMemoryBuffer:
def add_image( def add_image(
self, image: Any, metadata: Optional[dict] = None, buffer_max: int = 1 self, image: Any, metadata: Optional[dict] = None, buffer_max: int = 1
) -> int: ) -> int:
""" """Add an image to the Memory buffer
Add an image to the Memory buffer
This will add an image to the memory buffer. By default the buffer will This will add an image to the memory buffer. By default the buffer will
be cleared. To allow saving multiple images the buffer_max must be set be cleared. To allow saving multiple images the buffer_max must be set
@ -85,8 +83,7 @@ class CameraMemoryBuffer:
def get_image( def get_image(
self, buffer_id: Optional[int] = None, remove: bool = True self, buffer_id: Optional[int] = None, remove: bool = True
) -> tuple[Any, Optional[dict]]: ) -> tuple[Any, Optional[dict]]:
""" """Return the image with the given id.
Return the image with the given id.
If no id is given the most recent image is returned. However, the If no id is given the most recent image is returned. However, the
buffer is also cleared, otherwise it would be possible to accidentally buffer is also cleared, otherwise it would be possible to accidentally
@ -117,14 +114,11 @@ class CameraMemoryBuffer:
) from e ) from e
def clear(self): def clear(self):
""" """Clear all images from memory"""
Clear all images from memory
"""
self._storage.clear() self._storage.clear()
def _create_space(self, buffer_max: int) -> None: def _create_space(self, buffer_max: int) -> None:
""" """Create space to add an image.
Create space to add an image.
:param buffer_max: The maximum number of images that should be in the buffer :param buffer_max: The maximum number of images that should be in the buffer
once another images is added. once another images is added.
@ -170,8 +164,7 @@ class BaseCamera(lt.Thing):
) )
def kill_mjpeg_streams(self): def kill_mjpeg_streams(self):
""" """Kill the streams now as the server is shutting down.
Kill the streams now as the server is shutting down.
This is called when uvicorn gets the a shutdown signal. As this is called from This is called when uvicorn gets the a shutdown signal. As this is called from
the event loop it cannot interact with the our ThingProperties or run the event loop it cannot interact with the our ThingProperties or run
@ -293,8 +286,7 @@ class BaseCamera(lt.Thing):
metadata_getter: lt.deps.GetThingStates, metadata_getter: lt.deps.GetThingStates,
buffer_max: int = 1, buffer_max: int = 1,
) -> None: ) -> None:
""" """Capture an image to memory. This can be saved later with ``save_from_memory``
Capture an image to memory. This can be saved later with ``save_from_memory``
Note that only one image is held in memory so this will overwrite any image Note that only one image is held in memory so this will overwrite any image
in memory. in memory.
@ -319,8 +311,7 @@ class BaseCamera(lt.Thing):
save_resolution: Optional[Tuple[int, int]] = None, save_resolution: Optional[Tuple[int, int]] = None,
buffer_id: Optional[int] = None, buffer_id: Optional[int] = None,
) -> None: ) -> None:
""" """Save an image that has been captured to memory.
Save an image that has been captured to memory.
:param jpeg_path: The path to save the file to :param jpeg_path: The path to save the file to
:param logger: This should be injected automatically by Labthings FastAPI :param logger: This should be injected automatically by Labthings FastAPI

View file

@ -62,8 +62,7 @@ class PicameraStreamOutput(Output):
class SensorMode(BaseModel): class SensorMode(BaseModel):
""" """A Pydantic model holding all the information about a specific
A Pydantic model holding all the information about a specific
sensor mode as reported by the PiCamera. sensor mode as reported by the PiCamera.
""" """
@ -77,8 +76,7 @@ class SensorMode(BaseModel):
class SensorModeSelector(BaseModel): class SensorModeSelector(BaseModel):
""" """A Pydantic model holding the two values needed to select a PiCamera
A Pydantic model holding the two values needed to select a PiCamera
Sensor mode. The output size and the bit depth. Sensor mode. The output size and the bit depth.
This is a Pydantic modell so that it can be saved to the disk. This is a Pydantic modell so that it can be saved to the disk.
@ -89,8 +87,7 @@ class SensorModeSelector(BaseModel):
class LensShading(BaseModel): class LensShading(BaseModel):
""" """A Pydantic model holding the lens shading tables.
A Pydantic model holding the lens shading tables.
PiCamera needs three numpy arrays for lens shading correction. Each array is PiCamera needs three numpy arrays for lens shading correction. Each array is
(12, 16) in size. The arrays are luminance, red-difference chroma (Cr), and (12, 16) in size. The arrays are luminance, red-difference chroma (Cr), and
@ -363,8 +360,7 @@ class StreamingPiCamera2(BaseCamera):
def start_streaming( def start_streaming(
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6 self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6
) -> None: ) -> None:
""" """Start the MJPEG stream. This is where persistent controls are sent to camera.
Start the MJPEG stream. This is where persistent controls are sent to camera.
Sets the camera resolutions based on input parameters, and sets the low-res Sets the camera resolutions based on input parameters, and sets the low-res
resolution to (320, 240). Note: (320, 240) is a standard from the Pi Camera resolution to (320, 240). Note: (320, 240) is a standard from the Pi Camera
@ -434,9 +430,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_action @lt.thing_action
def stop_streaming(self, stop_web_stream: bool = True) -> None: def stop_streaming(self, stop_web_stream: bool = True) -> None:
""" """Stop the MJPEG stream"""
Stop the MJPEG stream
"""
with self._streaming_picamera() as picam: with self._streaming_picamera() as picam:
try: try:
picam.stop_recording() # This should also stop the extra lores encoder picam.stop_recording() # This should also stop the extra lores encoder
@ -679,8 +673,7 @@ class StreamingPiCamera2(BaseCamera):
@lt.thing_action @lt.thing_action
def reset_ccm(self): def reset_ccm(self):
""" """Overwrite the colour correction matrix in camera tuning with default values.
Overwrite the colour correction matrix in camera tuning with default values.
These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page
45. 45.

View file

@ -1,5 +1,4 @@
""" """Functions to set up a Raspberry Pi Camera v2 for scientific use
Functions to set up a Raspberry Pi Camera v2 for scientific use
This module provides slower, simpler functions to set the This module provides slower, simpler functions to set the
gain, exposure, and white balance of a Raspberry Pi camera, using gain, exposure, and white balance of a Raspberry Pi camera, using

View file

@ -1,5 +1,4 @@
""" """OpenFlexure Microscope API extension for stage calibration
OpenFlexure Microscope API extension for stage calibration
This file contains the HTTP API for camera/stage calibration. It This file contains the HTTP API for camera/stage calibration. It
includes calibration functions that measure the relationship between includes calibration functions that measure the relationship between

View file

@ -1,5 +1,4 @@
""" """OpenFlexure Settings Management
OpenFlexure Settings Management
This module provides some settings management across the other Things, and This module provides some settings management across the other Things, and
for code that currently lives in clients but needs to persist settings on for code that currently lives in clients but needs to persist settings on

View file

@ -229,8 +229,7 @@ class SmartScanThing(lt.Thing):
@_scan_running @_scan_running
def _calc_displacement_from_test_image(self, overlap: int) -> tuple[int, int]: def _calc_displacement_from_test_image(self, overlap: int) -> tuple[int, int]:
""" """Take a test image and use camera stage mapping to calculate x and y displacement
Take a test image and use camera stage mapping to calculate x and y displacement
:param overlap: The desired overlap as a fraction of the image. i.e. 0.5 means :param overlap: The desired overlap as a fraction of the image. i.e. 0.5 means
that each image should overlap its nearest neighbour by 50%. that each image should overlap its nearest neighbour by 50%.
@ -312,8 +311,7 @@ class SmartScanThing(lt.Thing):
@_scan_running @_scan_running
def _save_scan_inputs_json(self): def _save_scan_inputs_json(self):
""" """Save scan inputs as a JSON file in the scan folder, to allow
Save scan inputs as a JSON file in the scan folder, to allow
the user to review the settings used in the scan the user to review the settings used in the scan
""" """
# Should this be a method of the scan_data dataclass? # Should this be a method of the scan_data dataclass?
@ -337,8 +335,7 @@ class SmartScanThing(lt.Thing):
@_scan_running @_scan_running
def _update_scan_data_json(self, scan_result: str): def _update_scan_data_json(self, scan_result: str):
""" """Update scan data as a JSON file in the scan folder, with
Update scan data as a JSON file in the scan folder, with
data only known at the end of the scan. data only known at the end of the scan.
Takes scan_result, a string that is either "success", Takes scan_result, a string that is either "success",
@ -372,9 +369,7 @@ class SmartScanThing(lt.Thing):
@_scan_running @_scan_running
def _manage_stitching_threads(self): def _manage_stitching_threads(self):
""" """Manage the stitching threads, starting them if needed and not already running."""
Manage the stitching threads, starting them if needed and not already running.
"""
# Assume 4 images means at least one offset in x and y, making the stitching # Assume 4 images means at least one offset in x and y, making the stitching
# well constrained. # well constrained.
if self._scan_images_taken > 3: if self._scan_images_taken > 3:
@ -383,8 +378,7 @@ class SmartScanThing(lt.Thing):
@_scan_running @_scan_running
def _run_scan(self): def _run_scan(self):
""" """Prepare and run the main scan, handling the threads performing
Prepare and run the main scan, handling the threads performing
stitching. Uses the result (or exception) from the scanning to stitching. Uses the result (or exception) from the scanning to
determine whether the scan should be stitched and the microscope determine whether the scan should be stitched and the microscope
should return to the starting x,y,z position should return to the starting x,y,z position
@ -703,9 +697,7 @@ class SmartScanThing(lt.Thing):
@lt.thing_action @lt.thing_action
def purge_empty_scans(self, logger: lt.deps.InvocationLogger) -> None: def purge_empty_scans(self, logger: lt.deps.InvocationLogger) -> None:
""" """Delete all scan folders containing no images at the top level"""
Delete all scan folders containing no images at the top level
"""
# JSON is ignored as it's created before any images are captured # JSON is ignored as it's created before any images are captured
for scan_info in self._scan_dir_manager.all_scans_info(): for scan_info in self._scan_dir_manager.all_scans_info():
if scan_info.number_of_images == 0: if scan_info.number_of_images == 0:
@ -813,9 +805,7 @@ class SmartScanThing(lt.Thing):
@_scan_running @_scan_running
def _preview_stitch_wait(self): def _preview_stitch_wait(self):
""" """Wait for an ongoing preview stitch to return"""
Wait for an ongoing preview stitch to return
"""
if self._preview_stitch_running(): if self._preview_stitch_running():
with self._preview_stitch_popen_lock: with self._preview_stitch_popen_lock:
self._preview_stitch_popen.wait() self._preview_stitch_popen.wait()
@ -826,8 +816,7 @@ class SmartScanThing(lt.Thing):
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
cmd: list[str], cmd: list[str],
) -> CompletedProcess: ) -> CompletedProcess:
""" """Run a subprocess and log any output
Run a subprocess and log any output
Raises: Raises:
ChildProcessError if exit code is not zero ChildProcessError if exit code is not zero

View file

@ -1,5 +1,4 @@
""" """OpenFlexure Microscope system control Thing
OpenFlexure Microscope system control Thing
This module defines a Thing that can shut down or restart the host computer. This module defines a Thing that can shut down or restart the host computer.
""" """
@ -16,15 +15,11 @@ class CommandOutput(BaseModel):
class SystemControlThing(lt.Thing): class SystemControlThing(lt.Thing):
""" """Attempt to shutdown the device"""
Attempt to shutdown the device
"""
@lt.thing_action @lt.thing_action
def shutdown(self) -> CommandOutput: def shutdown(self) -> CommandOutput:
""" """Attempt to shutdown the device"""
Attempt to shutdown the device
"""
p = subprocess.Popen( p = subprocess.Popen(
["sudo", "shutdown", "-h", "now"], ["sudo", "shutdown", "-h", "now"],
stderr=subprocess.PIPE, stderr=subprocess.PIPE,

View file

@ -17,9 +17,7 @@ class ErrorCapturingThread(Thread):
""" """
def __init__(self, group=None, target=None, args=None, kwargs=None, daemon=None): def __init__(self, group=None, target=None, args=None, kwargs=None, daemon=None):
""" """Initialise with the same arguments as Thread"""
Initialise with the same arguments as Thread
"""
# As all inputs are keywords we need to set the default values for args and kwargs: # As all inputs are keywords we need to set the default values for args and kwargs:
if args is None: if args is None:
args = () args = ()
@ -45,8 +43,7 @@ class ErrorCapturingThread(Thread):
) )
def join(self, timeout=None): def join(self, timeout=None):
""" """Join when the thread is complete. If the thread ended due to an unhandled exception,
Join when the thread is complete. If the thread ended due to an unhandled exception,
the exception will be raised when this method is called. the exception will be raised when this method is called.
""" """
super().join(timeout) super().join(timeout)

View file

@ -1,6 +1,4 @@
""" """Tests for the built in buffer for the camera."""
Tests for the built in buffer for the camera.
"""
from random import randint from random import randint

View file

@ -156,9 +156,7 @@ def test_basic_directory_operations():
def test_scan_sequence_and_listing(): def test_scan_sequence_and_listing():
""" """Check created scans are added in order and listed correctly"""
Check created scans are added in order and listed correctly
"""
_clear_scan_dir() _clear_scan_dir()
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
# Make 4 scans # Make 4 scans
@ -185,8 +183,7 @@ def test_scan_sequence_and_listing():
def test_scan_name_non_sequential(): def test_scan_name_non_sequential():
""" """Check created scans is the correct name if the directories
Check created scans is the correct name if the directories
are not sequential are not sequential
""" """
_clear_scan_dir() _clear_scan_dir()
@ -213,9 +210,7 @@ def test_scan_name_non_sequential():
def test_all_scan_names_taken(): def test_all_scan_names_taken():
""" """If the next sequential scan name needs more than 4 digits check error is thrown."""
If the next sequential scan name needs more than 4 digits check error is thrown.
"""
_clear_scan_dir() _clear_scan_dir()
os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_9999")) os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_9999"))
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
@ -224,9 +219,7 @@ def test_all_scan_names_taken():
def test_no_scan_names_given(): def test_no_scan_names_given():
""" """Check correct default scan name is used if empty string is given"""
Check correct default scan name is used if empty string is given
"""
_clear_scan_dir() _clear_scan_dir()
scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR)
scan_dir = scan_dir_manager.new_scan_dir("") scan_dir = scan_dir_manager.new_scan_dir("")

View file

@ -222,8 +222,7 @@ def test_mark_wrong_location():
def test_closest_focus_wth_large_numbers(): def test_closest_focus_wth_large_numbers():
""" """The number of steps gets very large in reality runs some tests to check
The number of steps gets very large in reality runs some tests to check
that everything works well with huge numbers of steps that everything works well with huge numbers of steps
""" """
intial_position = (0, 0) intial_position = (0, 0)

View file

@ -1,5 +1,4 @@
""" """Test the SmartScanThing *without* connecting it to a LabThings Server.
Test the SmartScanThing *without* connecting it to a LabThings Server.
By testing without connecting to the LabThings server it is possible to By testing without connecting to the LabThings server it is possible to
directly poll any properties and to start any methods. directly poll any properties and to start any methods.
@ -151,8 +150,7 @@ def test_delete_all_scans(smart_scan_thing, caplog):
def _run_only_outer_scan(adjust_inital_state: Optional[Callable] = None): def _run_only_outer_scan(adjust_inital_state: Optional[Callable] = None):
""" """Create a subclass of SmartScanThing to mock _run_scan and run sample_scan
Create a subclass of SmartScanThing to mock _run_scan and run sample_scan
This should do all the set up for a scan, move into the mocked This should do all the set up for a scan, move into the mocked
_run_scan method where this can be tested. Once this is done _run_scan method where this can be tested. Once this is done

View file

@ -1,5 +1,4 @@
""" """Tests for the smart/fast stacking.
Tests for the smart/fast stacking.
Currently these tests don't test the Thing itself, just surrounding functionality Currently these tests don't test the Thing itself, just surrounding functionality
""" """
@ -146,8 +145,7 @@ def test_even_images_to_save(save_ims, extra_ims):
def test_computed_stack_params(): def test_computed_stack_params():
""" """Test StackParams computed properties are as expected
Test StackParams computed properties are as expected
Not using hypothesis or we will just copy in the same formulas. Not using hypothesis or we will just copy in the same formulas.
""" """
@ -188,8 +186,7 @@ def test_computed_stack_params():
def random_capture(set_id: Optional[int] = None): def random_capture(set_id: Optional[int] = None):
""" """Create a capture with random values
Create a capture with random values
:param set_id: Optional, use to set a fixed id rather than a random one :param set_id: Optional, use to set a fixed id rather than a random one
""" """
@ -206,18 +203,14 @@ def random_capture(set_id: Optional[int] = None):
def test_capture_filename_matches_regex(): def test_capture_filename_matches_regex():
""" """For 100 random captures check the image always matches the regex"""
For 100 random captures check the image always matches the regex
"""
for _ in range(100): for _ in range(100):
assert IMAGE_REGEX.search(random_capture().filename) assert IMAGE_REGEX.search(random_capture().filename)
@given(st.integers(min_value=0, max_value=5000)) @given(st.integers(min_value=0, max_value=5000))
def test_retrieval_of_captures(start): def test_retrieval_of_captures(start):
""" """For 20 random captures, check each can be retrieved correctly by id"""
For 20 random captures, check each can be retrieved correctly by id
"""
captures = [random_capture(start + i) for i in range(20)] captures = [random_capture(start + i) for i in range(20)]
for i, capture in enumerate(captures): for i, capture in enumerate(captures):

View file

@ -1,5 +1,4 @@
""" """Utilities for testing and debugging.
Utilities for testing and debugging.
At the top level are some basic testing functions. More specific testing utilities are At the top level are some basic testing functions. More specific testing utilities are
provided by modules inside the package. provided by modules inside the package.

View file

@ -14,21 +14,18 @@ THIS_DIR = os.path.dirname(os.path.realpath(__file__))
class FakeSample: class FakeSample:
""" """A fake sample to test scan algorithms. The sample is able to return
A fake sample to test scan algorithms. The sample is able to return
whether a given position is sample, no image associated with the sample whether a given position is sample, no image associated with the sample
""" """
def __init__(self, xy_points: list[tuple[int, int]]): def __init__(self, xy_points: list[tuple[int, int]]):
""" """Create the sample from a spline interpolation around
Create the sample from a spline interpolation around
the given points. the given points.
""" """
self._sample_perimeter = interp_closed_path(xy_points, 500) self._sample_perimeter = interp_closed_path(xy_points, 500)
def is_sample(self, pos: tuple[int, int], im_size: tuple[int, int]) -> bool: def is_sample(self, pos: tuple[int, int], im_size: tuple[int, int]) -> bool:
""" """Return whether an image at a given location with a given image size
Return whether an image at a given location with a given image size
is on the sample is on the sample
This doesn't check the entire image field as this is designed to be used This doesn't check the entire image field as this is designed to be used
@ -47,18 +44,14 @@ class FakeSample:
@property @property
def patch(self) -> PathPatch: def patch(self) -> PathPatch:
""" """The sample as a matplotlib patch for plotting"""
The sample as a matplotlib patch for plotting
"""
patch = PathPatch(self._sample_perimeter) patch = PathPatch(self._sample_perimeter)
patch.set(color=(1.0, 0.8, 1.0, 1.0)) patch.set(color=(1.0, 0.8, 1.0, 1.0))
return patch return patch
def visualise_scan(sample: FakeSample, planner: scan_planners.ScanPlanner) -> Figure: def visualise_scan(sample: FakeSample, planner: scan_planners.ScanPlanner) -> Figure:
""" """For a given sample and scanner object return a matplotlib figure of the scan"""
For a given sample and scanner object return a matplotlib figure of the scan
"""
fig, ax = plt.subplots(figsize=(8, 8)) fig, ax = plt.subplots(figsize=(8, 8))
ax.add_artist(sample.patch) ax.add_artist(sample.patch)
xh, yh = zip(*planner._path_history) xh, yh = zip(*planner._path_history)
@ -84,8 +77,7 @@ def visualise_scan(sample: FakeSample, planner: scan_planners.ScanPlanner) -> Fi
def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPath: def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPath:
""" """Given a lists of xy_points interpolate an n_point closed curve. This can be used
Given a lists of xy_points interpolate an n_point closed curve. This can be used
to create an arbitrary sample shape plan a scan. to create an arbitrary sample shape plan a scan.
Modified from: Modified from:
@ -113,8 +105,7 @@ def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPa
def example_smart_spiral( def example_smart_spiral(
sample_name: str = "lobed", sample_name: str = "lobed",
) -> tuple[FakeSample, scan_planners.ScanPlanner]: ) -> tuple[FakeSample, scan_planners.ScanPlanner]:
""" """Run an example scan and return the sample scanned and the planner object
Run an example scan and return the sample scanned and the planner object
after scan is complete after scan is complete
""" """
xy_sample_points = load_sample_points(sample_name) xy_sample_points = load_sample_points(sample_name)
@ -135,8 +126,7 @@ def example_smart_spiral(
def profile_and_save_plot_for_example_smart_spiral(): def profile_and_save_plot_for_example_smart_spiral():
""" """Run the example scan and save a plot and the profile data
Run the example scan and save a plot and the profile data
Also print the cumulative stats Also print the cumulative stats
@ -162,8 +152,7 @@ def profile_and_save_plot_for_example_smart_spiral():
def update_example_smart_spiral_pickle(sample_name: str): def update_example_smart_spiral_pickle(sample_name: str):
""" """Pickle the ScanPlanner for the example_smart_spiral(),
Pickle the ScanPlanner for the example_smart_spiral(),
this is done so the history can be compared by testing to check this is done so the history can be compared by testing to check
the algorithm is unchanged. the algorithm is unchanged.
@ -182,8 +171,7 @@ def update_example_smart_spiral_pickle(sample_name: str):
def get_expected_result_for_example_smart_spiral( def get_expected_result_for_example_smart_spiral(
sample_name: str, sample_name: str,
) -> scan_planners.ScanPlanner: ) -> scan_planners.ScanPlanner:
""" """Return the expected ScanPlanner object for the example_smart_spiral(),
Return the expected ScanPlanner object for the example_smart_spiral(),
this is pickled, so that it can be committed. this is pickled, so that it can be committed.
""" """
pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl") pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl")