diff --git a/hardware-specific-tests/picamera2/test_tuning.py b/hardware-specific-tests/picamera2/test_tuning.py index bed9e2fd..35a67676 100644 --- a/hardware-specific-tests/picamera2/test_tuning.py +++ b/hardware-specific-tests/picamera2/test_tuning.py @@ -95,19 +95,35 @@ def _test_bad_tuning_after_good_tuning(configure: bool = False): @pytest.mark.filterwarnings("ignore: Exception ignored") def test_bad_tuning_after_good_tuning_noconfigure(): - _test_bad_tuning_after_good_tuning(False) + """First run setting good, then bad, then good tuning. + + create_preview_configuration() is NOT run on initial setup. + """ + _test_bad_tuning_after_good_tuning(configure=False) @pytest.mark.filterwarnings("ignore: Exception ignored") def test_bad_tuning_after_good_tuning_configure(): - _test_bad_tuning_after_good_tuning(True) + """Second run setting good, then bad, then good tuning. + + create_preview_configuration() is run on initial setup this time. + """ + _test_bad_tuning_after_good_tuning(configure=True) @pytest.mark.filterwarnings("ignore: Exception ignored") def test_bad_tuning_after_good_tuning_noconfigure2(): - _test_bad_tuning_after_good_tuning(False) + """3rd run setting good, then bad, then good tuning. + + create_preview_configuration() is NOT run on initial setup. + """ + _test_bad_tuning_after_good_tuning(configure=False) @pytest.mark.filterwarnings("ignore: Exception ignored") def test_bad_tuning_after_good_tuning_configure2(): - _test_bad_tuning_after_good_tuning(True) + """Final run setting good, then bad, then good tuning. + + create_preview_configuration() is run on initial setup again. + """ + _test_bad_tuning_after_good_tuning(configure=True) diff --git a/pyproject.toml b/pyproject.toml index 1c04d0f7..68b6e6d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,7 +129,6 @@ ignore = [ "D213", # incompatible with D212 "D400", # A stricter version of #415 that doesn't allow ! # The checkers below should be turned on as they complain about missing docstrings. - "D103", "D104", "D105", "D107", diff --git a/scripts/zenodo/upload_to_zenodo.py b/scripts/zenodo/upload_to_zenodo.py index d52eeeb4..81507716 100644 --- a/scripts/zenodo/upload_to_zenodo.py +++ b/scripts/zenodo/upload_to_zenodo.py @@ -21,17 +21,19 @@ else: def parse_arguments() -> Namespace: + """Parse the commandline arguments and return a namespace containing the result.""" p = ArgumentParser(description="Upload data to Zenodo") p.add_argument("paths", help="Directories and files to upload to Zenodo", nargs="*") return p.parse_args() def script_directory(path): - """Resolve path to directory of the current script.""" + """Return path to directory of the current script.""" return os.path.join(os.path.dirname(os.path.realpath(__file__)), path) def get_meta(): + """Return the metadata in the script directory as a dictionary.""" with open(script_directory("metadata.yaml"), encoding="utf-8") as f: metadata = f.read() @@ -39,6 +41,10 @@ def get_meta(): def main(): + """Create the zenodo deposit and upload it to Zenodo. + + This is the main function called when the full script is run. + """ args = parse_arguments() metadata = get_meta() diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index a7cf580d..0530ecf0 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -21,6 +21,14 @@ OFM_LOG_FILE = None def configure_logging(log_folder): + """Configure logging for the server while it is running. + + This modifies the root logger to have a rotating file handler and + adds a custom handler that prints and stores all records except + ``uvicorn.access`` logs. + + It is important not to let Uvicorn override these settings. + """ root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) # Explictly make OFM_LOG_FILE a global so it can be updated based on log settings diff --git a/src/openflexure_microscope_server/server/legacy_api.py b/src/openflexure_microscope_server/server/legacy_api.py index 6cca9c30..65be40d5 100644 --- a/src/openflexure_microscope_server/server/legacy_api.py +++ b/src/openflexure_microscope_server/server/legacy_api.py @@ -6,6 +6,7 @@ from socket import gethostname def add_v2_endpoints(thing_server: lt.ThingServer): + """Add the v2 API endpoints for OpenFlexure Connect discoverability.""" app = thing_server.app # TODO: update openflexure connect to make this unnecessary!! diff --git a/src/openflexure_microscope_server/things/settings_manager.py b/src/openflexure_microscope_server/things/settings_manager.py index 28e41ced..dec88b3b 100644 --- a/src/openflexure_microscope_server/things/settings_manager.py +++ b/src/openflexure_microscope_server/things/settings_manager.py @@ -14,6 +14,7 @@ import labthings_fastapi as lt def thing_server_from_request(request: Request) -> lt.ThingServer: + """Wrap lt.find_thing_server.""" return lt.find_thing_server(request.app) diff --git a/tests/test_dummy_server.py b/tests/test_dummy_server.py index fd62ca5e..1fa2e2dc 100644 --- a/tests/test_dummy_server.py +++ b/tests/test_dummy_server.py @@ -31,6 +31,7 @@ camera_stage_mapping.DEFAULT_SETTLING_TIME = 0 # skip the settling time for tes @pytest.fixture def thing_server(): + """Yield a server with a very basic configuration.""" temp_folder = tempfile.TemporaryDirectory() server = lt.ThingServer(settings_folder=temp_folder.name) server.add_thing( @@ -43,36 +44,46 @@ def thing_server(): server.add_thing(AutofocusThing(), "/autofocus/") server.add_thing(CameraStageMapper(), "/camera_stage_mapping/") assert os.path.exists(os.path.join(temp_folder.name, "camera/")) - # NB yield is important: otherwise, the temp folder gets deleted before the test runs + # Note: yield is important. If return is used the temp folder gets deleted + # before the test runs yield server @pytest.fixture def client(thing_server): + """Yield a FastAPI TestClient for the server.""" with TestClient(thing_server.app) as client: yield client @pytest.fixture def slower_client(thing_server): + """Yield a FastAPI TestClient for the server with a slower moving stage. + + The step time for the stage is 100 microseconds rather than + 1 microsecond. + """ thing_server.things["/stage/"].step_time = 0.0001 with TestClient(thing_server.app) as client: yield client def test_autofocus(slower_client): + """Test Fast Autofocus can run doesn't raise an exception.""" client = slower_client autofocus = lt.ThingClient.from_url("/autofocus/", client) _ = autofocus.fast_autofocus() def test_grab_jpeg(client): + """Check that grab_jpeg returns a blob that can be opened.""" camera = lt.ThingClient.from_url("/camera/", client) blob = camera.grab_jpeg() _image = Image.open(blob.open()) def test_capture_jpeg_metadata(client): + """Check that the position is encoded into the image metadata.""" camera = lt.ThingClient.from_url("/camera/", client) blob = camera.capture_jpeg() image = Image.open(blob.open()) @@ -83,6 +94,7 @@ def test_capture_jpeg_metadata(client): def test_stage(client): + """Test moving th stage forwards and backwards.""" stage = lt.ThingClient.from_url("/stage/", client) start = stage.position move = {"x": 1, "y": 2, "z": 3} @@ -97,12 +109,13 @@ def test_stage(client): def test_capture_array(client): + """Capture array from simulation and check the size is as expected.""" camera = lt.ThingClient.from_url("/camera/", client) array = np.asarray(camera.capture_array()) assert array.shape == (240, 320, 3) -# Currently this fails, not yet sure why. def test_camera_stage_mapping_calibration(client): + """Check that camera stage mapping can run without an exception.""" camera_stage_mapping = lt.ThingClient.from_url("/camera_stage_mapping/", client) camera_stage_mapping.calibrate_xy() diff --git a/tests/test_scan_planners.py b/tests/test_scan_planners.py index 10f02a58..dd6bc83a 100644 --- a/tests/test_scan_planners.py +++ b/tests/test_scan_planners.py @@ -12,6 +12,7 @@ from .utilities import scan_test_helpers def test_enforce_xy_tuple(): + """Check that 2 value tuples (or ValueErrors) are always returned.""" bad_len_vals = [[1], [], (1,), (2, 4, 4), [1, 4, 5, 7]] bad_type_vals = ["hi", 1, {"this": "that"}, {1, 2}] for value in bad_len_vals + bad_type_vals: @@ -23,6 +24,7 @@ def test_enforce_xy_tuple(): def test_enforce_xyz_tuple(): + """Check that 3 value tuples (or ValueErrors) are always returned.""" bad_len_vals = [[1], [], (1,), (2, 4), [1, 4, 5, 7]] bad_type_vals = ["hi!", 1, {"this": "that"}, {1, 2, 3}] for value in bad_len_vals + bad_type_vals: @@ -34,12 +36,14 @@ def test_enforce_xyz_tuple(): def test_base_class_not_implemented(): + """Check NotImplementedError is raised when initialising ScanPlanner directly.""" intial_position = (100, 50) with pytest.raises(NotImplementedError): scan_planners.ScanPlanner(intial_position=intial_position) def test_v_basic_smart_spiral(): + """Check that a SmartSpiral where the first image is not sample completes.""" intial_position = (100, 50) planner_settings = {"dx": 50, "dy": 50, "max_dist": 10000} planner = scan_planners.SmartSpiral( @@ -71,6 +75,7 @@ def test_v_basic_smart_spiral(): def test_bad_smart_spiral_settings(): + """Check that KeyError is raised when SmartSpiral is given bad settings.""" intial_position = (100, 50) # Class init should raise error if no planner_settings dictionary set @@ -195,6 +200,7 @@ def test_smart_spiral_first_few_pos(): def test_smart_spiral_stops_on_max_dist(): + """Test that if max distance is reached smart spiral really does stop.""" intial_position = (0, 0) planner_settings = {"dx": 100, "dy": 100, "max_dist": 1000} # Create a planner