diff --git a/looptrace_regionals_vis/__init__.py b/looptrace_regionals_vis/__init__.py index d532c9c..a5a4f84 100644 --- a/looptrace_regionals_vis/__init__.py +++ b/looptrace_regionals_vis/__init__.py @@ -22,8 +22,8 @@ ), returns="List of paths to files in the given/default location within this package", ) -def find_package_files(subfolder: str = "examples") -> list[Path]: - search_folder = get_package_resources().joinpath(subfolder) +def find_package_files(subfolder: str = "examples") -> list[Path]: # noqa: D103 + search_folder = _get_package_resources().joinpath(subfolder) result = [path for path in search_folder.iterdir() if path.is_file()] if len(result) == 0: raise ValueError( @@ -33,18 +33,18 @@ def find_package_files(subfolder: str = "examples") -> list[Path]: @doc(summary="Get the path to this package's examples folder.") -def get_package_examples_folder() -> Path: - return get_package_resources().joinpath("examples") +def get_package_examples_folder() -> Path: # noqa: D103 + return _get_package_resources().joinpath("examples") @doc( summary="Get the hook with which to access resources bundled with this packge.", see_also="importlib.resources.files", ) -def get_package_resources() -> Traversable: +def _get_package_resources() -> Traversable: return importlib.resources.files(_PACKAGE_NAME) @doc(summary="List the files bundles as examples with this package.") -def list_package_example_files() -> list[Path]: +def list_package_example_files() -> list[Path]: # noqa: D103 return [path for path in get_package_examples_folder().iterdir() if path.is_file()] diff --git a/looptrace_regionals_vis/bounding_box.py b/looptrace_regionals_vis/bounding_box.py index b744e8a..34a17ad 100644 --- a/looptrace_regionals_vis/bounding_box.py +++ b/looptrace_regionals_vis/bounding_box.py @@ -1,9 +1,10 @@ """Bounding boxes""" -from abc import abstractmethod import dataclasses +from abc import abstractmethod +from collections.abc import Iterable from math import floor -from typing import Iterable, Protocol +from typing import Protocol from numpydoc_decorator import doc @@ -71,8 +72,18 @@ class BoundingBox3D(RectangularPrismLike): # noqa: D101 x_max: float @classmethod - def from_flat_arguments( - cls, *, zc, yc, xc, z_min, z_max, y_min, y_max, x_min, x_max + def from_flat_arguments( # noqa: D102 + cls, + *, + zc, # noqa: ANN001 + yc, # noqa: ANN001 + xc, # noqa: ANN001 + z_min, # noqa: ANN001 + z_max, # noqa: ANN001 + y_min, # noqa: ANN001 + y_max, # noqa: ANN001 + x_min, # noqa: ANN001 + x_max, # noqa: ANN001 ) -> "BoundingBox3D": point = Point3D(z=zc, y=yc, x=xc) return cls( @@ -86,27 +97,27 @@ def from_flat_arguments( ) @doc(summary="Left side in x dimension") - def get_x_min(self) -> float: + def get_x_min(self) -> float: # noqa: D102 return self.x_min @doc(summary="Right side in x dimension") - def get_x_max(self) -> float: + def get_x_max(self) -> float: # noqa: D102 return self.x_max @doc(summary="Left side in y dimension") - def get_y_min(self) -> float: + def get_y_min(self) -> float: # noqa: D102 return self.y_min @doc(summary="Right side in y dimension") - def get_y_max(self) -> float: + def get_y_max(self) -> float: # noqa: D102 return self.y_max @doc(summary="Left side in z dimension") - def get_z_min(self) -> float: + def get_z_min(self) -> float: # noqa: D102 return self.z_min @doc(summary="Right side in z dimension") - def get_z_max(self) -> float: + def get_z_max(self) -> float: # noqa: D102 return self.z_max def __post_init__(self) -> None: diff --git a/looptrace_regionals_vis/processing.py b/looptrace_regionals_vis/processing.py index 0d80a38..d126371 100644 --- a/looptrace_regionals_vis/processing.py +++ b/looptrace_regionals_vis/processing.py @@ -1,12 +1,12 @@ """Data types related to encoding of data processing steps and status""" -from enum import Enum import logging +from enum import Enum from pathlib import Path from typing import Optional -from numpydoc_decorator import doc import pandas as pd +from numpydoc_decorator import doc from .bounding_box import BoundingBox3D from .colors import INDIGO, PALE_RED_CLAY, PALE_SKY_BLUE @@ -32,7 +32,7 @@ def filename_extension(self) -> str: def from_string(cls, s: str) -> Optional["ProcessingStep"]: """Attempt to parse given string as a processing step.""" for member in cls: - if s == member.name or s == member.value or s == member.filename_extension: + if s in {member.name, member.value, member.filename_extension}: return member return None @@ -63,7 +63,7 @@ def color(self) -> str: summary="Decide whether to use the given record.", parameters=dict(record="Record (e.g., row from CSV) of data to consider for building box."), ) - def record_to_box(self, record: MappingLike) -> Optional[BoundingBox3D]: + def record_to_box(self, record: MappingLike) -> Optional[BoundingBox3D]: # noqa: D102 data = record.to_dict() if isinstance(record, pd.Series) else record return BoundingBox3D.from_flat_arguments(**data) @@ -72,10 +72,10 @@ def from_filename(cls, fn: str) -> Optional["ProcessingStatus"]: """Attempt to infer processing status from given filename.""" chunks = fn.split(".") if not chunks[0].endswith("_rois"): - logging.debug(f"There's no ROI-indicative suffix in file basename ({chunks[0]})") + logging.debug("There's no ROI-indicative suffix in file basename (%s)", chunks[0]) return None if chunks[-1] != "csv": - logging.debug(f"No CSV extension on filename '{fn}'") + logging.debug("No CSV extension on filename '%s'", fn) return None steps = tuple(ProcessingStep.from_string(c) for c in chunks[1:-1]) for member in cls: diff --git a/looptrace_regionals_vis/reader.py b/looptrace_regionals_vis/reader.py index 2ce274e..62cfb3f 100644 --- a/looptrace_regionals_vis/reader.py +++ b/looptrace_regionals_vis/reader.py @@ -1,16 +1,15 @@ """Tools for creating the reader of regional points data""" -from collections import Counter -from collections.abc import Callable import dataclasses import logging -import os +from collections import Counter +from collections.abc import Callable from pathlib import Path from typing import Literal, Optional +import pandas as pd from gertils.types import TimepointFrom0 from numpydoc_decorator import doc -import pandas as pd from .bounding_box import BoundingBox3D from .point import FloatLike, Point3D @@ -36,7 +35,7 @@ def get_reader(path: PathOrPaths) -> Optional[Reader]: """Get a single-file parser with which to build layer data.""" - def do_not_parse(msg, *, level=logging.DEBUG) -> None: + def do_not_parse(msg, *, level=logging.DEBUG) -> None: # noqa: ANN001 logging.log(msg=msg, level=level) # Check that the given path is indeed a single extant file. @@ -62,7 +61,7 @@ def do_not_parse(msg, *, level=logging.DEBUG) -> None: return None # Create the parser. - def build_layers(folder) -> list[FullDataLayer]: + def build_layers(folder) -> list[FullDataLayer]: # noqa: ANN001 # Map (uniquely!) each data kind/status to a file to parse. file_by_kind: dict[ProcessingStatus, Path] = {} for fp in Path(folder).iterdir(): @@ -90,7 +89,9 @@ def build_layers(folder) -> list[FullDataLayer]: if box is None: continue for q1, q2, q3, q4, is_center_slice in box.iter_z_slices(): - corners.append([[timepoint, channel] + point_to_list(pt) for pt in [q1, q2, q3, q4]]) + corners.append( + [[timepoint, channel, *point_to_list(pt)] for pt in [q1, q2, q3, q4]] + ) shapes.append("rectangle" if is_center_slice else "ellipse") logging.debug("Point count for status %s: %d", status.name, len(corners)) params: dict[str, object] = { @@ -111,12 +112,16 @@ def build_layers(folder) -> list[FullDataLayer]: parameters=dict(path="Path to data file from which to parse bounding boxes"), raises=dict(ValueError="If data kind/status can't be inferred from given path"), ) -def parse_boxes(path: Path) -> tuple[ProcessingStatus, list[tuple[TimepointFrom0, Optional[BoundingBox3D]]]]: +def parse_boxes( # noqa: D103 + path: Path, +) -> tuple[ProcessingStatus, list[tuple[TimepointFrom0, Optional[BoundingBox3D]]]]: status = ProcessingStatus.from_filepath(path) if status is None: raise ValueError(f"Could not infer data kind/status from path: {path}") box_cols = [f.name for f in dataclasses.fields(BoundingBox3D) if f.name != "center"] - spot_data = pd.read_csv(path, usecols=BOX_CENTER_COLUMN_NAMES + box_cols + [TIME_COLUMN, CHANNEL_COLUMN]) + spot_data = pd.read_csv( + path, usecols=BOX_CENTER_COLUMN_NAMES + box_cols + [TIME_COLUMN, CHANNEL_COLUMN] + ) time_channel_box_trios: list[tuple[TimepointFrom0, int, Optional[BoundingBox3D]]] = [] for _, record in spot_data.iterrows(): data = record.to_dict() if isinstance(record, pd.Series) else record @@ -132,5 +137,5 @@ def parse_boxes(path: Path) -> tuple[ProcessingStatus, list[tuple[TimepointFrom0 parameters=dict(pt="Point to flatten"), returns="[z, y, x]", ) -def point_to_list(pt: Point3D) -> list[FloatLike]: +def point_to_list(pt: Point3D) -> list[FloatLike]: # noqa: D103 return [pt.z, pt.y, pt.x] diff --git a/tests/test_bounding_box.py b/tests/test_bounding_box.py index 1372498..e7d0a4b 100644 --- a/tests/test_bounding_box.py +++ b/tests/test_bounding_box.py @@ -5,8 +5,8 @@ from typing import Optional import hypothesis as hyp -from hypothesis import strategies as st import pytest +from hypothesis import strategies as st from looptrace_regionals_vis.bounding_box import BoundingBox3D from looptrace_regionals_vis.point import Point3D @@ -129,20 +129,16 @@ def test_rectangle_protocol_support(box, api_member, validation_attribute): @hyp.given(error_inducing_arguments=gen_bbox_arguments_with_contextually_illegal_center()) def test_center_must_be_within_bounds(error_inducing_arguments): - with pytest.raises(ValueError) as error_context: - return BoundingBox3D.from_flat_arguments(**error_inducing_arguments) - exp_msg = "For each dimension, center coordinate must be within min/max bounds!" - obs_msg = str(error_context.value) - assert obs_msg == exp_msg, f"Expected error message '{exp_msg}' but got '{obs_msg}'" + with pytest.raises( + ValueError, match="For each dimension, center coordinate must be within min/max bounds!" + ): + BoundingBox3D.from_flat_arguments(**error_inducing_arguments) @hyp.given(error_inducing_arguments=gen_bbox_arguments_with_contextually_illegal_endpoints()) def test_endpoints_must_make_sense(error_inducing_arguments): - with pytest.raises(ValueError) as error_context: - return BoundingBox3D.from_flat_arguments(**error_inducing_arguments) - exp_msg = "For each dimension, min must be no more than max!" - obs_msg = str(error_context.value) - assert obs_msg == exp_msg, f"Expected error message '{exp_msg}' but got '{obs_msg}'" + with pytest.raises(ValueError, match="For each dimension, min must be no more than max!"): + BoundingBox3D.from_flat_arguments(**error_inducing_arguments) @hyp.given(box=gen_bbox_legit()) @@ -174,16 +170,16 @@ def test_iter_z_slices__always_designates_zero_or_one_z_slice_as_central(box): @hyp.given(box=gen_bbox_legit(min_z=-5, max_z=5)) # smaller z range here for efficiency def test_iter_z_slices__maintains_box_coordinates(box): for i, (q1, q2, q3, q4, _) in enumerate(box.iter_z_slices()): - assert ( + assert ( # noqa: PT018 q1.x == box.x_max and q1.y == box.y_min ), f"Bad top-left point ({q1}) in {i}-th z-slice, from box {box}" - assert ( + assert ( # noqa: PT018 q2.x == box.x_min and q2.y == box.y_min ), f"Bad top-left point ({q2}) in {i}-th z-slice, from box {box}" - assert ( + assert ( # noqa: PT018 q3.x == box.x_min and q3.y == box.y_max ), f"Bad top-left point ({q3}) in {i}-th z-slice, from box {box}" - assert ( + assert ( # noqa: PT018 q4.x == box.x_max and q4.y == box.y_max ), f"Bad bottom-right point ({q4}) in {i}-th z-slice, from box {box}" diff --git a/tests/test_bounding_box_parsing.py b/tests/test_bounding_box_parsing.py index 6c96f1c..92cf955 100644 --- a/tests/test_bounding_box_parsing.py +++ b/tests/test_bounding_box_parsing.py @@ -31,7 +31,7 @@ def test_cannot_parse_boxes_without_center(tmp_path, spots_file, drop_cols): pd.read_csv(spots_file, index_col=0).drop(list(drop_cols), axis=1).to_csv(target) # We expect an error because of a request to read specific columns (usecols) that will now be absent (having been removed). - with pytest.raises(ValueError) as error_context: + with pytest.raises(ValueError) as error_context: # noqa: PT011 parse_boxes(target) assert str(error_context.value).startswith( "Usecols do not match columns, columns expected but not found" diff --git a/tests/test_color_determination.py b/tests/test_color_determination.py index 1edb22a..4f04b37 100644 --- a/tests/test_color_determination.py +++ b/tests/test_color_determination.py @@ -19,6 +19,6 @@ def test_colors_are_as_expected(status, expected_color): ), f"Color for data processing status {status} wasn't as expected ({expected_color}): {observed_color}" -@pytest.mark.parametrize("status", [s for s in ProcessingStatus]) +@pytest.mark.parametrize("status", list(ProcessingStatus)) def test_each_member_of_data_status_enum_resolves_to_a_color(status): assert status.color is not None, f"Got null color for data processing status {status}" diff --git a/tests/test_find_package_files.py b/tests/test_find_package_files.py index bba698c..05fbd0f 100644 --- a/tests/test_find_package_files.py +++ b/tests/test_find_package_files.py @@ -5,13 +5,12 @@ from unittest import mock import hypothesis as hyp -from hypothesis import strategies as st import pytest +from hypothesis import strategies as st import looptrace_regionals_vis from looptrace_regionals_vis import find_package_files - gen_non_extant_resource_folder = st.text( alphabet=string.ascii_letters + string.digits + "_-" ).filter(lambda sub: not importlib.resources.files(looptrace_regionals_vis).joinpath(sub).is_dir()) @@ -40,6 +39,8 @@ def test_empty_subfolder_search_raises_expected_error(tmp_path, subfolder): """Here we patch the resource-finding call so that we simulate injecting an empty folder into the package resources.""" filemock = mock.MagicMock() filemock.joinpath.return_value = tmp_path - with mock.patch("looptrace_regionals_vis.importlib.resources.files", return_value=filemock): - with pytest.raises(ValueError): - find_package_files(subfolder) + with ( + mock.patch("looptrace_regionals_vis.importlib.resources.files", return_value=filemock), + pytest.raises(ValueError), # noqa: PT011 + ): + find_package_files(subfolder) diff --git a/tests/test_layers.py b/tests/test_layers.py index 0c234a7..53de3f8 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -27,9 +27,9 @@ def determine_parameters(folder) -> list[LayerParams]: assert folder.is_dir(), f"Could not find example folder: {folder}" read = get_reader(folder) if not callable(read): - raise AssertionError(f"Expected to be able to read {folder} but couldn't!") + raise RuntimeError(f"Expected to be able to read {folder} but couldn't!") # noqa: TRY004 try: layers = read(folder) except ValueError as e: - raise AssertionError("Expected successful data parse but didn't get it!") from e + raise RuntimeError("Expected successful data parse but didn't get it!") from e return [params for _, params, _ in layers] diff --git a/tests/test_point.py b/tests/test_point.py index c6d77b3..87b78fd 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -3,12 +3,11 @@ import math import hypothesis as hyp -from hypothesis import strategies as st import pytest +from hypothesis import strategies as st from looptrace_regionals_vis.point import Point3D - legal_float = st.floats(allow_nan=False, allow_infinity=False) gen_int_or_float = st.one_of(st.integers(), legal_float) gen_infinity = st.sampled_from((-math.inf, math.inf)) @@ -52,9 +51,8 @@ def test_point_cannot_be_constructed_with_non_float(coordinates): ) def test_point_cannot_be_constructed_with_infinite(coordinates): z, y, x = coordinates - with pytest.raises(ValueError) as error_context: + with pytest.raises(ValueError, match="Cannot use an infinite value as a point coordinate!"): Point3D(z=z, y=y, x=x) - assert "Cannot use an infinite value as a point coordinate!" == str(error_context.value) @hyp.given( @@ -68,6 +66,5 @@ def test_point_cannot_be_constructed_with_infinite(coordinates): ) def test_point_cannot_be_constructed_with_nan(coordinates): z, y, x = coordinates - with pytest.raises(ValueError) as error_context: + with pytest.raises(ValueError, match="Cannot use a null numeric as a point coordinate!"): Point3D(z=z, y=y, x=x) - assert "Cannot use a null numeric as a point coordinate!" == str(error_context.value) diff --git a/tests/test_processing_status_inference.py b/tests/test_processing_status_inference.py index 8a56af3..45b55cb 100644 --- a/tests/test_processing_status_inference.py +++ b/tests/test_processing_status_inference.py @@ -5,9 +5,8 @@ from typing import Optional import hypothesis as hyp -from hypothesis import strategies as st import pytest - +from hypothesis import strategies as st from numpydoc_decorator import doc from looptrace_regionals_vis import find_package_files @@ -23,7 +22,7 @@ ), ) @dataclass(kw_only=True, frozen=True) -class ProcessingStatusInferenceParameterization: # noqa: D101 +class ProcessingStatusInferenceParameterization: suffix: str extension: str expectation: Optional[ProcessingStatus] diff --git a/tests/test_what_can_and_cannot_be_parsed.py b/tests/test_what_can_and_cannot_be_parsed.py index 595497a..d263db3 100644 --- a/tests/test_what_can_and_cannot_be_parsed.py +++ b/tests/test_what_can_and_cannot_be_parsed.py @@ -3,12 +3,12 @@ import itertools import shutil from pathlib import Path + import pytest from looptrace_regionals_vis import get_package_examples_folder, list_package_example_files from looptrace_regionals_vis.reader import get_reader - EXAMPLE_FILES = list_package_example_files() @@ -18,7 +18,7 @@ def test_cannot_read_list_of_files(): ), "Expected inability to parse list of filepaths, but got non-null reader!" -@pytest.mark.parametrize("wrap", (str, Path)) +@pytest.mark.parametrize("wrap", [str, Path]) def test_would_read_collectivity_of_package_examples(wrap): folder = wrap(get_package_examples_folder()) assert callable( @@ -26,7 +26,7 @@ def test_would_read_collectivity_of_package_examples(wrap): ), f"Expected a callable reader for path {folder} but didn't get one!" -@pytest.mark.parametrize("wrap", (str, Path)) +@pytest.mark.parametrize("wrap", [str, Path]) @pytest.mark.parametrize( "what_to_copy", [