diff --git a/hardware-specific-tests/picamera2/test_exposure_time_drift.py b/hardware-specific-tests/picamera2/test_exposure_time_drift.py index ffd81914..8e566612 100644 --- a/hardware-specific-tests/picamera2/test_exposure_time_drift.py +++ b/hardware-specific-tests/picamera2/test_exposure_time_drift.py @@ -18,7 +18,7 @@ logging.basicConfig(level=logging.DEBUG) 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 """ diff --git a/hardware-specific-tests/picamera2/test_sensor_mode.py b/hardware-specific-tests/picamera2/test_sensor_mode.py index 0fa24099..9b738065 100644 --- a/hardware-specific-tests/picamera2/test_sensor_mode.py +++ b/hardware-specific-tests/picamera2/test_sensor_mode.py @@ -12,7 +12,7 @@ logging.basicConfig(level=logging.DEBUG) def test_sensor_mode(): - """Test capturing raw arrays in two different sensor modes""" + """Test capturing raw arrays in two different sensor modes.""" cam = StreamingPiCamera2() server = ThingServer() server.add_thing(cam, "/camera/") diff --git a/hardware-specific-tests/picamera2/test_tuning.py b/hardware-specific-tests/picamera2/test_tuning.py index 5decd77b..bed9e2fd 100644 --- a/hardware-specific-tests/picamera2/test_tuning.py +++ b/hardware-specific-tests/picamera2/test_tuning.py @@ -1,4 +1,4 @@ -"""Tests that check that a tuning file can be reloaded""" +"""Tests that check that a tuning file can be reloaded.""" import os diff --git a/integration-tests/testfile.py b/integration-tests/testfile.py index 2fd8fb58..a61fdb64 100755 --- a/integration-tests/testfile.py +++ b/integration-tests/testfile.py @@ -30,7 +30,7 @@ SERVER_CMD: list[str] = [ def main() -> None: - """Set up the server, run basic checks, shutdown, check for graceful exit + """Set up the server, run basic checks, shutdown, check for graceful exit. The basic checks include checks that: - The server boots @@ -69,7 +69,7 @@ def main() -> None: def test_client_connection() -> None: - """Check a ThingClient can interact with the simulation microscope camera""" + """Check a ThingClient can interact with the simulation microscope camera.""" print("Connecting Python client to microscope, and capturing image") cam_client = lt.ThingClient.from_url("http://localhost:5000/camera/") img = Image.open(cam_client.grab_jpeg().open()) @@ -178,7 +178,7 @@ def error_if_server_not_started( def check_for_graceful_shutdown(server_process: subprocess.Popen) -> None: - """Check the server shutdown gracefully + """Check the server shutdown gracefully. Check the subprocess is not running Check the logs have no errors @@ -201,7 +201,7 @@ def check_for_graceful_shutdown(server_process: subprocess.Popen) -> None: def read_process_buffers(process: subprocess.Popen) -> list[str]: - """Return STDOUT from a process""" + """Return STDOUT from a process.""" stdout = [] while line := process.stdout.readline(): diff --git a/pyproject.toml b/pyproject.toml index cb45e121..531b8d30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,11 +127,8 @@ ignore = [ "E501", # Ignore long lines for now "D203", # incompatible with D204 "D213", # incompatible with D212 - "D400", # A stricter version of #415 - # Below are checkers to be turned on gradually - "D415", - "D200", - # These should be turned on before this MR is complete + "D400", # A stricter version of #415 that doesn't allow ! + # The checkers below should be turned on as they complain about missing docstrings. "D100", "D102", "D101", diff --git a/scripts/zenodo/upload_to_zenodo.py b/scripts/zenodo/upload_to_zenodo.py index a2709e9e..3fff944f 100644 --- a/scripts/zenodo/upload_to_zenodo.py +++ b/scripts/zenodo/upload_to_zenodo.py @@ -25,7 +25,7 @@ def parse_arguments() -> Namespace: def script_directory(path): - """Resolve path to directory of the current script""" + """Resolve path to directory of the current script.""" return os.path.join(os.path.dirname(os.path.realpath(__file__)), path) diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index 7ecdf0ac..dfad0937 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -87,7 +87,7 @@ class OFMHandler(logging.Handler): self._log.pop(0) 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 # These are the logs each time an API endpoint is accessed # This is only the log for the UI. @@ -105,7 +105,7 @@ class OFMHandler(logging.Handler): @property 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) diff --git a/src/openflexure_microscope_server/scan_directories.py b/src/openflexure_microscope_server/scan_directories.py index c8289bb2..1a47262d 100644 --- a/src/openflexure_microscope_server/scan_directories.py +++ b/src/openflexure_microscope_server/scan_directories.py @@ -20,7 +20,7 @@ class NotEnoughFreeSpaceError(IOError): class ScanInfo(BaseModel): - """Summary information about a scan folder""" + """Summary information about a scan folder.""" name: str created: float @@ -31,7 +31,7 @@ class ScanInfo(BaseModel): class ScanDirectoryManager: - """A class for managing interactions with scan directories""" + """A class for managing interactions with scan directories.""" _base_scan_dir: str @@ -42,22 +42,22 @@ class ScanDirectoryManager: @property def base_dir(self) -> str: - """The base directory scans are saved to""" + """The base directory scans are saved to.""" return self._base_scan_dir def exists(self, scan_name: str) -> bool: - """Return True if scan of this name exists on disk""" + """Return True if scan of this name exists on disk.""" return os.path.isdir(self.path_for(scan_name)) def path_for(self, scan_name: str) -> str: - """Return the path for a given scan name + """Return the path for a given scan name. Returns the path even if it doesn't exist """ return os.path.join(self._base_scan_dir, scan_name) def img_dir_for(self, scan_name: str) -> str: - """Return the path for the image dir for a given scan name + """Return the path for the image dir for a given scan name. Returns the path even if it doesn't exist """ @@ -66,7 +66,7 @@ class ScanDirectoryManager: def get_file_path_from( self, scan_name: str, filename: str, check_exists: bool = False ) -> Optional[str]: - """Return the full file path for the file within a scan directory + """Return the full file path for the file within a scan directory. If check_exists is True then None will be returned if the file does not exist. @@ -80,7 +80,7 @@ class ScanDirectoryManager: def get_file_path_from_img_dir( self, scan_name: str, filename: str, check_exists: bool = False ) -> Optional[str]: - """Return the full file path for the file within a scan directory + """Return the full file path for the file within a scan directory. If check_exists is True, None is returned if the file does not exist. If False then the path is returned anyway @@ -103,18 +103,18 @@ class ScanDirectoryManager: @property def all_scans(self) -> list[str]: - """Return a list of the scan names in the base directory""" + """Return a list of the scan names in the base directory.""" return [f.name for f in os.scandir(self._base_scan_dir) if f.is_dir()] def all_scans_info(self) -> list[ScanInfo]: - """Return a lists of ScanInfo objects for each scan""" + """Return a lists of ScanInfo objects for each scan.""" all_info: list[ScanInfo] = [] for scan_name in self.all_scans: all_info.append(ScanDirectory(scan_name, self.base_dir).scan_info()) return all_info def _unique_scan_name(self, scan_name: str) -> str: - """Get the next unique scan name starting with the given name + """Get the next unique scan name starting with the given name. For more explanation on the scan naming see `new_scan_dir` """ @@ -143,7 +143,7 @@ class ScanDirectoryManager: return f"{scan_name}_{scan_num:0{SCAN_ZERO_PAD_DIGITS}d}" def new_scan_dir(self, scan_name: str) -> "ScanDirectory": - """Get a unique name for this scan and create a directory for it + """Get a unique name for this scan and create a directory for it. The scan will be named ``{scan_name}_0001`` where the number is zero-padded to be SCAN_ZERO_PAD_DIGITS digits long (to allow correct sorting if the @@ -165,11 +165,11 @@ class ScanDirectoryManager: return ScanDirectory(full_scan_name, self.base_dir) def delete_scan(self, scan_name: str) -> None: - """Delete a scan""" + """Delete a scan.""" shutil.rmtree(self.path_for(scan_name)) def zip_scan(self, scan_name: str, final_version: bool = False) -> "ScanDirectory": - """Zips any images from the scan not yet zipped, return full path to zip + """Zips any images from the scan not yet zipped, return full path to zip. ``final_version`` Set true to stitch all files not just the scan images this should only be done at the end as it is not possible to update a file @@ -178,7 +178,7 @@ class ScanDirectoryManager: return ScanDirectory(scan_name, self.base_dir).zip_files(final_version) 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. Default = 500,000,000 (500MB) @@ -194,7 +194,7 @@ class ScanDirectoryManager: class ScanDirectory: - """A class for handling interactions with scan directories + """A class for handling interactions with scan directories. Initalisation parameters: name: the name of the scan (the scan directory basename) @@ -214,12 +214,12 @@ class ScanDirectory: @property def name(self) -> str: - """The name of the scan""" + """The name of the scan.""" return self._name @property def dir_path(self) -> str: - """The full path to the scan directory""" + """The full path to the scan directory.""" return os.path.join(self._base_scan_dir, self._name) @property @@ -235,17 +235,17 @@ class ScanDirectory: @property def created_time(self) -> float: - """The time the directory was created on disk""" + """The time the directory was created on disk.""" return os.path.getctime(self.dir_path) def get_scan_files(self): - """Return a list of the files in the images dir""" + """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]): - """Extract files which match the naming convention for scan images + """Extract files which match the naming convention for scan images. :param file_list: The list of files to search. Normally this would be ``self.get_scan_files()`` @@ -255,7 +255,7 @@ class ScanDirectory: return [i for i in file_list if IMAGE_REGEX.search(i)] def _extract_final_stitches(self, file_list: list[str]): - """Extract files which match the naming convention for final stitches + """Extract files which match the naming convention for final stitches. :param file_list: The list of files to search. @@ -264,7 +264,7 @@ class ScanDirectory: return [i for i in file_list if STITCH_REGEX.search(i)] def _extract_dzi_files(self, file_list: list[str]): - """Extract files which match the naming convention for dzi_files + """Extract files which match the naming convention for dzi_files. :param file_list: The list of files to search. @@ -273,7 +273,7 @@ class ScanDirectory: return [i for i in file_list if i.endswith("dzi")] def get_final_stitch_name(self) -> Optional[str]: - """Return the filename for the final stitch (in the images dir) + """Return the filename for the final stitch (in the images dir). If no final stitch is found, return None """ @@ -283,11 +283,11 @@ class ScanDirectory: return stitches[0] def get_modified_time(self) -> float: - """Return the modified time of the directory""" + """Return the modified time of the directory.""" return max(os.stat(root).st_mtime for root, _, _ in os.walk(self.dir_path)) def scan_info(self) -> ScanInfo: - """Return the information for the scan directory as a ScanInfo object""" + """Return the information for the scan directory as a ScanInfo object.""" scan_files = self.get_scan_files() scan_images = self._extract_scan_images(scan_files) stitches = self._extract_final_stitches(scan_files) @@ -325,7 +325,7 @@ class ScanDirectory: return files def zip_files(self, final_version: bool = False) -> str: - """Zips any images from the scan not yet zipped, return full path to zip + """Zips any images from the scan not yet zipped, return full path to zip. ``final_version`` Set true to stitch all files not just the scan images this should only be done at the end as it is not possible to update a file @@ -357,6 +357,6 @@ class ScanDirectory: def get_files_in_zip(zip_path): - """List the relative paths of all files and folders in the zip folder specified""" + """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()] diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 8d159ad6..59ba1469 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -115,7 +115,7 @@ class ScanPlanner: @property 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) @property @@ -145,12 +145,12 @@ class ScanPlanner: raise NotImplementedError("Did you call the ScanPlanner base class?") 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! return tuple(position) in self._path_history 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! return tuple(position) in self._remaining_locations @@ -205,7 +205,7 @@ class ScanPlanner: def mark_location_visited( self, xyz_pos: XYZPos, imaged: bool, focused: bool ) -> None: - """Mark the location as visited + """Mark the location as visited. Args: xyz_pos: the x_y_z position @@ -282,7 +282,7 @@ class SmartSpiral(ScanPlanner): def mark_location_visited( self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True ) -> None: - """Mark the location as visited. Adjust extra positions accordingly + """Mark the location as visited. Adjust extra positions accordingly. Args: xyz_pos: the x_y_z position @@ -327,7 +327,7 @@ class SmartSpiral(ScanPlanner): self._remaining_locations.append(new_pos) 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 def sort_key(pos): @@ -421,7 +421,7 @@ class SmartSpiral(ScanPlanner): def distance_between( current_pos: XYPos | np.ndarray, next_pos: XYPos | np.ndarray ) -> float: - """Calculate the distance between the two xy positions + """Calculate the distance between the two xy positions. This was previously called ``distance_to_site`` """ diff --git a/src/openflexure_microscope_server/server/__init__.py b/src/openflexure_microscope_server/server/__init__.py index 601b9f62..9959978f 100644 --- a/src/openflexure_microscope_server/server/__init__.py +++ b/src/openflexure_microscope_server/server/__init__.py @@ -14,7 +14,7 @@ from ..logging import configure_logging, retrieve_log, retrieve_log_from_file 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 the uvicorn ``lifecycle`` events and FastAPI ``shutdown`` events only fire once @@ -65,7 +65,7 @@ def _get_scans_dir(config: dict) -> Optional[str]: def serve_from_cli(argv: Optional[list[str]] = None): - """Start the server from the command line""" + """Start the server from the command line.""" args = lt.cli.parse_args(argv) log_config = copy(uvicorn.config.LOGGING_CONFIG) diff --git a/src/openflexure_microscope_server/server/serve_static_files.py b/src/openflexure_microscope_server/server/serve_static_files.py index 9bffe511..af708f2e 100644 --- a/src/openflexure_microscope_server/server/serve_static_files.py +++ b/src/openflexure_microscope_server/server/serve_static_files.py @@ -25,7 +25,7 @@ def add_static_file(app: FastAPI, fname: str, folder: str) -> None: def add_static_files(app: FastAPI, scans_folder: Optional[str]) -> None: - """Add the static files responsible for the webapp app to the FastAPI app + """Add the static files responsible for the webapp app to the FastAPI app. app: The FastAPI app to add to, in this case the OpenFlexure server """ diff --git a/src/openflexure_microscope_server/things/auto_recentre_stage.py b/src/openflexure_microscope_server/things/auto_recentre_stage.py index 56bcd78b..2b68bbbc 100644 --- a/src/openflexure_microscope_server/things/auto_recentre_stage.py +++ b/src/openflexure_microscope_server/things/auto_recentre_stage.py @@ -22,7 +22,7 @@ class RecentringThing(lt.Thing): max_steps=15, lateral_distance=5000, ): - """Recentre the stage, based on the focal plane + """Recentre the stage, based on the focal plane. Autofocuses at multiple points around the sample to find the overall maximum (or minimum) height, which diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 177ba811..2b894aa3 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -1,4 +1,4 @@ -"""OpenFlexure Microscope autofocus module +"""OpenFlexure Microscope autofocus module. This module defines a Thing that is responsible for using the stage and camera together to perform an autofocus routine. @@ -26,7 +26,7 @@ from .stage import StageDependency as Stage class StackParams: - """A class for holding for scan parameters + """A class for holding for scan parameters. All arguments are keyword only @@ -97,7 +97,7 @@ class StackParams: @property def stack_z_range(self) -> int: - """The range of the z stack, in steps + """The range of the z stack, in steps. Note that this is the range of the minimum number of images captured, which is also the range of the images stored in memory that can be @@ -107,7 +107,7 @@ class StackParams: @property 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 a stack too high requires it to move to the start, # autofocus and then re-stack. Starting slightly too low only @@ -116,14 +116,14 @@ class StackParams: @property def max_images_to_test(self) -> int: - """The maximum number of images that will be captured and tested in a stack + """The maximum number of images that will be captured and tested in a stack. This is 15 images more then the minimum number that are captured. """ return self.min_images_to_test + 15 def slice_to_save(self, sharpest_index): - """Return the slice of images to save given the index of the sharpest image""" + """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( max(sharpest_index - images_each_side, 0), @@ -133,7 +133,7 @@ class StackParams: @dataclass class CaptureInfo: - """The information from a capture in a z_stack""" + """The information from a capture in a z_stack.""" buffer_id: int position: dict[str, int] @@ -141,7 +141,7 @@ class CaptureInfo: @property def filename(self) -> str: - """The filename for this image generated from the position""" + """The filename for this image generated from the position.""" return f"{self.position['x']}_{self.position['y']}_{self.position['z']}.jpeg" @@ -195,7 +195,7 @@ class JPEGSharpnessMonitor: running = False async def monitor_sharpness(self): - """Start monitoring the frame sizes""" + """Start monitoring the frame sizes.""" self.running = True async for frame in self.camera.lores_mjpeg_stream.frame_async_generator(): self.jpeg_times.append(time.time()) @@ -205,7 +205,7 @@ class JPEGSharpnessMonitor: @contextmanager def run(self): - """Context manager, during which we will monitor sharpness from the camera""" + """Context manager, during which we will monitor sharpness from the camera.""" self.portal.start_task_soon(self.monitor_sharpness) try: yield @@ -233,7 +233,7 @@ class JPEGSharpnessMonitor: def move_data( self, istart: int, istop: Optional[int] = None ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Extract sharpness as a function of (interpolated) z""" + """Extract sharpness as a function of (interpolated) z.""" if istop is None: istop = istart + 2 jpeg_times: np.ndarray = np.array(self.jpeg_times) @@ -261,7 +261,7 @@ class JPEGSharpnessMonitor: return jpeg_times, jpeg_zs, jpeg_sizes[start:stop] def sharpest_z_on_move(self, index: int) -> int: - """Return the z position of the sharpest image on a given move""" + """Return the z position of the sharpest image on a given move.""" _, jz, js = self.move_data(index) if len(js) == 0: raise ValueError( @@ -270,7 +270,7 @@ class JPEGSharpnessMonitor: return jz[np.argmax(js)] def data_dict(self) -> SharpnessDataArrays: - """Return the gathered data as a single convenient dictionary""" + """Return the gathered data as a single convenient dictionary.""" data = {} for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]: data[k] = getattr(self, k) @@ -295,7 +295,7 @@ class AutofocusThing(lt.Thing): dz: int = 2000, start: str = "centre", ) -> SharpnessDataArrays: - """Sweep the stage up and down, then move to the sharpest point + """Sweep the stage up and down, then move to the sharpest point. This method will will move down by dz/2, sweep up by dz, and then evaluate the position where the image was sharpest. We'll then move back down, and @@ -325,7 +325,7 @@ class AutofocusThing(lt.Thing): dz: Sequence[int], wait: float = 0, ) -> SharpnessDataArrays: - """Make a move (or a series of moves) and monitor sharpness + """Make a move (or a series of moves) and monitor sharpness. This method will will make a series of relative moves in z, and return the sharpness (JPEG size) vs time, along with timestamps @@ -561,7 +561,7 @@ class AutofocusThing(lt.Thing): cam: WrappedCamera, stage: Stage, ) -> tuple[bool, list[CaptureInfo], Optional[int]]: - """Capture a series of images checking that sharpest image central + """Capture a series of images checking that sharpest image central. The images are seperated in z offset by stack_parameters.stack_dz, as they are captured the last stack_parameters.min_images_to_test images are checked diff --git a/src/openflexure_microscope_server/things/background_detect.py b/src/openflexure_microscope_server/things/background_detect.py index f27b9096..7ada837f 100644 --- a/src/openflexure_microscope_server/things/background_detect.py +++ b/src/openflexure_microscope_server/things/background_detect.py @@ -22,7 +22,7 @@ class BackgroundDetectThing(lt.Thing): @lt.thing_setting def background_distributions(self) -> Optional[ChannelDistributions]: - """The statistics of the background image""" + """The statistics of the background image.""" bd = self._background_distributions if bd is None: return None @@ -56,7 +56,7 @@ class BackgroundDetectThing(lt.Thing): ) def background_mask(self, image: np.ndarray) -> np.ndarray: - """Calculate a binary image, showing whether each pixel is background + """Calculate a binary image, showing whether each pixel is background. The image should be in LUV format, the ouput will be binary with the same shape in the first two dimensions. @@ -78,7 +78,7 @@ class BackgroundDetectThing(lt.Thing): @lt.thing_action def background_fraction(self, cam: CamDep) -> float: - """Determine what fraction of the current image is background + """Determine what fraction of the current image is background. This action will acquire a new image from the preview stream, then evaluate whether it is foreground or background, by comparing it @@ -96,7 +96,7 @@ class BackgroundDetectThing(lt.Thing): @lt.thing_action def image_is_sample(self, cam: CamDep) -> bool: - """Label the current image as either background or sample""" + """Label the current image as either background or sample.""" b_fraction = self.background_fraction(cam) fraction_threshold = self.fraction @@ -104,7 +104,7 @@ class BackgroundDetectThing(lt.Thing): @lt.thing_action def set_background(self, cam: CamDep): - """Grab an image, and use its statistics to set the background + """Grab an image, and use its statistics to set the background. This should be run when the microscope is looking at an empty region, and will calculate the mean and standard deviation of the pixel values diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 805c6d47..25162ff8 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -1,4 +1,4 @@ -"""OpenFlexure Microscope Camera +"""OpenFlexure Microscope Camera. This module defines the interface for cameras. Any compatible lt.Thing should enabe the server to work. @@ -23,23 +23,23 @@ class JPEGBlob(lt.blob.Blob): class PNGBlob(lt.blob.Blob): - """A class representing a PNG image as a LabThings FastAPI Blob""" + """A class representing a PNG image as a LabThings FastAPI Blob.""" media_type: str = "image/png" class ArrayModel(RootModel): - """A model for an array""" + """A model for an array.""" root: NDArray class CaptureError(RuntimeError): - """An error trying to capture from a CameraThing""" + """An error trying to capture from a CameraThing.""" 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 in in memory when an method is called to use that image.""" class CameraMemoryBuffer: @@ -61,7 +61,7 @@ class CameraMemoryBuffer: def add_image( self, image: Any, metadata: Optional[dict] = None, buffer_max: int = 1 ) -> 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 be cleared. To allow saving multiple images the buffer_max must be set @@ -114,7 +114,7 @@ class CameraMemoryBuffer: ) from e def clear(self): - """Clear all images from memory""" + """Clear all images from memory.""" self._storage.clear() def _create_space(self, buffer_max: int) -> None: @@ -140,7 +140,7 @@ 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.""" mjpeg_stream = lt.outputs.MJPEGStreamDescriptor() lores_mjpeg_stream = lt.outputs.MJPEGStreamDescriptor() @@ -181,7 +181,7 @@ class BaseCamera(lt.Thing): @lt.thing_property def stream_active(self) -> bool: - """Whether the MJPEG stream is active""" + """Whether the MJPEG stream is active.""" raise NotImplementedError( "CameraThings must define their own stream_active method" ) @@ -203,7 +203,7 @@ class BaseCamera(lt.Thing): resolution: Literal["lores", "main", "full"] = "main", wait: Optional[float] = 5, ) -> JPEGBlob: - """Acquire one image from the camera and return as a JPEG blob""" + """Acquire one image from the camera and return as a JPEG blob.""" raise NotImplementedError( "CameraThings must define their own capture_jpeg method" ) @@ -214,7 +214,7 @@ class BaseCamera(lt.Thing): portal: lt.deps.BlockingPortal, stream_name: Literal["main", "lores"] = "main", ) -> JPEGBlob: - """Acquire one image from the preview stream and return as an array + """Acquire one image from the preview stream and return as an array. This differs from ``capture_jpeg`` in that it does not pause the MJPEG preview stream. Instead, we simply return the next frame from that @@ -233,7 +233,7 @@ class BaseCamera(lt.Thing): portal: lt.deps.BlockingPortal, stream_name: Literal["main", "lores"] = "main", ) -> int: - """Acquire one image from the preview stream and return its size""" + """Acquire one image from the preview stream and return its size.""" stream = ( self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream ) @@ -245,7 +245,7 @@ class BaseCamera(lt.Thing): stream_name: Literal["main", "lores", "raw"], wait: Optional[float], ) -> None: - """Capture a PIL image from stream stream_name with timeout wait""" + """Capture a PIL image from stream stream_name with timeout wait.""" raise NotImplementedError( "CameraThings must define their own capture_image method" ) @@ -258,7 +258,7 @@ class BaseCamera(lt.Thing): metadata_getter: lt.deps.GetThingStates, save_resolution: Optional[Tuple[int, int]] = None, ) -> None: - """Capture an image and save it to disk + """Capture an image and save it to disk. :param jpeg_path: The path to save the file to :param logger: This should be injected automatically by Labthings FastAPI @@ -288,7 +288,7 @@ class BaseCamera(lt.Thing): metadata_getter: lt.deps.GetThingStates, buffer_max: int = 1, ) -> 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 in memory. @@ -336,7 +336,7 @@ class BaseCamera(lt.Thing): @lt.thing_action def clear_buffers(self) -> None: - """Clear all images in memory""" + """Clear all images in memory.""" self._memory_buffer.clear() def _robust_image_capture( diff --git a/src/openflexure_microscope_server/things/camera/opencv.py b/src/openflexure_microscope_server/things/camera/opencv.py index cd9f4092..89bc132a 100644 --- a/src/openflexure_microscope_server/things/camera/opencv.py +++ b/src/openflexure_microscope_server/things/camera/opencv.py @@ -1,4 +1,4 @@ -"""OpenFlexure Microscope OpenCV Camera +"""OpenFlexure Microscope OpenCV Camera. This module defines a camera Thing that uses OpenCV's ``VideoCapture``. @@ -23,7 +23,7 @@ from . import BaseCamera, JPEGBlob class OpenCVCamera(BaseCamera): - """A Thing representing an OpenCV camera""" + """A Thing representing an OpenCV camera.""" def __init__(self, camera_index: int = 0): self.camera_index = camera_index @@ -45,7 +45,7 @@ class OpenCVCamera(BaseCamera): @lt.thing_property def stream_active(self) -> bool: - """Whether the MJPEG stream is active""" + """Whether the MJPEG stream is active.""" if self._capture_enabled and self._capture_thread: return self._capture_thread.is_alive() return False @@ -71,7 +71,7 @@ class OpenCVCamera(BaseCamera): self, resolution: Literal["main", "full"] = "full", ) -> NDArray: - """Acquire one image from the camera and return as an array + """Acquire one image from the camera and return as an array. This function will produce a nested list containing an uncompressed RGB image. It's likely to be highly inefficient - raw and/or uncompressed captures using @@ -91,7 +91,7 @@ class OpenCVCamera(BaseCamera): metadata_getter: lt.deps.GetThingStates, resolution: Literal["main", "full"] = "main", ) -> JPEGBlob: - """Acquire one image from the camera and return as a JPEG blob + """Acquire one image from the camera and return as a JPEG blob. This function will produce a JPEG image. """ diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index dfb293e5..d40c8cf4 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -40,10 +40,10 @@ from . import BaseCamera, JPEGBlob, ArrayModel class PicameraStreamOutput(Output): - """An Output class that sends frames to a stream""" + """An Output class that sends frames to a stream.""" def __init__(self, stream: lt.outputs.MJPEGStream, portal: lt.deps.BlockingPortal): - """Create an output that puts frames in an MJPEGStream + """Create an output that puts frames in an MJPEGStream. We need to pass the stream object, and also the blocking portal, because new frame notifications happen in the anyio event loop and frames are @@ -57,7 +57,7 @@ class PicameraStreamOutput(Output): def outputframe( self, frame, _keyframe=True, _timestamp=None, _packet=None, _audio=False ): - """Add a frame to the stream's ringbuffer""" + """Add a frame to the stream's ringbuffer.""" self.stream.add_frame(frame, self.portal) @@ -104,7 +104,7 @@ class LensShading(BaseModel): class StreamingPiCamera2(BaseCamera): - """A Thing that represents an OpenCV camera""" + """A Thing that represents an OpenCV camera.""" def __init__(self, camera_num: int = 0): self._setting_save_in_progress = False @@ -244,7 +244,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_property def sensor_modes(self) -> list[SensorMode]: - """All the available modes the current sensor supports""" + """All the available modes the current sensor supports.""" if not self._sensor_modes: with self._streaming_picamera() as cam: self._sensor_modes = cam.sensor_modes @@ -254,14 +254,14 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_property def sensor_mode(self) -> Optional[SensorModeSelector]: - """The intended sensor mode of the camera""" + """The intended sensor mode of the camera.""" if self._sensor_mode is None: return None return SensorModeSelector(**self._sensor_mode) @sensor_mode.setter def sensor_mode(self, new_mode: Optional[SensorModeSelector | dict]): - """Change the sensor mode used""" + """Change the sensor mode used.""" if new_mode is None: self._sensor_mode = None elif isinstance(new_mode, SensorModeSelector): @@ -277,7 +277,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_property def sensor_resolution(self) -> Optional[tuple[int, int]]: - """The native resolution of the camera's sensor""" + """The native resolution of the camera's sensor.""" with self._streaming_picamera() as cam: return cam.sensor_resolution @@ -324,7 +324,7 @@ class StreamingPiCamera2(BaseCamera): @property def streaming(self) -> bool: - """True if the camera is streaming""" + """True if the camera is streaming.""" return self._picamera is not None and self._picamera.started @contextmanager @@ -432,7 +432,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_action def stop_streaming(self, stop_web_stream: bool = True) -> None: - """Stop the MJPEG stream""" + """Stop the MJPEG stream.""" with self._streaming_picamera() as picam: try: picam.stop_recording() # This should also stop the extra lores encoder @@ -474,7 +474,7 @@ class StreamingPiCamera2(BaseCamera): stream_name: Literal["main", "lores", "raw", "full"] = "main", wait: Optional[float] = 0.9, ) -> ArrayModel: - """Acquire one image from the camera and return as an array + """Acquire one image from the camera and return as an array. This function will produce a nested list containing an uncompressed RGB image. It's likely to be highly inefficient - raw and/or uncompressed captures using @@ -496,7 +496,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_property def camera_configuration(self) -> Mapping: - """The "configuration" dictionary of the picamera2 object + """The "configuration" dictionary of the picamera2 object. The "configuration" sets the resolution and format of the camera's streams. Together with the "tuning" it determines how the sensor is configured and @@ -516,7 +516,7 @@ class StreamingPiCamera2(BaseCamera): resolution: Literal["lores", "main", "full"] = "main", wait: Optional[float] = 0.9, ) -> JPEGBlob: - """Acquire one image from the camera as a JPEG + """Acquire one image from the camera as a JPEG. The JPEG will be acquired using ``Picamera2.capture_file``. If the ``resolution`` parameter is ``main`` or ``lores``, it will be captured @@ -567,7 +567,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_property def capture_metadata(self) -> dict: - """Return the metadata from the camera""" + """Return the metadata from the camera.""" with self._streaming_picamera() as cam: return cam.capture_metadata() @@ -577,7 +577,7 @@ class StreamingPiCamera2(BaseCamera): target_white_level: int = 700, percentile: float = 99.9, ): - """Adjust exposure until a the target white level is reached + """Adjust exposure until a the target white level is reached. Starting from the minimum exposure, gradually increase exposure until the image reaches the specified white level. @@ -603,7 +603,7 @@ class StreamingPiCamera2(BaseCamera): method: Literal["percentile", "centre"] = "centre", luminance_power: float = 1.0, ): - """Correct the white balance of the image + """Correct the white balance of the image. This calibration requires a neutral image, such that the 99th centile of each colour channel should correspond to white. We calculate the @@ -711,7 +711,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_action def full_auto_calibrate(self) -> None: - """Perform a full auto-calibration + """Perform a full auto-calibration. This function will call the other calibration actions in sequence: @@ -729,7 +729,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_action def flat_lens_shading(self) -> None: - """Disable flat-field correction + """Disable flat-field correction. This method will set a completely flat lens shading table. It is not the same as the default behaviour, which is to use an adaptive lens shading @@ -748,7 +748,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_property def lens_shading_tables(self) -> Optional[LensShading]: - """The current lens shading (i.e. flat-field correction) + """The current lens shading (i.e. flat-field correction). Return the current lens shading correction, as three 2D lists each with dimensions 16x12, if a static lens shading table is in use. @@ -770,7 +770,7 @@ class StreamingPiCamera2(BaseCamera): return None def reshape_lst(lin: list[float]) -> list[list[float]]: - """Reshape the 192 element list into a 2D 16x12 list""" + """Reshape the 192 element list into a 2D 16x12 list.""" w, h = 16, 12 return [lin[w * i : w * (i + 1)] for i in range(h)] @@ -782,7 +782,7 @@ class StreamingPiCamera2(BaseCamera): @lens_shading_tables.setter def lens_shading_tables(self, lst: LensShading) -> None: - """Set the lens shading tables""" + """Set the lens shading tables.""" with self._streaming_picamera(pause_stream=True): recalibrate_utils.set_static_lst( self.tuning, @@ -795,7 +795,7 @@ class StreamingPiCamera2(BaseCamera): def correct_colour_gains_for_lens_shading( self, colour_gains: tuple[float, float] ) -> tuple[float, float]: - """Correct white balance gains for the effect of lens shading + """Correct white balance gains for the effect of lens shading. The white balance algorithm we use assumes the brightest pixels should be white, and that the only thing affecting the colour of @@ -825,7 +825,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_action def flat_lens_shading_chrominance(self) -> None: - """Disable flat-field correction + """Disable flat-field correction. This method will set the chrominance of the lens shading table to be flat, i.e. we'll correct vignetting of intensity, but not any change in @@ -840,7 +840,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_action def reset_lens_shading(self) -> None: - """Revert to default lens shading settings + """Revert to default lens shading settings. This method will restore the default "adaptive" lens shading method used by the Raspberry Pi camera. @@ -851,7 +851,7 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_property def lens_shading_is_static(self) -> bool: - """Whether the lens shading is static + """Whether the lens shading is static. This property is true if the lens shading correction has been set to use a static table (i.e. the number of automatic correction iterations is zero). diff --git a/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py b/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py index 33f5d4ab..ef9b9b82 100644 --- a/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py +++ b/src/openflexure_microscope_server/things/camera/picamera_recalibrate_utils.py @@ -1,4 +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 gain, exposure, and white balance of a Raspberry Pi camera, using @@ -52,7 +52,7 @@ LensShadingTables = tuple[np.ndarray, np.ndarray, np.ndarray] def load_default_tuning(cam: Picamera2) -> dict: - """Load the default tuning file for the camera + """Load the default tuning file for the camera. This will open and close the camera to determine its model. If you are using a model that's supported by ``picamera2`` it should have a tuning @@ -76,7 +76,7 @@ def load_default_tuning(cam: Picamera2) -> dict: def set_minimum_exposure(camera: Picamera2) -> None: - """Enable manual exposure, with low gain and shutter speed + """Enable manual exposure, with low gain and shutter speed. We set exposure mode to manual, analog and digital gain to 1, and shutter speed to the minimum (8us for Pi Camera v2) @@ -93,7 +93,7 @@ def set_minimum_exposure(camera: Picamera2) -> None: class ExposureTest(BaseModel): - """Record the results of testing the camera's current exposure settings""" + """Record the results of testing the camera's current exposure settings.""" level: int exposure_time: int @@ -101,7 +101,7 @@ class ExposureTest(BaseModel): def test_exposure_settings(camera: Picamera2, percentile: float) -> ExposureTest: - """Evaluate current exposure settings using a raw image + """Evaluate current exposure settings using a raw image. CAMERA SHOULD BE STARTED! @@ -136,7 +136,7 @@ def test_exposure_settings(camera: Picamera2, percentile: float) -> ExposureTest def check_convergence(test: ExposureTest, target: int, tolerance: float) -> bool: - """Check whether the brightness is within the specified target range""" + """Check whether the brightness is within the specified target range.""" return abs(test.level - target) < target * tolerance @@ -321,7 +321,7 @@ def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray: def get_16x12_grid(chan: np.ndarray, dx: int, dy: int) -> np.ndarray: - """Compresses channel down to a 16x12 grid - from libcamera + """Compresses channel down to a 16x12 grid - from libcamera. This is taken from https://git.linuxtv.org/libcamera.git/tree/utils/raspberrypi/ctt/ctt_alsc.py @@ -343,7 +343,7 @@ def get_16x12_grid(chan: np.ndarray, dx: int, dy: int) -> np.ndarray: def upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray: - """Zoom an image in the last two dimensions + """Zoom an image in the last two dimensions. This is effectively the inverse operation of ``get_16x12_grid`` """ @@ -354,7 +354,7 @@ def upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray: def downsampled_channels(channels: np.ndarray, blacklevel=64) -> list[np.ndarray]: - """Generate a downsampled, un-normalised image from which to calculate the LST + """Generate a downsampled, un-normalised image from which to calculate the LST. TODO: blacklevel probably ought to be determined from the camera... """ @@ -382,7 +382,7 @@ def lst_from_channels(channels: np.ndarray) -> LensShadingTables: def lst_from_grids(grids: np.ndarray) -> LensShadingTables: - """Given 4 downsampled grids, generate the luminance and chrominance tables + """Given 4 downsampled grids, generate the luminance and chrominance tables. The grids are the 4 BAYER channels RGGB @@ -408,7 +408,7 @@ def lst_from_grids(grids: np.ndarray) -> LensShadingTables: def grids_from_lst(lum: np.ndarray, Cr: np.ndarray, Cb: np.ndarray) -> np.ndarray: - """Convert form luminance/chrominance dict to four RGGB channels + """Convert form luminance/chrominance dict to four RGGB channels. Note that these will be normalised - the maximum green value is always 1. Also, note that the channels are BGGR, to be consistent with the @@ -462,13 +462,13 @@ def set_static_ccm( def get_static_ccm(tuning: dict) -> None: - """Get the ``rpi.ccm`` section of a camera tuning dict""" + """Get the ``rpi.ccm`` section of a camera tuning dict.""" ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm") return ccm["ccms"] def lst_is_static(tuning: dict) -> bool: - """Whether the lens shading table is set to static""" + """Whether the lens shading table is set to static.""" alsc = Picamera2.find_tuning_algo(tuning, "rpi.alsc") return alsc["n_iter"] == 0 @@ -493,13 +493,13 @@ def set_static_geq( def _geq_is_static(tuning: dict) -> bool: - """Whether the green equalisation is set to static""" + """Whether the green equalisation is set to static.""" geq = Picamera2.find_tuning_algo(tuning, "rpi.geq") return geq["offset"] == 65535 def index_of_algorithm(algorithms: list[dict], algorithm: str) -> int: - """Find the index of an algorithm's section in the tuning file""" + """Find the index of an algorithm's section in the tuning file.""" for i, a in enumerate(algorithms): if algorithm in a: return i @@ -557,5 +557,5 @@ def recreate_camera_manager() -> None: def as_flat_rounded_list(array: np.ndarray, round_to: int = 3) -> list[float]: - """Flatten array, round, and then convert to list""" + """Flatten array, round, and then convert to list.""" return np.reshape(array, -1).round(round_to).tolist() diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index e27f365a..04423755 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -1,4 +1,4 @@ -"""OpenFlexure Microscope OpenCV Camera +"""OpenFlexure Microscope OpenCV Camera. This module defines a Thing that is responsible for using the stage and camera together to perform an autofocus routine. @@ -30,7 +30,7 @@ RATIO = 0.2 class SimulatedCamera(BaseCamera): - """A Thing representing an OpenCV camera""" + """A Thing representing an OpenCV camera.""" _stage: Optional[BaseStage] = None _server: Optional[lt.ThingServer] = None @@ -53,7 +53,7 @@ class SimulatedCamera(BaseCamera): self.generate_canvas() def generate_sprites(self): - """Generate sprites to populate the image""" + """Generate sprites to populate the image.""" self.sprites = [] black = np.zeros(self.glyph_shape, dtype=np.uint8) x = np.arange(black.shape[0]) @@ -65,7 +65,7 @@ class SimulatedCamera(BaseCamera): self.sprites.append(sprite) def generate_blobs(self, n_blobs: int = 1000): - """Generate coordinates of blobs + """Generate coordinates of blobs. Blobs are characterised by X, Y, sprite We also generate a KD tree to rapidly find blobs in an image @@ -78,7 +78,7 @@ class SimulatedCamera(BaseCamera): self.blobs[:, 2] = rng.choice(len(self.sprites), n_blobs) def generate_canvas(self): - """Generate a blank canvas""" + """Generate a blank canvas.""" self.canvas = np.zeros(self.canvas_shape, dtype=np.uint8) self.canvas[...] = 255 w, h, _ = self.glyph_shape @@ -89,7 +89,7 @@ class SimulatedCamera(BaseCamera): ] -= self.sprites[int(sprite)] def generate_image(self, pos: tuple[int, int, int]): - """Generate an image with blobs based on supplied coordinates""" + """Generate an image with blobs based on supplied coordinates.""" canvas_width, canvas_height, _ = self.canvas_shape image_width, image_height, _ = self.shape pos = tuple(x * RATIO for x in pos) @@ -124,7 +124,7 @@ class SimulatedCamera(BaseCamera): return self._stage.instantaneous_position def generate_frame(self): - """Generate a frame with blobs based on the stage coordinates""" + """Generate a frame with blobs based on the stage coordinates.""" try: pos = self.get_stage_position() except Exception as e: @@ -145,7 +145,7 @@ class SimulatedCamera(BaseCamera): @lt.thing_property def stream_active(self) -> bool: - """Whether the MJPEG stream is active""" + """Whether the MJPEG stream is active.""" if self._capture_enabled and self._capture_thread: return self._capture_thread.is_alive() return False @@ -170,7 +170,7 @@ class SimulatedCamera(BaseCamera): self, resolution: Literal["main", "full"] = "full", ) -> ArrayModel: - """Acquire one image from the camera and return as an array + """Acquire one image from the camera and return as an array. This function will produce a nested list containing an uncompressed RGB image. It's likely to be highly inefficient - raw and/or uncompressed captures using @@ -185,7 +185,7 @@ class SimulatedCamera(BaseCamera): metadata_getter: lt.deps.GetThingStates, resolution: Literal["main", "full"] = "main", ) -> JPEGBlob: - """Acquire one image from the camera and return as a JPEG blob + """Acquire one image from the camera and return as a JPEG blob. This function will produce a JPEG image. """ diff --git a/src/openflexure_microscope_server/things/camera_stage_mapping.py b/src/openflexure_microscope_server/things/camera_stage_mapping.py index 59efe2e3..7126fd98 100644 --- a/src/openflexure_microscope_server/things/camera_stage_mapping.py +++ b/src/openflexure_microscope_server/things/camera_stage_mapping.py @@ -1,4 +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 includes calibration functions that measure the relationship between @@ -53,7 +53,7 @@ class HardwareInterfaceModel(BaseModel): def downsample(factor: int, image: np.ndarray) -> np.ndarray: - """Downsample an image by taking the mean of each nxn region + """Downsample an image by taking the mean of each nxn region. This should be very efficient: we calculate the mean of each ``factor * factor`` square, no interpolation. If the image is @@ -79,7 +79,7 @@ DEFAULT_SETTLING_TIME = 0.2 def make_hardware_interface( stage: Stage, camera: Camera, downsample_factor: int = 2 ) -> HardwareInterfaceModel: - """Construct the functions we need to interface with the hardware""" + """Construct the functions we need to interface with the hardware.""" axes = stage.axis_names def pos2dict(pos: Sequence[float]) -> Mapping[str, float]: @@ -144,7 +144,7 @@ class LoggingMoveWrapper: self.clear_history() def __call__(self, new_position: CoordinateType, *args, **kwargs): - """Move to a new position, and record it""" + """Move to a new position, and record it.""" self._history.append((time.time(), self._current_position)) self._move_function(new_position, *args, **kwargs) self._current_position = new_position @@ -152,13 +152,13 @@ class LoggingMoveWrapper: @property def history(self) -> MoveHistory: - """The history, as a numpy array of times and another of positions""" + """The history, as a numpy array of times and another of positions.""" times: List[float] = [t for t, p in self._history if p is not None] positions: List[CoordinateType] = [p for t, p in self._history if p is not None] return MoveHistory(times, positions) def clear_history(self): - """Reset our history to be an empty list""" + """Reset our history to be an empty list.""" self._history: List[Tuple[float, Optional[CoordinateType]]] = [] @@ -175,7 +175,7 @@ class CSMUncalibratedError(HTTPException): class CameraStageMapper(lt.Thing): - """A Thing to manage mapping between image and stage coordinates""" + """A Thing to manage mapping between image and stage coordinates.""" @lt.thing_action def calibrate_1d( @@ -185,7 +185,7 @@ class CameraStageMapper(lt.Thing): logger: lt.deps.InvocationLogger, direction: Tuple[float, float, float], ) -> DenumpifyingDict: - """Move a microscope's stage in 1D, and figure out the relationship with the camera""" + """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 @@ -214,7 +214,7 @@ class CameraStageMapper(lt.Thing): def calibrate_xy( self, hw: HardwareInterfaceDep, stage: Stage, logger: lt.deps.InvocationLogger ) -> DenumpifyingDict: - """Move the microscope's stage in X and Y, to calibrate its relationship to the camera + """Move the microscope's stage in X and Y, to calibrate its relationship to the camera. This performs two 1d calibrations in x and y, then combines their results. """ @@ -291,13 +291,13 @@ class CameraStageMapper(lt.Thing): @lt.thing_property def image_resolution(self) -> Optional[Tuple[float, float]]: - """The image size used to calibrate the image_to_stage_displacement_matrix""" + """The image size used to calibrate the image_to_stage_displacement_matrix.""" if self.last_calibration is None: return None return self.last_calibration["image_resolution"] def assert_calibrated(self): - """Raise an exception if the image_to_stage_displacement matrix is not set""" + """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 # added by CSMUncalibratedError @@ -310,7 +310,7 @@ class CameraStageMapper(lt.Thing): x: float, y: float, ): - """Move by a given number of pixels on the camera + """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 vertical axes of the image. In many toolkits, "matrix indices" are used, which @@ -329,7 +329,7 @@ class CameraStageMapper(lt.Thing): @lt.thing_property def thing_state(self) -> dict[str, Any]: - """Summary metadata describing the current state of the Thing""" + """Summary metadata describing the current state of the Thing.""" return { k: getattr(self, k) for k in ["image_to_stage_displacement_matrix", "image_resolution"] diff --git a/src/openflexure_microscope_server/things/settings_manager.py b/src/openflexure_microscope_server/things/settings_manager.py index 2882e7e1..63373988 100644 --- a/src/openflexure_microscope_server/things/settings_manager.py +++ b/src/openflexure_microscope_server/things/settings_manager.py @@ -1,4 +1,4 @@ -"""OpenFlexure Settings Management +"""OpenFlexure Settings Management. This module provides some settings management across the other Things, and for code that currently lives in clients but needs to persist settings on @@ -21,7 +21,7 @@ ThingServerDep = Annotated[lt.ThingServer, Depends(thing_server_from_request)] def recursive_update(old_dict: MutableMapping, update: Mapping): - """Update a dictionary recursively""" + """Update a dictionary recursively.""" for k, v in update.items(): if isinstance(v, Mapping): if k in old_dict and isinstance(old_dict[k], MutableMapping): @@ -33,7 +33,7 @@ def recursive_update(old_dict: MutableMapping, update: Mapping): def nested_dict_get(data: dict, key: Sequence[str], create=False) -> Any: - """Index a dict-of-dicts with a sequence of strings""" + """Index a dict-of-dicts with a sequence of strings.""" subdict = data for k in key: if k not in subdict and create: @@ -100,7 +100,7 @@ class SettingsManager(lt.Thing): @lt.thing_setting def microscope_id(self) -> UUID: - """A unique identifier for this microscope""" + """A unique identifier for this microscope.""" if self._microscope_id is None: self._microscope_id = str(uuid4()) return UUID(self._microscope_id) @@ -117,7 +117,7 @@ class SettingsManager(lt.Thing): @lt.thing_action def get_things_state(self, metadata_getter: lt.deps.GetThingStates) -> Mapping: - """Metadata summarising the current state of all Things in the server""" + """Metadata summarising the current state of all Things in the server.""" return metadata_getter() @property diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 0c5cfeb8..d2d7b695 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -42,7 +42,7 @@ STITCHING_RESOLUTION = (820, 616) class ScanNotRunningError(RuntimeError): - """Exception called when scan not running that requires a scan to be running""" + """Exception called when scan not running that requires a scan to be running.""" def _scan_running(method): @@ -170,7 +170,7 @@ class SmartScanThing(lt.Thing): @_scan_running def _check_background_and_csm_set(self): - """Before starting a scan, check that background and camera-stage-mapping are set + """Before starting a scan, check that background and camera-stage-mapping are set. Raise error if: - background is to be skipped but is not set @@ -229,7 +229,7 @@ class SmartScanThing(lt.Thing): @_scan_running 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 that each image should overlap its nearest neighbour by 50%. @@ -520,7 +520,7 @@ class SmartScanThing(lt.Thing): @_scan_running def _return_to_starting_position(self): - """Return to the initial scan position, if set""" + """Return to the initial scan position, if set.""" self._scan_logger.info("Returning to starting position.") if self._starting_position is not None: self._stage.move_absolute( @@ -529,7 +529,7 @@ class SmartScanThing(lt.Thing): @_scan_running def _perform_final_stitch(self): - """Update the scan zip and perform final stitch of the data""" + """Update the scan zip and perform final stitch of the data.""" if self._scan_images_taken <= 3: self._scan_logger.info("Not performing a stitch as 3 or fewer images taken") return @@ -626,7 +626,7 @@ class SmartScanThing(lt.Thing): @lt.thing_property def scans(self) -> list[scan_directories.ScanInfo]: - """All the available scans + """All the available scans. Each scan has a name (which can be used to access it), along with its modified and created times (according to the filesystem) and @@ -687,7 +687,7 @@ class SmartScanThing(lt.Thing): "scans", ) def delete_all_scans(self, logger: lt.deps.InvocationLogger) -> None: - """Delete all the scans on the microscope + """Delete all the scans on the microscope. **This will irreversibly remove all scanned data from the microscope!** @@ -698,7 +698,7 @@ class SmartScanThing(lt.Thing): @lt.thing_action 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 for scan_info in self._scan_dir_manager.all_scans_info(): if scan_info.number_of_images == 0: @@ -722,7 +722,7 @@ class SmartScanThing(lt.Thing): @property def latest_preview_stitch_path(self) -> Optional[str]: - """The path of the latest preview stitched image, or None if not available""" + """The path of the latest preview stitched image, or None if not available.""" if not self.latest_scan_name: return None @@ -732,7 +732,7 @@ class SmartScanThing(lt.Thing): @lt.thing_property def latest_preview_stitch_time(self) -> Optional[float]: - """The modification time of the latest preview image, to allow live updating + """The modification time of the latest preview image, to allow live updating. This will return None (``null`` to JS) if there is no preview image to return. @@ -795,7 +795,7 @@ class SmartScanThing(lt.Thing): @_scan_running def _preview_stitch_running(self) -> bool: - """Whether there is a preview stitch running in a subprocess""" + """Whether there is a preview stitch running in a subprocess.""" with self._preview_stitch_popen_lock: if self._preview_stitch_popen is None: return False @@ -805,7 +805,7 @@ class SmartScanThing(lt.Thing): @_scan_running 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(): with self._preview_stitch_popen_lock: self._preview_stitch_popen.wait() @@ -816,7 +816,7 @@ class SmartScanThing(lt.Thing): cancel: lt.deps.CancelHook, cmd: list[str], ) -> CompletedProcess: - """Run a subprocess and log any output + """Run a subprocess and log any output. Raises: ChildProcessError if exit code is not zero @@ -870,7 +870,7 @@ class SmartScanThing(lt.Thing): stitch_resize: Optional[float] = None, overlap: float = 0.0, ) -> None: - """Generate a stitched image based on stage position metadata + """Generate a stitched image based on stage position metadata. Note that as this is a lt.thing_action it needs the logger passed as a variable if called from another thing action diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index 1a84aada..809b50e1 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -5,7 +5,7 @@ import labthings_fastapi as lt class BaseStage(lt.Thing): - """A base stage class for OpenFlexure translation stages + """A base stage class for OpenFlexure translation stages. This can't be used directly but should reduce boilerplate code when implementing new stages. A minimal working stage must implement @@ -38,7 +38,7 @@ class BaseStage(lt.Thing): @property def thing_state(self): - """Summary metadata describing the current state of the stage""" + """Summary metadata describing the current state of the stage.""" return {"position": self.position} @lt.thing_action @@ -67,7 +67,7 @@ class BaseStage(lt.Thing): @lt.thing_action def set_zero_position(self): - """Make the current position zero in all axes + """Make the current position zero in all axes. This action does not move the stage, but resets the position to zero. It is intended for use after manually or automatically recentring the diff --git a/src/openflexure_microscope_server/things/stage/dummy.py b/src/openflexure_microscope_server/things/stage/dummy.py index cd3c6387..499eb761 100644 --- a/src/openflexure_microscope_server/things/stage/dummy.py +++ b/src/openflexure_microscope_server/things/stage/dummy.py @@ -9,7 +9,7 @@ from . import BaseStage class DummyStage(BaseStage): - """A dummy stage for testing purposes + """A dummy stage for testing purposes. This stage should work similarly to a Sangaboard stage, but without any hardware attached. @@ -83,7 +83,7 @@ class DummyStage(BaseStage): @lt.thing_action def set_zero_position(self): - """Make the current position zero in all axes + """Make the current position zero in all axes. This action does not move the stage, but resets the position to zero. It is intended for use after manually or automatically recentring the diff --git a/src/openflexure_microscope_server/things/stage/sangaboard.py b/src/openflexure_microscope_server/things/stage/sangaboard.py index 9b5542f0..641990f9 100644 --- a/src/openflexure_microscope_server/things/stage/sangaboard.py +++ b/src/openflexure_microscope_server/things/stage/sangaboard.py @@ -13,7 +13,7 @@ from . import BaseStage class SangaboardThing(BaseStage): - """A Thing to manage a Sangaboard motor controller + """A Thing to manage a Sangaboard motor controller. Internally, this uses the ``pysangaboard`` package from PyPi. This imports as ``sangaboard``. As ``pysangaboard`` does not support some features added @@ -115,7 +115,7 @@ class SangaboardThing(BaseStage): @lt.thing_action def set_zero_position(self) -> None: - """Make the current position zero in all axes + """Make the current position zero in all axes. This action does not move the stage, but resets the position to zero. It is intended for use after manually or automatically recentring the @@ -132,7 +132,7 @@ class SangaboardThing(BaseStage): dt: float = 0.5, led_channel: Literal["cc"] = "cc", ) -> None: - """Flash the LED to identify the board + """Flash the LED to identify the board. This is intended to be useful in situations where there are multiple Sangaboards in use, and it is necessary to identify which one is diff --git a/src/openflexure_microscope_server/things/system_control.py b/src/openflexure_microscope_server/things/system_control.py index 88287a52..ca41c22c 100644 --- a/src/openflexure_microscope_server/things/system_control.py +++ b/src/openflexure_microscope_server/things/system_control.py @@ -1,4 +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. """ @@ -15,11 +15,11 @@ class CommandOutput(BaseModel): class SystemControlThing(lt.Thing): - """Attempt to shutdown the device""" + """Attempt to shutdown the device.""" @lt.thing_action def shutdown(self) -> CommandOutput: - """Attempt to shutdown the device""" + """Attempt to shutdown the device.""" p = subprocess.Popen( ["sudo", "shutdown", "-h", "now"], stderr=subprocess.PIPE, @@ -36,7 +36,7 @@ class SystemControlThing(lt.Thing): @lt.thing_action def reboot(self) -> CommandOutput: - """Attempt to reboot the device""" + """Attempt to reboot the device.""" p = subprocess.Popen( ["sudo", "shutdown", "-r", "now"], stderr=subprocess.PIPE, diff --git a/src/openflexure_microscope_server/utilities.py b/src/openflexure_microscope_server/utilities.py index 82175431..aceb7eac 100644 --- a/src/openflexure_microscope_server/utilities.py +++ b/src/openflexure_microscope_server/utilities.py @@ -17,7 +17,7 @@ class ErrorCapturingThread(Thread): """ 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: if args is None: args = () diff --git a/tests/mock_things/mock_autofocus.py b/tests/mock_things/mock_autofocus.py index 8ce619fa..a6c2bd58 100644 --- a/tests/mock_things/mock_autofocus.py +++ b/tests/mock_things/mock_autofocus.py @@ -13,5 +13,5 @@ class MockAutoFocusThing: mock_call_count = {"looping_autofocus": 0} def looping_autofocus(self, dz: int = 2000, start: str = "centre") -> None: # noqa: ARG002 - """Mock autofocus with no return""" + """Mock autofocus with no return.""" self.mock_call_count["looping_autofocus"] += 1 diff --git a/tests/test_camera_buffer.py b/tests/test_camera_buffer.py index 65f16eef..c62252c9 100644 --- a/tests/test_camera_buffer.py +++ b/tests/test_camera_buffer.py @@ -16,7 +16,7 @@ RANDOM_GENERATOR = np.random.default_rng() def random_image(): - """Create a random image""" + """Create a random image.""" imarray = RANDOM_GENERATOR.integers( low=0, high=255, size=(100, 100, 3), dtype="uint8" ) @@ -24,14 +24,14 @@ def random_image(): def random_metadata(): - """Create a misc dictionary to pretend to be metadata""" + """Create a misc dictionary to pretend to be metadata.""" # Not very metadata like, but we are just checking that the same dict it # is returned return {"a": randint(1, 100), "b": randint(1, 100)} def test_add_and_get_image(): - """Check images can be captured and retrieved""" + """Check images can be captured and retrieved.""" mem_buf = CameraMemoryBuffer() misc_image = random_image() buffer_id = mem_buf.add_image(misc_image) @@ -44,7 +44,7 @@ def test_add_and_get_image(): def test_add_and_get_image_twice(): - """Check images can be retrieved twice if remove flag set false""" + """Check images can be retrieved twice if remove flag set false.""" mem_buf = CameraMemoryBuffer() misc_image = random_image() buffer_id = mem_buf.add_image(misc_image) @@ -60,7 +60,7 @@ def test_add_and_get_image_twice(): def test_get_without_id(): - """Check images can be captured and retrieved without ID""" + """Check images can be captured and retrieved without ID.""" mem_buf = CameraMemoryBuffer() misc_image = random_image() mem_buf.add_image(misc_image) @@ -92,7 +92,7 @@ def test_get_two_images(): def test_get_two_images_without_setting_buffer_size(): - """Check two images can't be retrieved if the buffer size isn't set""" + """Check two images can't be retrieved if the buffer size isn't set.""" mem_buf = CameraMemoryBuffer() misc_image1 = random_image() misc_image2 = random_image() @@ -106,7 +106,7 @@ def test_get_two_images_without_setting_buffer_size(): def test_buffer_size_changing(): - """Check buffer size resets back to 1 when if not set""" + """Check buffer size resets back to 1 when if not set.""" mem_buf = CameraMemoryBuffer() misc_image1 = random_image() misc_image2 = random_image() @@ -126,7 +126,7 @@ def test_buffer_size_changing(): def test_capture_two_images_get_without_id(): - """Check that all images are deleted when getting without id""" + """Check that all images are deleted when getting without id.""" mem_buf = CameraMemoryBuffer() misc_image1 = random_image() misc_image2 = random_image() @@ -142,7 +142,7 @@ def test_capture_two_images_get_without_id(): def test_buffer_size_respected(): - """Capture 10 images with a buffer size of 5. Check only last 5 exist""" + """Capture 10 images with a buffer size of 5. Check only last 5 exist.""" mem_buf = CameraMemoryBuffer() images = [] @@ -163,7 +163,7 @@ def test_buffer_size_respected(): def test_clear_buffer(): - """Capture 10 images clear the buffer and check they are gone""" + """Capture 10 images clear the buffer and check they are gone.""" mem_buf = CameraMemoryBuffer() images = [] @@ -184,7 +184,7 @@ def test_clear_buffer(): def test_get_metadata_too(): - """Capture 10 images with metadata and check metadata is returned as expected""" + """Capture 10 images with metadata and check metadata is returned as expected.""" mem_buf = CameraMemoryBuffer() images = [] diff --git a/tests/test_scan_directories.py b/tests/test_scan_directories.py index 27d613b5..b66180bf 100644 --- a/tests/test_scan_directories.py +++ b/tests/test_scan_directories.py @@ -28,13 +28,13 @@ BASE_SCAN_DIR = os.path.join(tempfile.gettempdir(), "scans") def _clear_scan_dir() -> None: - """Delete the scan dir""" + """Delete the scan dir.""" if os.path.exists(BASE_SCAN_DIR): shutil.rmtree(BASE_SCAN_DIR) def _add_fake_image(scan_dir: ScanDirectory) -> None: - """Make a fake image on disk in the scan directory""" + """Make a fake image on disk in the scan directory.""" unique = False while not unique: x_pos = random.randint(-10000, 10000) @@ -63,7 +63,7 @@ def _add_fake_file( def _make_fake_dzi(scan_dir: ScanDirectory, n_layers: int = 8) -> None: - """Create a fake DZI in a scan + """Create a fake DZI in a scan. :param n_layers: The number of layers of tiles. I.e. tile directories numbered 0...(n_layers-1) will be created. Default 8 @@ -96,7 +96,7 @@ def _make_fake_dzi(scan_dir: ScanDirectory, n_layers: int = 8) -> None: def test_basic_directory_operations(): - """Test some basic operations + """Test some basic operations. Test some basic operations, including: - ScanDirectoryManager creates a scan directory @@ -156,7 +156,7 @@ def test_basic_directory_operations(): 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() scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) # Make 4 scans @@ -183,7 +183,7 @@ def test_scan_sequence_and_listing(): def test_scan_name_non_sequential(): - """Check new scan has the correct name if the directories are not sequential""" + """Check new scan has the correct name if the directories are not sequential.""" _clear_scan_dir() os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_0001")) os.makedirs(os.path.join(BASE_SCAN_DIR, "fake_scan_0002")) @@ -217,7 +217,7 @@ def test_all_scan_names_taken(): 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() scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) scan_dir = scan_dir_manager.new_scan_dir("") @@ -225,7 +225,7 @@ def test_no_scan_names_given(): def test_scan_info(): - """Test the scan info is correct even using fake scan data""" + """Test the scan info is correct even using fake scan data.""" _clear_scan_dir() scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) scan_dir = scan_dir_manager.new_scan_dir("fake_scan") @@ -252,7 +252,7 @@ def test_scan_info(): def test_get_final_stitch(): - """Check that the final stitch can be retrieved""" + """Check that the final stitch can be retrieved.""" _clear_scan_dir() scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) scan_dir = scan_dir_manager.new_scan_dir("fake_scan") @@ -280,7 +280,7 @@ def test_get_final_stitch(): def test_empty_scan_info(): - """Test the scan info is correct even if the scan is empty""" + """Test the scan info is correct even if the scan is empty.""" _clear_scan_dir() scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) scan_dir = scan_dir_manager.new_scan_dir("fake_scan") @@ -298,7 +298,7 @@ def test_empty_scan_info(): def test_zipping_scan_data(): - """Test zipping the scan images with fake image data""" + """Test zipping the scan images with fake image data.""" # Run twice, once calling the ScanDirectory directly, # Once calling the ScanDirectoryManager for caller in ["scan_dir", "manager"]: @@ -338,7 +338,7 @@ def test_zipping_scan_data(): def test_all_files(): - """Test all_files returns the path, and respects skipped directories""" + """Test all_files returns the path, and respects skipped directories.""" _clear_scan_dir() scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) scan_dir = scan_dir_manager.new_scan_dir("fake_scan") @@ -393,7 +393,7 @@ def test_none_returned_for_missing_images_dir(): def test_extracting_files(): - """Test the private _find_files method of ScanDirectories + """Test the private _find_files method of ScanDirectories. Add files to directory and check expected returns. """ @@ -445,7 +445,7 @@ DiskUsage = namedtuple("DiskUsage", ["total", "used", "free"]) @pytest.mark.parametrize("free_space", [500_000_001, 700_000_000, 5_000_000_000]) def test_disk_not_full(mocker, free_space): - """Check no error thrown if disk has over 500MB of space""" + """Check no error thrown if disk has over 500MB of space.""" total_space = 16_000_000_000 # Mock the disk_usage mocker.patch( @@ -461,7 +461,7 @@ def test_disk_not_full(mocker, free_space): @pytest.mark.parametrize("free_space", [100_000_000, 499_999_999]) def test_disk_full(mocker, free_space): - """Check error thrown if disk has under 500MB of space""" + """Check error thrown if disk has under 500MB of space.""" total_space = 16_000_000_000 # Mock the disk_usage mocker.patch( diff --git a/tests/test_smart_scan.py b/tests/test_smart_scan.py index 9ff2d0cb..fb14a17a 100644 --- a/tests/test_smart_scan.py +++ b/tests/test_smart_scan.py @@ -41,14 +41,14 @@ SCAN_DIR = os.path.join(tempfile.gettempdir(), "scans") def _clear_scan_dir() -> None: - """Delete the scan dir""" + """Delete the scan dir.""" if os.path.exists(SCAN_DIR): shutil.rmtree(SCAN_DIR) @pytest.fixture def smart_scan_thing(): - """Return a smart scan thing as a fixture""" + """Return a smart scan thing as a fixture.""" return SmartScanThing(SCAN_DIR) @@ -75,7 +75,7 @@ def test_inaccessible_scan_methods(smart_scan_thing): def test_private_delete_scan(smart_scan_thing, caplog): - """Test the private _delete_scan method deletes directories or warns if it can't""" + """Test the private _delete_scan method deletes directories or warns if it can't.""" _clear_scan_dir() with caplog.at_level(logging.INFO): fake_scan_name = "fake_scan_0001" @@ -101,7 +101,7 @@ def test_private_delete_scan(smart_scan_thing, caplog): def test_public_delete_scan(smart_scan_thing, caplog): - """Test the delete_scan API call deletes directories or warns if it can't""" + """Test the delete_scan API call deletes directories or warns if it can't.""" _clear_scan_dir() with caplog.at_level(logging.INFO): fake_scan_name = "fake_scan_0001" @@ -152,7 +152,7 @@ def test_delete_all_scans(smart_scan_thing, caplog): 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 _run_scan method where this can be tested. Once this is done diff --git a/tests/test_stack.py b/tests/test_stack.py index 7420fc17..06e18686 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -90,7 +90,7 @@ def test_stack_params_not_enough_test_images(save_ims, extra_ims): extra_ims=even_integers(min_value=0, max_value=10), ) def test_stack_params_negative_images_to_save(save_ims, extra_ims): - """save_ims is negative so images_to_save is negative, failing validation + """save_ims is negative so images_to_save is negative, failing validation. For arguments see test_stack_params_validation """ @@ -110,7 +110,7 @@ def test_stack_params_negative_images_to_save(save_ims, extra_ims): extra_ims=odd_integers(min_value=0, max_value=10), ) def test_even_min_images_to_test(save_ims, extra_ims): - """extra_ims is odd so min_images_to_test is even, failing validation + """extra_ims is odd so min_images_to_test is even, failing validation. For arguments see test_stack_params_validation """ @@ -130,7 +130,7 @@ def test_even_min_images_to_test(save_ims, extra_ims): extra_ims=odd_integers(min_value=0, max_value=10), ) def test_even_images_to_save(save_ims, extra_ims): - """save_ims is even so images_to_save is even, failing validation + """save_ims is even so images_to_save is even, failing validation. For arguments see test_stack_params_validation """ @@ -146,7 +146,7 @@ def test_even_images_to_save(save_ims, extra_ims): 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. """ @@ -187,7 +187,7 @@ def test_computed_stack_params(): 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 """ @@ -204,14 +204,14 @@ def random_capture(set_id: Optional[int] = None): 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): assert IMAGE_REGEX.search(random_capture().filename) @given(st.integers(min_value=0, max_value=5000)) 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)] for i, capture in enumerate(captures): diff --git a/tests/utilities/__init__.py b/tests/utilities/__init__.py index 3012d375..a261de29 100644 --- a/tests/utilities/__init__.py +++ b/tests/utilities/__init__.py @@ -9,7 +9,7 @@ from collections.abc import Hashable class SizedIterableHashable(Iterable[Hashable], Protocol): - """A protocol for sized iterable of hashable objects""" + """A protocol for sized iterable of hashable objects.""" def __len__(self) -> int: """Add a len function to protocol so Python knows the object is sized.""" diff --git a/tests/utilities/scan_test_helpers.py b/tests/utilities/scan_test_helpers.py index 9bb40423..7052c579 100644 --- a/tests/utilities/scan_test_helpers.py +++ b/tests/utilities/scan_test_helpers.py @@ -44,14 +44,14 @@ class FakeSample: @property 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.set(color=(1.0, 0.8, 1.0, 1.0)) return patch 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)) ax.add_artist(sample.patch) xh, yh = zip(*planner._path_history) @@ -184,7 +184,7 @@ def get_expected_result_for_example_smart_spiral( def load_sample_points(sample_name: str): - """Return the points to generate the FakeSample corresponding to the given input name + """Return the points to generate the FakeSample corresponding to the given input name. Options are "lobed", "regular", and "core". """