Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions gertils/pathtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ def _invalidate(self) -> None:

@doc(
summary=(
"Directly in given folder, find all filepaths with a field" " of view embedded in filename."
"Directly in given folder, find all filepaths with a field"
" of view embedded in filename."
),
parameters=dict(
folder="Path to folder in which to find files",
Expand All @@ -83,7 +84,7 @@ def _invalidate(self) -> None:
get_fov_sort_key="The function used to try to parse FOV from filename",
),
)
def find_multiple_paths_by_fov( # noqa: D103
def find_multiple_paths_by_fov(
folder: PathLike, *, extensions: Iterable[str]
) -> dict[FieldOfViewFrom1, list[Path]]:
if isinstance(folder, str):
Expand All @@ -108,14 +109,18 @@ def find_multiple_paths_by_fov( # noqa: D103
folder="Path to folder in which to find files",
extension="The extension of files to find",
),
raises=dict(RuntimeError="If the same FOV is found to correspond to more than one path"),
raises=dict(
RuntimeError="If the same FOV is found to correspond to more than one path"
),
returns="Mapping from field of view to filepath",
see_also=dict(
find_multiple_paths_by_fov="Similar function, for multiple paths by FOV, no particular extension",
get_fov_sort_key="The function used to try to parse FOV from filename",
),
)
def find_single_path_by_fov(folder: PathLike, *, extension: str) -> dict[FieldOfViewFrom1, Path]: # noqa: D103
def find_single_path_by_fov(
folder: PathLike, *, extension: str
) -> dict[FieldOfViewFrom1, Path]:
if isinstance(folder, str):
folder = Path(folder)
image_paths: dict[FieldOfViewFrom1, Path] = {}
Expand All @@ -140,7 +145,7 @@ def find_single_path_by_fov(folder: PathLike, *, extension: str) -> dict[FieldOf
),
returns="Field of view parsed from filename, if parse succeeded; otherwise, None",
)
def get_fov_sort_key(path: PathLike, *, extension: str) -> Optional[FieldOfViewFrom1]: # noqa: D103
def get_fov_sort_key(path: PathLike, *, extension: str) -> Optional[FieldOfViewFrom1]:
# Checked at runtime, since callers may pass any type despite the annotation.
if not isinstance(cast("object", path), str | Path):
raise TypeError(
Expand Down
43 changes: 31 additions & 12 deletions gertils/pixel_value_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ def from_image(
# Only the shape is read here, so that a lazy array such as dask's isn't loaded yet.
shape = np.shape(img)
if len(shape) != 3: # noqa: PLR2004
raise ValueError(f"To build {cls.__name__}, image must be 3D, not {len(shape)}D")
raise ValueError(
f"To build {cls.__name__}, image must be 3D, not {len(shape)}D"
)
planes = _z_window(
central_z,
shape[0],
Expand All @@ -91,7 +93,11 @@ def from_image(


def _z_window(
central_z: ZCoordinate, num_planes: int, *, plus_minus_planes: int, require_full_padding: bool
central_z: ZCoordinate,
num_planes: int,
*,
plus_minus_planes: int,
require_full_padding: bool,
) -> slice:
"""Get the z-planes to measure: the one nearest the point, plus padding, cut off at the edges."""
# Round halves up, as for x and y; round() would round them to the nearest even number.
Expand All @@ -117,9 +123,7 @@ def _z_window(
lower_slice_bound = round_z - plus_minus_planes
upper_slice_bound = round_z + plus_minus_planes + 1
if lower_slice_bound < 0 or upper_slice_bound > num_planes:
oob_slice_msg = (
f"[{lower_slice_bound}, {upper_slice_bound}) slice for image of {num_planes} z-slices"
)
oob_slice_msg = f"[{lower_slice_bound}, {upper_slice_bound}) slice for image of {num_planes} z-slices"
if require_full_padding:
raise ValueError(oob_slice_msg)
logging.debug(oob_slice_msg)
Expand Down Expand Up @@ -179,7 +183,12 @@ def _locate_region( # noqa: PLR0913
f"Region x [{left}, {right}), y [{top}, {bottom}) around point ({pt.x}, {pt.y}) "
f"lies outside the image of {num_cols} x {num_rows} pixels"
)
if require_full_region and (y_min, y_max, x_min, x_max) != (top, bottom, left, right):
if require_full_region and (y_min, y_max, x_min, x_max) != (
top,
bottom,
left,
right,
):
raise ValueError(
f"Region x [{left}, {right}), y [{top}, {bottom}) around point ({pt.x}, {pt.y}) "
f"extends beyond the image of {num_cols} x {num_rows} pixels"
Expand All @@ -191,7 +200,12 @@ def _locate_region( # noqa: PLR0913
require_full_padding=require_full_padding,
)
return _Region(
bounds={"y_min_px": y_min, "y_max_px": y_max, "x_min_px": x_min, "x_max_px": x_max},
bounds={
"y_min_px": y_min,
"y_max_px": y_max,
"x_min_px": x_min,
"x_max_px": x_max,
},
planes=planes,
rows=slice(y_min, y_max),
columns=slice(x_min, x_max),
Expand Down Expand Up @@ -222,7 +236,7 @@ def _locate_region( # noqa: PLR0913
compute_pixel_statistics="The same, for several channels at once",
),
)
def compute_pixel_statistics_for_channel( # noqa: D103, PLR0913
def compute_pixel_statistics_for_channel( # noqa: PLR0913
img: PixelArray,
pt: ImagePoint3D,
*,
Expand All @@ -243,7 +257,9 @@ def compute_pixel_statistics_for_channel( # noqa: D103, PLR0913
)
# Only the region is read, and of it only the planes of the z-window.
# Indexing a dask array has no type information; the region is the same kind of array.
region_img = cast("PixelArray", img[channel.get, region.planes, region.rows, region.columns])
region_img = cast(
"PixelArray", img[channel.get, region.planes, region.rows, region.columns]
)
return {
channel_column: channel.get,
**region.bounds,
Expand Down Expand Up @@ -275,7 +291,7 @@ def compute_pixel_statistics_for_channel( # noqa: D103, PLR0913
compute_pixel_statistics_for_channel="The same, for a single channel",
),
)
def compute_pixel_statistics( # noqa: D103, PLR0913
def compute_pixel_statistics( # noqa: PLR0913
img: PixelArray,
pt: ImagePoint3D,
*,
Expand All @@ -301,12 +317,15 @@ def compute_pixel_statistics( # noqa: D103, PLR0913
# information; the result is the same kind of array.
all_channels = cast(
"PixelArray",
img[[ch.get for ch in channels], region.planes, region.rows, region.columns],
img[
[ch.get for ch in channels], region.planes, region.rows, region.columns
],
)
region_imgs = list(np.asarray(all_channels))
else:
region_imgs = [
np.asarray(img[ch.get, region.planes, region.rows, region.columns]) for ch in channels
np.asarray(img[ch.get, region.planes, region.rows, region.columns])
for ch in channels
]
return [
{channel_column: ch.get, **region.bounds, **_statistics_of(region_img).to_dict}
Expand Down
14 changes: 10 additions & 4 deletions gertils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class FieldOfViewFrom1: # noqa: D101
def __post_init__(self) -> None:
if not _is_int(self.get):
raise TypeError(
f"Non-integer as 1-based FOV! {self.get} (type" f" {type(self.get).__name__})"
f"Non-integer as 1-based FOV! {self.get} (type"
f" {type(self.get).__name__})"
)
if self.get < 1:
raise ValueError(f"1-based FOV view must be positive int; got {self.get}")
Expand All @@ -50,7 +51,9 @@ class ImagingChannel: # noqa: D101

def __post_init__(self) -> None:
if not _is_int(self.get):
raise TypeError(f"Imaging channel must be integer, not {type(self.get).__name__}")
raise TypeError(
f"Imaging channel must be integer, not {type(self.get).__name__}"
)
if self.get < 0:
raise ValueError(f"Imaging channel must be nonnegative; got {self.get}")

Expand All @@ -70,7 +73,8 @@ class NucleusNumber: # noqa: D101
def __post_init__(self) -> None:
if not _is_int(self.get):
raise TypeError(
f"Non-integer as nucleus number! {self.get} (type" f" {type(self.get).__name__})"
f"Non-integer as nucleus number! {self.get} (type"
f" {type(self.get).__name__})"
)
if self.get < 1:
raise ValueError(f"Nucleus number must be positive int; got {self.get}")
Expand Down Expand Up @@ -111,6 +115,8 @@ class TraceIdFrom0: # noqa: D101

def __post_init__(self) -> None:
if not _is_int(self.get):
raise TypeError(f"Non-integer as trace ID! {self.get} (type {type(self.get).__name__})")
raise TypeError(
f"Non-integer as trace ID! {self.get} (type {type(self.get).__name__})"
)
if self.get < 0:
raise ValueError(f"Trace ID must be nonnegative int; got {self.get}")
6 changes: 4 additions & 2 deletions gertils/zarr_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@
parameters=dict(root="Path at which datastore is rooted"),
returns="Array of pixel (or similar) data",
)
def read_zarr(root: Path) -> PixelArray: # noqa: D103
def read_zarr(root: Path) -> PixelArray:
logging.debug("Reading ZARR: %s", root)
if (root / ".zarray").is_file():
data_root = root
elif (root / "0" / ".zarray").is_file():
data_root = root / "0"
else:
raise ZarrParseException(path=root, msg="Failed to find .zarray to indicate data folder")
raise ZarrParseException(
path=root, msg="Failed to find .zarray to indicate data folder"
)
# The check above found an array, so open it as one; reading is all this function does.
return np.asarray(zarr.open_array(str(data_root), mode="r"))

Expand Down
45 changes: 23 additions & 22 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 13 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ optional = true
codespell = "^2.2.2"
hypothesis = "^6.68.2"
pre-commit = "^4.0"
pyright = "^1.1.411"
pyright = "==1.1.411"
pytest = "^7.1.0"
pytest-cov = "^5.0.0"
ruff = ">=0.3"
ruff = "==0.7.4"

[tool.poetry.group.lint]
optional = true
Expand All @@ -57,9 +57,11 @@ optional = true
codespell = "^2.2.2"
hypothesis = "^6.68.2"
pre-commit = "^4.0"
pyright = "^1.1.411"
# The version looptrace pins, so the projects are type-checked by the same pyright.
pyright = "==1.1.411"
pytest = "^7.1.0"
ruff = ">=0.3"
# The version looptrace pins, so the projects are formatted and linted by the same ruff.
ruff = "==0.7.4"

[tool.poetry.group.test]
optional = true
Expand All @@ -83,9 +85,7 @@ strict = ["gertils"]
reportUnnecessaryTypeIgnoreComment = "error"

[tool.ruff]
# Black uses line-length = 88, but allows exceptions when breaking the line
# would lead to other rule violations. Use 100 as a maximum hard limit:
line-length = 100
# No line-length: ruff's default of 88, as in the other repositories behind looptrace.
target-version = "py311"

[tool.ruff.lint]
Expand Down Expand Up @@ -134,6 +134,12 @@ ignore = [
"E501", # line-too-long
]

[tool.ruff.lint.pydocstyle]
# numpydoc_decorator's @doc writes the docstring of the functions it decorates when the
# module is imported, so ruff, which only reads the source, doesn't see one. The setting
# applies only to functions and methods, so classes keep their `# noqa: D101`.
ignore-decorators = ["numpydoc_decorator.doc"]

[tool.ruff.lint.per-file-ignores]
"typings/*" = [
# Stubs copy the signatures of third-party code, which we can't change.
Expand Down
17 changes: 13 additions & 4 deletions tests/test_get_fov_sort_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

import pytest

from gertils.pathtools import find_multiple_paths_by_fov, find_single_path_by_fov, get_fov_sort_key
from gertils.pathtools import (
find_multiple_paths_by_fov,
find_single_path_by_fov,
get_fov_sort_key,
)
from gertils.types import FieldOfViewFrom1


Expand Down Expand Up @@ -45,10 +49,14 @@ def test_only_one_leading_p_is_removed():

def test_double_zarr_extension_is_parsed_with_a_warning():
with pytest.warns(FutureWarning, match="Stripping second '.zarr' extension"):
assert get_fov_sort_key("P0003.zarr.zarr", extension=".zarr") == FieldOfViewFrom1(3)
assert get_fov_sort_key(
"P0003.zarr.zarr", extension=".zarr"
) == FieldOfViewFrom1(3)


def test_find_multiple_paths_by_fov_keeps_fovs_apart_for_extension_with_digits(tmp_path):
def test_find_multiple_paths_by_fov_keeps_fovs_apart_for_extension_with_digits(
tmp_path,
):
for name in ["P0001.nd2", "P0012.nd2"]:
(tmp_path / name).touch()
assert find_multiple_paths_by_fov(tmp_path, extensions=[".nd2"]) == {
Expand Down Expand Up @@ -80,7 +88,8 @@ def test_file_with_number_zero_is_skipped_in_a_folder_scan(tmp_path):


@pytest.mark.parametrize(
"filename", ["P 12.zarr", "P+12.zarr", "P1_2.zarr", "P\uff11\uff12.zarr", "P12 .zarr"]
"filename",
["P 12.zarr", "P+12.zarr", "P1_2.zarr", "P\uff11\uff12.zarr", "P12 .zarr"],
)
def test_fov_number_must_be_plain_ascii_digits(filename):
assert get_fov_sort_key(filename, extension=".zarr") is None
Expand Down
Loading
Loading