diff --git a/aicsimageio/readers/bioformats_reader.py b/aicsimageio/readers/bioformats_reader.py index 04104697c..d0f72f863 100644 --- a/aicsimageio/readers/bioformats_reader.py +++ b/aicsimageio/readers/bioformats_reader.py @@ -412,9 +412,9 @@ def to_dask(self, series: Optional[int] = None) -> DaskArrayProxy: return DaskArrayProxy(arr, self) @property - def is_open(self) -> bool: + def closed(self) -> bool: """Whether the underlying file is currently open""" - return bool(self._r.getCurrentFile()) + return not bool(self._r.getCurrentFile()) @property def filename(self) -> str: @@ -437,8 +437,7 @@ def ome_metadata(self) -> OME: return OME.from_xml(xml) def __enter__(self) -> BioFile: - if not self.is_open: - self.open() + self.open() return self def __exit__(self, *args: Any) -> None: @@ -476,7 +475,7 @@ def _get_plane( array of requested bytes. """ with self._lock: - was_open = self.is_open + was_open = not self.closed if not was_open: self.open() diff --git a/aicsimageio/tests/readers/test_bioformats_reader.py b/aicsimageio/tests/readers/test_bioformats_reader.py index 804544301..5a4a07ef0 100644 --- a/aicsimageio/tests/readers/test_bioformats_reader.py +++ b/aicsimageio/tests/readers/test_bioformats_reader.py @@ -8,7 +8,7 @@ from ome_types import OME from aicsimageio import dimensions, exceptions -from aicsimageio.readers.bioformats_reader import BioformatsReader +from aicsimageio.readers.bioformats_reader import BioFile, BioformatsReader from aicsimageio.tests.image_container_test_utils import ( run_image_file_checks, run_multi_scene_image_read_checks, @@ -406,12 +406,17 @@ def test_multi_scene_bioformats_reader( ) -@pytest.mark.parametrize( - "filename, ", - [ - ("CMU-1-Small-Region.svs"), - ], -) +def test_biofile_scene_change() -> None: + """Make sure that DaskArrayProxy doesn't close an opened file.""" + uri = get_resource_full_path("ND2_dims_p4z5t3c2y32x32.nd2", LOCAL) + f = BioFile(uri) + assert isinstance(f.to_dask().compute(), np.ndarray) + f.set_series(1) + assert isinstance(f.to_dask().compute(), np.ndarray) + f.close() + + +@pytest.mark.parametrize("filename, ", [("CMU-1-Small-Region.svs")]) def test_bioformats_dask_tiling_shapes(filename: str) -> None: # Construct full filepath uri = get_resource_full_path(filename, LOCAL) diff --git a/aicsimageio/tests/utils/test_dask_proxy.py b/aicsimageio/tests/utils/test_dask_proxy.py index df3686938..22fc7d7bc 100644 --- a/aicsimageio/tests/utils/test_dask_proxy.py +++ b/aicsimageio/tests/utils/test_dask_proxy.py @@ -13,6 +13,10 @@ class FileContext: FILE_OPEN = False OPEN_COUNT = 0 + @property + def closed(self) -> bool: + return not self.FILE_OPEN + def __enter__(self) -> "FileContext": if not self.FILE_OPEN: self.OPEN_COUNT += 1 diff --git a/aicsimageio/utils/dask_proxy.py b/aicsimageio/utils/dask_proxy.py index e42b7d4b8..0b7bb65ba 100644 --- a/aicsimageio/utils/dask_proxy.py +++ b/aicsimageio/utils/dask_proxy.py @@ -3,12 +3,24 @@ """ from __future__ import annotations -from typing import Any, Callable, ContextManager +from contextlib import nullcontext +from typing import TYPE_CHECKING, Any, Callable import dask.array as da import numpy as np from wrapt import ObjectProxy +if TYPE_CHECKING: + from typing_extensions import Protocol + + # fmt: off + class CheckableContext(Protocol): + @property + def closed(self) -> bool: ... # noqa: E704 + def __enter__(self) -> CheckableContext: ... # noqa: E704 + def __exit__(self, *a: Any) -> None: ... # noqa: E704 + # fmt: on + class DaskArrayProxy(ObjectProxy): """Dask array wrapper that provides a 'file open' context when computing. @@ -34,28 +46,26 @@ class DaskArrayProxy(ObjectProxy): __wrapped__: da.Array - def __init__(self, wrapped: da.Array, file_ctx: ContextManager) -> None: + def __init__(self, wrapped: da.Array, file_ctx: CheckableContext) -> None: super().__init__(wrapped) - self.__wrapped__._ctx_ = file_ctx + self._file_ctx = file_ctx def __getitem__(self, key: Any) -> DaskArrayProxy: - return DaskArrayProxy(self.__wrapped__.__getitem__(key), self.__wrapped__._ctx_) + return DaskArrayProxy(self.__wrapped__.__getitem__(key), self._file_ctx) def __getattr__(self, key: Any) -> Any: attr = getattr(self.__wrapped__, key) - return ( - _ArrayMethodProxy(attr, self.__wrapped__._ctx_) if callable(attr) else attr - ) + return _ArrayMethodProxy(attr, self._file_ctx) if callable(attr) else attr def __repr__(self) -> str: return repr(self.__wrapped__) def compute(self, **kwargs: Any) -> np.ndarray: - with self.__wrapped__._ctx_: + with self._file_ctx if self._file_ctx.closed else nullcontext(): # type: ignore return self.__wrapped__.compute(**kwargs) def __array__(self, dtype: str = None, **kwargs: Any) -> np.ndarray: - with self.__wrapped__._ctx_: + with self._file_ctx if self._file_ctx.closed else nullcontext(): # type: ignore return self.__wrapped__.__array__(dtype, **kwargs) @@ -63,17 +73,17 @@ class _ArrayMethodProxy: """Wraps method on a dask array and returns a DaskArrayProxy if the result of the method is a dask array. see details in DaskArrayProxy docstring.""" - def __init__(self, method: Callable, file_ctx: ContextManager) -> None: + def __init__(self, method: Callable, file_ctx: CheckableContext) -> None: self.method = method - self.file_ctx = file_ctx + self._file_ctx = file_ctx def __repr__(self) -> str: return repr(self.method) def __call__(self, *args: Any, **kwds: Any) -> Any: - with self.file_ctx: + with self._file_ctx if self._file_ctx.closed else nullcontext(): # type: ignore result = self.method(*args, **kwds) if isinstance(result, da.Array): - return DaskArrayProxy(result, self.file_ctx) + return DaskArrayProxy(result, self._file_ctx) return result