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
26 changes: 25 additions & 1 deletion iohub/ngff/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ def _scale_integers(values: Sequence[int], factor: int) -> tuple[int, ...]:
return tuple(int(math.ceil(v / factor)) for v in values)


def _case_insensitive_local_fs() -> bool:
"""Check if the local filesystem is case-insensitive."""
return Path(__file__.lower()).exists() and Path(__file__.upper()).exists()


class NGFFNode:
"""A node (group level in Zarr) in an NGFF dataset."""

Expand Down Expand Up @@ -123,6 +128,9 @@ def __init__(
self._parse_meta()
if not hasattr(self, "axes"):
self.axes = self._DEFAULT_AXES
# TODO: properly check the underlying storage type
# This works for now as only the local filesystem is supported
self._case_insensitive_fs = _case_insensitive_local_fs()

@property
def zgroup(self):
Expand Down Expand Up @@ -196,7 +204,18 @@ def __delitem__(self, key):

def __contains__(self, key):
key = normalize_storage_path(key)
return key.lower() in [name.lower() for name in self._member_names]
if not self._case_insensitive_fs:
return key in self._member_names
for name in self._member_names:
if key.lower() != name.lower():
continue
if key != name:
_logger.warning(
f"Key '{key}' matched member '{name}'. "
"This may not work on case-sensitive filesystems."
)
return True
return False

def __iter__(self):
yield from self._member_names
Expand Down Expand Up @@ -1681,6 +1700,11 @@ def create_well(
# normalize input
row_name = normalize_storage_path(row_name)
col_name = normalize_storage_path(col_name)
if row_name in self:
if col_name in self[row_name]:
raise FileExistsError(
f"Well '{row_name}/{col_name}' already exists."
)
row_meta = PlateAxisMeta(name=row_name)
col_meta = PlateAxisMeta(name=col_name)
row_index = self._auto_idx(row_name, row_index, "row")
Expand Down
58 changes: 58 additions & 0 deletions tests/ngff/test_ngff.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import platform
import shutil
import string
from contextlib import contextmanager
Expand All @@ -22,8 +23,10 @@

from iohub.ngff.nodes import (
TO_DICT_SETTINGS,
NGFFNode,
Plate,
TransformationMeta,
_case_insensitive_local_fs,
_open_store,
_pad_shape,
open_ome_zarr,
Expand Down Expand Up @@ -150,6 +153,19 @@ def test_open_store_read_nonexist():
_ = _open_store(store_path, mode=mode, version="0.4")


def test_case_insensitive_local_fs():
"""Test `iohub.ngff._case_insensitive_local_fs()`"""
match platform.system():
case "Windows":
assert _case_insensitive_local_fs() is True
case "Darwin":
assert _case_insensitive_local_fs() is True
case "Linux":
assert _case_insensitive_local_fs() is False
case _:
_ = _case_insensitive_local_fs()


@given(channel_names=channel_names_st)
@settings(max_examples=16)
def test_init_ome_zarr(channel_names):
Expand Down Expand Up @@ -920,6 +936,20 @@ def test_get_axis_index():
_ = position.get_axis_index("DOG")


def test_ngff_node_contains_cross_platform(caplog):
"""Test `iohub.ngff.NGFFNode.__contains__()` on multiple platforms."""
with open_ome_zarr(hcs_ref, layout="hcs", mode="r") as dataset:
assert "B" in dataset
match platform.system():
case "Linux":
assert "b" not in dataset
case "Windows" | "Darwin":
assert "b" in dataset
assert any(
"Key 'b' matched" in r.message for r in caplog.records
)


@given(
row=short_alpha_numeric, col=short_alpha_numeric, pos=short_alpha_numeric
)
Expand Down Expand Up @@ -963,6 +993,34 @@ def test_create_well(row_names: list[str], col_names: list[str]):
] == row_names


def test_create_case_sensitive_well(tmp_path):
"""Test `iohub.ngff.Plate.create_well()` with case-sensitive names."""
store_path = tmp_path / "hcs.zarr"
with open_ome_zarr(
store_path, layout="hcs", mode="w-", channel_names=["1", "2"]
) as dataset:
well = dataset.create_well("A", "B")
fov = well.create_position("0")
fov.create_zeros("0", shape=(1, 2, 3, 4, 5), dtype=int)
match platform.system():
case "Windows" | "Darwin":
with pytest.raises(FileExistsError):
dataset.create_well("a", "B")
with pytest.raises(FileExistsError):
dataset.create_well("A", "b")
new_well = dataset.create_well("a", "1")
expected_rows = 1
case "Linux":
new_well = dataset.create_well("a", "b")
expected_rows = 2
new_fov = new_well.create_position("0")
new_fov.create_zeros("0", shape=(1, 2, 3, 4, 5), dtype=int)
with open_ome_zarr(store_path) as dataset:
assert len(dataset.metadata.rows) == expected_rows
assert len(list(dataset.rows())) == expected_rows
assert len(dataset.metadata.columns) == 2


@given(
row=short_alpha_numeric, col=short_alpha_numeric, pos=short_alpha_numeric
)
Expand Down