Skip to content

Commit 67e5088

Browse files
authored
Merge branch 'develop' into fix-zarr-check
2 parents 4b65046 + 01948a9 commit 67e5088

File tree

9 files changed

+992
-226
lines changed

9 files changed

+992
-226
lines changed

.github/workflows/python-package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
sudo apt update
3131
sudo apt-get install -y libopenslide-dev openslide-tools libopenjp2-7 libopenjp2-tools
3232
python -m pip install --upgrade pip
33-
python -m pip install ruff==0.9.4 pytest pytest-cov pytest-runner
33+
python -m pip install ruff==0.9.9 pytest pytest-cov pytest-runner
3434
pip install -r requirements/requirements.txt
3535
- name: Cache tiatoolbox static assets
3636
uses: actions/cache@v3

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ repos:
6060
- id: rst-inline-touching-normal # Detect mistake of inline code touching normal text in rst.
6161
- repo: https://github.com/astral-sh/ruff-pre-commit
6262
# Ruff version.
63-
rev: v0.9.4
63+
rev: v0.9.9
6464
hooks:
6565
- id: ruff
6666
args: [--fix, --exit-non-zero-on-fix]

requirements/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# torch installation
22
--extra-index-url https://download.pytorch.org/whl/cu118; sys_platform != "darwin"
3+
aiohttp>=3.8.1
34
albumentations>=1.3.0
45
bokeh>=3.1.1, <3.6.0
56
Click>=8.1.3

requirements/requirements_dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pytest>=7.2.0
1010
pytest-cov>=4.0.0
1111
pytest-runner>=6.0
1212
pytest-xdist[psutil]
13-
ruff==0.9.4 # This will be updated by pre-commit bot to latest version
13+
ruff==0.9.9 # This will be updated by pre-commit bot to latest version
1414
toml>=0.10.2
1515
twine>=4.0.1
1616
wheel>=0.37.1

tests/test_wsireader.py

Lines changed: 170 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
import shutil
1010
from copy import deepcopy
1111
from pathlib import Path
12-
13-
# When no longer supporting Python <3.9 this should be collections.abc.Iterable
1412
from typing import TYPE_CHECKING, Callable
13+
from unittest.mock import patch
1514

1615
import cv2
1716
import glymur
@@ -27,7 +26,7 @@
2726

2827
from tiatoolbox import cli, utils
2928
from tiatoolbox.annotation import SQLiteStore
30-
from tiatoolbox.utils import imread
29+
from tiatoolbox.utils import imread, tiff_to_fsspec
3130
from tiatoolbox.utils.exceptions import FileNotSupportedError
3231
from tiatoolbox.utils.magic import is_sqlite3
3332
from tiatoolbox.utils.transforms import imresize, locsize2bounds
@@ -37,6 +36,7 @@
3736
AnnotationStoreReader,
3837
ArrayView,
3938
DICOMWSIReader,
39+
FsspecJsonWSIReader,
4040
JP2WSIReader,
4141
NGFFWSIReader,
4242
OpenSlideWSIReader,
@@ -221,6 +221,43 @@ def read_bounds_level_consistency(wsi: WSIReader, bounds: IntBounds) -> None:
221221
# Utility Test Classes & Functions
222222
# -------------------------------------------------------------------------------------
223223

224+
_FSSPEC_WSI_CACHE = {}
225+
226+
227+
def fsspec_wsi(sample_svs: Path, tmp_path: Path) -> FsspecJsonWSIReader:
228+
"""Returns cached FsspecJsonWSIReader instance.
229+
230+
The reader instance opens CMU-1-Small-Region.svs image.
231+
232+
It's cached so the reader can be reused,
233+
234+
since loading the whole image using HTTP range requests from:
235+
236+
https://tiatoolbox.dcs.warwick.ac.uk/sample_wsis/CMU-1-Small-Region.svs
237+
238+
takes about 20 seconds.
239+
240+
"""
241+
cache_key = "sample_svs"
242+
243+
if cache_key in _FSSPEC_WSI_CACHE:
244+
return _FSSPEC_WSI_CACHE[cache_key] # Return cached instance
245+
246+
file_types = ("*.svs",)
247+
files_all = utils.misc.grab_files_from_dir(
248+
input_path=Path(sample_svs).parent,
249+
file_types=file_types,
250+
)
251+
svs_file_path = str(files_all[0])
252+
json_file_path = str(tmp_path / "fsspec.json")
253+
final_url = (
254+
"https://tiatoolbox.dcs.warwick.ac.uk/sample_wsis/CMU-1-Small-Region.svs"
255+
)
256+
tiff_to_fsspec.main(svs_file_path, json_file_path, final_url)
257+
258+
_FSSPEC_WSI_CACHE[cache_key] = wsireader.FsspecJsonWSIReader(json_file_path)
259+
return _FSSPEC_WSI_CACHE[cache_key]
260+
224261

225262
class DummyMutableOpenSlideObject:
226263
"""Dummy OpenSlide object with mutable properties."""
@@ -2812,3 +2849,133 @@ def test_read_multi_channel(source_image: Path) -> None:
28122849
assert region.shape == (100, 50, (new_img_array.shape[-1]))
28132850
assert np.abs(np.median(region.astype(int) - target.astype(int))) == 0
28142851
assert np.abs(np.mean(region.astype(int) - target.astype(int))) < 0.2
2852+
2853+
2854+
def test_fsspec_json_wsi_reader_instantiation() -> None:
2855+
"""Test if FsspecJsonWSIReader is instantiated.
2856+
2857+
In case json is passed to WSIReader.open, FsspecJsonWSIReader
2858+
should be instantiated.
2859+
"""
2860+
input_path = "mock_path.json"
2861+
mpp = None
2862+
power = None
2863+
2864+
with (
2865+
patch(
2866+
"tiatoolbox.wsicore.wsireader.FsspecJsonWSIReader.is_valid_zarr_fsspec",
2867+
return_value=True,
2868+
),
2869+
patch("tiatoolbox.wsicore.wsireader.FsspecJsonWSIReader") as mock_reader,
2870+
):
2871+
WSIReader.open(input_path, mpp, power)
2872+
mock_reader.assert_called_once_with(input_path, mpp=mpp, power=power)
2873+
2874+
2875+
def test_generate_fsspec_json_file_and_validate(
2876+
sample_svs: Path, tmp_path: Path
2877+
) -> None:
2878+
"""Test generate fsspec json file and validate it."""
2879+
file_types = ("*.svs",)
2880+
2881+
files_all = utils.misc.grab_files_from_dir(
2882+
input_path=Path(sample_svs).parent,
2883+
file_types=file_types,
2884+
)
2885+
2886+
svs_file_path = str(files_all[0])
2887+
json_file_path = str(tmp_path / "fsspec.json")
2888+
final_url = "https://example.com/some_id"
2889+
2890+
tiff_to_fsspec.main(svs_file_path, json_file_path, final_url)
2891+
2892+
assert Path(json_file_path).exists(), "Output JSON file was not created."
2893+
2894+
assert FsspecJsonWSIReader.is_valid_zarr_fsspec(json_file_path), (
2895+
"FSSPEC JSON file is invalid."
2896+
)
2897+
2898+
2899+
def test_fsspec_wsireader_info_read(sample_svs: Path, tmp_path: Path) -> None:
2900+
"""Test info read of the FsspecJsonWSIReader.
2901+
2902+
Generate fsspec json file and load image from:
2903+
2904+
https://tiatoolbox.dcs.warwick.ac.uk/sample_wsis/CMU-1-Small-Region.svs
2905+
2906+
"""
2907+
wsi = fsspec_wsi(sample_svs, tmp_path)
2908+
info = wsi.info
2909+
2910+
assert info is not None, "info should not be None."
2911+
2912+
2913+
def test_read_bounds_fsspec_reader_baseline(sample_svs: Path, tmp_path: Path) -> None:
2914+
"""Test FsspecJsonWSIReader read bounds at baseline.
2915+
2916+
Location coordinate is in baseline (level 0) reference frame.
2917+
2918+
"""
2919+
wsi = fsspec_wsi(sample_svs, tmp_path)
2920+
2921+
bounds = SVS_TEST_TISSUE_BOUNDS
2922+
size = SVS_TEST_TISSUE_SIZE
2923+
im_region = wsi.read_bounds(bounds, resolution=0, units="level")
2924+
2925+
assert isinstance(im_region, np.ndarray)
2926+
assert im_region.dtype == "uint8"
2927+
assert im_region.shape == (*size[::-1], 3)
2928+
2929+
2930+
def test_read_rect_fsspec_reader_baseline(sample_svs: Path, tmp_path: Path) -> None:
2931+
"""Test FsspecJsonWSIReader read rect at baseline.
2932+
2933+
Location coordinate is in baseline (level 0) reference frame.
2934+
2935+
"""
2936+
wsi = fsspec_wsi(sample_svs, tmp_path)
2937+
2938+
location = SVS_TEST_TISSUE_LOCATION
2939+
size = SVS_TEST_TISSUE_SIZE
2940+
im_region = wsi.read_rect(location, size, resolution=0, units="level")
2941+
2942+
assert isinstance(im_region, np.ndarray)
2943+
assert im_region.dtype == "uint8"
2944+
assert im_region.shape == (*size[::-1], 3)
2945+
2946+
2947+
def test_fsspec_reader_open_invalid_json_file(tmp_path: Path) -> None:
2948+
"""Ensure JSONDecodeError is handled properly.
2949+
2950+
Pass invalid JSON to FsspecJsonWSIReader.is_valid_zarr_fsspec.
2951+
"""
2952+
json_path = tmp_path / "invalid.json"
2953+
json_path.write_text("{invalid json}") # Corrupt JSON
2954+
2955+
assert not FsspecJsonWSIReader.is_valid_zarr_fsspec(str(json_path))
2956+
2957+
2958+
def test_fsspec_reader_open_oserror_handling() -> None:
2959+
"""Ensure OSError is handled properly.
2960+
2961+
Pass non existent JSON to FsspecJsonWSIReader.is_valid_zarr_fsspec.
2962+
2963+
"""
2964+
with patch("builtins.open", side_effect=OSError("File not found")):
2965+
result = FsspecJsonWSIReader.is_valid_zarr_fsspec("non_existent.json")
2966+
2967+
assert result is False, "Function should return False for OSError"
2968+
2969+
2970+
def test_fsspec_reader_open_pass_empty_json(tmp_path: Path) -> None:
2971+
"""Ensure empty JSON is handled properly.
2972+
2973+
Pass empty JSON to FsspecJsonWSIReader.is_valid_zarr_fsspec and
2974+
2975+
verify that it's not valid.
2976+
2977+
"""
2978+
json_path = tmp_path / "empty.json"
2979+
json_path.write_text("{}")
2980+
2981+
assert not FsspecJsonWSIReader.is_valid_zarr_fsspec(str(json_path))

tiatoolbox/annotation/storage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,8 @@ class AnnotationStore(ABC, MutableMapping[str, Annotation]):
442442

443443
def __new__(
444444
cls: type[StoreInstanceType],
445-
*args: str, # noqa: ARG003
446-
**kwargs: int, # noqa: ARG003
445+
*args: str, # noqa: ARG004
446+
**kwargs: int, # noqa: ARG004
447447
) -> StoreInstanceType:
448448
"""Return an instance of a subclass of AnnotationStore."""
449449
if cls is AnnotationStore:

0 commit comments

Comments
 (0)