Skip to content
This repository was archived by the owner on Dec 1, 2025. It is now read-only.
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
9 changes: 4 additions & 5 deletions aicsimageio/readers/bioformats_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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()

Expand Down
19 changes: 12 additions & 7 deletions aicsimageio/tests/readers/test_bioformats_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions aicsimageio/tests/utils/test_dask_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 23 additions & 13 deletions aicsimageio/utils/dask_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -34,46 +46,44 @@ 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)


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