Repository: NeurodataWithoutBorders/nwbinspector (language: python)

Issue:
[Read Helpers IIa] Add read_nwbfile with TestCase tests
Introducing the latest version of the `read_nwbfile` function as a stand-alone PR with its own unit tests; the integration of it into the full NWB Inspector workflow and subsequent integration testing +/- debugging will occur in a follow-up

I also reorganized the `tools` sub-module here to separate the division of each helper function (some of which may be able to be deprecated soon), which I will also splinter in a smaller PR (which should be merged first, or I'll just rebase this one...) to make this PR specific to only the introduction of the `read_nwbfile` function and its tests

This PR serves as an first example of the convenience functions that will and should eventually make their way up to PyNWB once confirmed to operate as intended to the best of our knowledge

Depends on [HDMF #882](https://github.com/hdmf-dev/hdmf/pull/882)



Discussion / hints:

Fix patch:
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 3ac7f948e..a0d4dd7b1 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -8,7 +8,7 @@ version: 2
 build:
   os: ubuntu-20.04
   tools:
-    python: "3.8"
+    python: "3.9"
 
 # Build documentation in the docs/ directory with Sphinx
 sphinx:
diff --git a/requirements.txt b/requirements.txt
index 44ab1431e..6a44b416e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,8 @@
 pynwb
+fsspec
+s3fs
+requests
+aiohttp
 PyYAML
 jsonschema
 packaging
diff --git a/setup.py b/setup.py
index 65146430f..0cd488467 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@
     package_dir={"": "src"},
     include_package_data=True,  # Includes files described in MANIFEST.in in the installation
     install_requires=install_requires,
-    extras_require=dict(dandi=["dandi>=0.39.2"]),
+    extras_require=dict(dandi=["dandi>=0.39.2"], zarr=["hdmf_zarr>=0.3.0"]),
     entry_points={"console_scripts": ["nwbinspector=nwbinspector.nwbinspector:inspect_all_cli"]},
     license="BSD-3-Clause",
     classifiers=[
diff --git a/src/nwbinspector/tools/__init__.py b/src/nwbinspector/tools/__init__.py
index a13c67c0f..3521f5a37 100644
--- a/src/nwbinspector/tools/__init__.py
+++ b/src/nwbinspector/tools/__init__.py
@@ -1,3 +1,4 @@
 from .dandi import get_s3_urls_and_dandi_paths
 from .nwb import all_of_type, get_nwbfile_path_from_internal_object
+from ._read_nwbfile import read_nwbfile
 from ..testing import check_streaming_enabled, make_minimal_nwbfile  # To maintain back-compatability
diff --git a/src/nwbinspector/tools/_read_nwbfile.py b/src/nwbinspector/tools/_read_nwbfile.py
new file mode 100644
index 000000000..2a5e12e20
--- /dev/null
+++ b/src/nwbinspector/tools/_read_nwbfile.py
@@ -0,0 +1,132 @@
+"""Temporary module for thorough testing and evaluation of the proposed `read_nwbfile` helper function."""
+from pathlib import Path
+from warnings import filterwarnings
+from typing import Optional, Literal, Union
+
+import h5py
+from pynwb import NWBHDF5IO, NWBFile
+
+_BACKEND_IO_CLASSES = dict(hdf5=NWBHDF5IO)
+
+try:
+    from hdmf_zarr import NWBZarrIO
+
+    _BACKEND_IO_CLASSES.update(zarr=NWBZarrIO)
+except ModuleNotFoundError as exception:
+    if str(exception) != "No module named 'hdmf_zarr'":  # not the exception we're looking for, so re-raise
+        raise exception
+
+
+def _get_method(path: str):
+    if path.startswith(("https://", "http://", "s3://")):
+        return "fsspec"
+    elif Path(path).exists():
+        return "local"
+    else:
+        raise ValueError(
+            f"Unable to automatically determine method. Path {path} does not appear to be a URL and is not a file on "
+            f"the local filesystem."
+        )
+
+
+def _init_fsspec(path):
+    import fsspec
+
+    if path.startswith(("https://", "http://")):
+        return fsspec.filesystem("http")
+    elif path.startswith("s3://"):
+        return fsspec.filesystem("s3", anon=True)
+
+
+def _get_backend(path: str, method: Literal["local", "fsspec", "ros3"]):
+    if method == "ros3":
+        return "hdf5"
+
+    possible_backends = []
+    if method == "fsspec":
+        fs = _init_fsspec(path=path)
+        with fs.open(path=path, mode="rb") as file:
+            for backend_name, backend_class in _BACKEND_IO_CLASSES.items():
+                if backend_class.can_read(path=file):
+                    possible_backends.append(backend_name)
+    else:
+        for backend_name, backend_class in _BACKEND_IO_CLASSES.items():
+            if backend_class.can_read(path):
+                possible_backends.append(backend_name)
+
+    if len(possible_backends) > 1:  # pragma: no cover
+        raise ValueError(
+            f"More than one possible backend found - please choose from the following: {possible_backends}"
+        )
+    elif len(possible_backends) == 0:
+        raise ValueError("No compatible backend found.")
+
+    return possible_backends[0]
+
+
+def read_nwbfile(
+    nwbfile_path: Union[str, Path],
+    method: Optional[Literal["local", "fsspec", "ros3"]] = None,
+    backend: Optional[Literal["hdf5", "zarr"]] = None,
+) -> NWBFile:
+    """
+    Read an NWB file using the specified (or auto-detected) method and specified (or auto-detected) backend.
+
+    Parameters
+    ----------
+    nwbfile_path : str or pathlib.Path
+        Path to the file on your system.
+    method : "local", "fsspec", "ros3", or None (default)
+        Where to read the file from; a local disk drive or steaming from an https:// or s3:// path.
+        The default auto-detects based on the form of the path.
+        When streaming, the default method is "fsspec".
+        Note that "ros3" is specific to HDF5 backend files.
+    backend : "hdf5", "zarr", or None (default)
+        Type of backend used to write the file.
+        The default auto-detects the type of the file.
+
+    Returns
+    -------
+    pynwb.NWBFile
+    """
+    nwbfile_path = str(nwbfile_path)  # If pathlib.Path, cast to str; if already str, no harm done
+
+    method = method or _get_method(nwbfile_path)
+    if method != "local" and Path(nwbfile_path).exists():
+        raise ValueError(
+            f"The file ({nwbfile_path}) is a local path on your system, but the method ({method}) was selected! "
+            "Please set method='local'."
+        )
+    if method == "local" and any(protocol in nwbfile_path for protocol in ["s3://", "https://"]):
+        raise ValueError(
+            f"The path ({nwbfile_path}) is an external URL, but the method (local) was selected! "
+            "Please set method='fsspec' or 'ros3' (for HDF5 only)."
+        )
+    if method == "ros3" and nwbfile_path.startswith("s3://"):
+        raise ValueError(
+            "The ROS3 method was selected, but the URL starts with 's3://'! Please switch to an 'https://' URL."
+        )
+
+    backend = backend or _get_backend(nwbfile_path, method)
+    if method == "local" and not _BACKEND_IO_CLASSES[  # Temporary until .can_read() is able to work on streamed bytes
+        backend
+    ].can_read(path=nwbfile_path):
+        raise IOError(f"The chosen backend ({backend}) is unable to read the file! Please select a different backend.")
+
+    # Filter out some of most common warnings that don't really matter with `load_namespaces=True`
+    filterwarnings(action="ignore", message="No cached namespaces found in .*")
+    filterwarnings(action="ignore", message="Ignoring cached namespace .*")
+    io_kwargs = dict(mode="r", load_namespaces=True)
+    if method == "fsspec":
+        fs = _init_fsspec(nwbfile_path)
+        f = fs.open(nwbfile_path, "rb")
+        file = h5py.File(f)
+        io_kwargs.update(file=file)
+    else:
+        io_kwargs.update(path=nwbfile_path)
+    if method == "ros3":
+        io_kwargs.update(driver="ros3")
+    io = _BACKEND_IO_CLASSES[backend](**io_kwargs)
+    nwbfile = io.read()
+
+    return nwbfile


Test patch:
diff --git a/.github/workflows/deploy-tests.yml b/.github/workflows/deploy-tests.yml
index 9dbbad998..c04bf78db 100644
--- a/.github/workflows/deploy-tests.yml
+++ b/.github/workflows/deploy-tests.yml
@@ -19,12 +19,12 @@ jobs:
   run-tests:
     needs: assess-file-changes
     if: ${{ needs.assess-file-changes.outputs.SOURCE_CHANGED == 'true' }}
-    uses: neurodatawithoutborders/nwbinspector/.github/workflows/testing.yml@dev
+    uses: neurodatawithoutborders/nwbinspector/.github/workflows/testing.yml@add_read_nwbfile_fixtures
 
   run-streaming-tests:
     needs: assess-file-changes
     if: ${{ needs.assess-file-changes.outputs.SOURCE_CHANGED == 'true' }}
-    uses: neurodatawithoutborders/nwbinspector/.github/workflows/streaming-tests.yml@dev
+    uses: neurodatawithoutborders/nwbinspector/.github/workflows/streaming-tests.yml@add_read_nwbfile_fixtures
 
   run-past-gallery:
     needs: assess-file-changes
@@ -46,13 +46,6 @@ jobs:
     if: ${{ needs.assess-file-changes.outputs.SOURCE_CHANGED == 'true' }}
     uses: neurodatawithoutborders/nwbinspector/.github/workflows/dandi-dev.yml@dev
 
-  update-testing-files:
-    needs: assess-file-changes
-    if: ${{ needs.assess-file-changes.outputs.TESTING_CHANGED == 'true' }}
-    uses: neurodatawithoutborders/nwbinspector/.github/workflows/update-testing-files.yml@dev
-    secrets:
-      DANDI_API_KEY: ${{ secrets.DANDI_API_KEY }}
-
   check-final-status:
     name: All tests passing
     if: always()
diff --git a/.github/workflows/streaming-tests.yml b/.github/workflows/streaming-tests.yml
index 32d470954..8e2a57d9a 100644
--- a/.github/workflows/streaming-tests.yml
+++ b/.github/workflows/streaming-tests.yml
@@ -26,6 +26,10 @@ jobs:
         run: |
           pip install -e .
           pip install dandi
+      - name: Install HDMF-zarr # temporary
+        run: pip install hdmf-zarr
+      - name: Install latest HDMF # temporary
+        run: pip install -U hdmf
       - name: Uninstall h5py
         run: pip uninstall -y h5py
       - name: Install ROS3
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 324563ac1..e84344896 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -36,9 +36,15 @@ jobs:
         run: pip uninstall -y h5py
       - name: Install ROS3
         run: conda install -c conda-forge h5py
+      - name: Install HDMF-zarr # temporary
+        run: pip install hdmf-zarr
+      - name: Install latest HDMF # temporary
+        run: pip install -U hdmf
 
       - name: Run pytest with coverage
-        run: pytest -rsx --cov=./ --cov-report xml:./nwbinspector/nwbinspector/coverage.xml
+        run: |
+          pytest -rsx --cov=./ --cov-report xml:./nwbinspector/nwbinspector/coverage.xml
+          pytest tests/ -rsx tests/read_nwbfile_tests.py  # TODO when read_nwbfile is ported
       - if: ${{ matrix.python-version == '3.10' && matrix.os == 'ubuntu-latest' }}
         name: Upload coverage to Codecov
         uses: codecov/codecov-action@v3
diff --git a/src/nwbinspector/testing.py b/src/nwbinspector/testing.py
index dc23cbfb8..2bb2ec7cc 100644
--- a/src/nwbinspector/testing.py
+++ b/src/nwbinspector/testing.py
@@ -7,15 +7,13 @@
 from datetime import datetime
 from typing import Tuple, Optional
 from urllib import request
-from tempfile import mkdtemp
-from shutil import rmtree
-from warnings import warn
 
 import h5py
+from hdmf.backends.hdf5 import HDF5IO
+from hdmf.backends.io import HDMFIO
 from packaging.version import Version
 from pynwb import NWBHDF5IO, NWBFile
 from pynwb.image import ImageSeries
-from hdmf.testing import TestCase as HDMFTestCase
 
 from .utils import is_module_installed, get_package_version
 
@@ -150,3 +148,15 @@ def check_streaming_enabled() -> Tuple[bool, Optional[str]]:
     if "ros3" not in h5py.registered_drivers():
         return False, "ROS3 driver not installed."
     return True, None
+
+
+def check_hdf5_io_open(io: HDF5IO):
+    """Check if an h5py.File object is open by using the file object's .id attribute, which is invalid when the file is closed."""
+    return io._file.id.valid
+
+
+def check_zarr_io_open(io: HDMFIO):
+    """For Zarr, the private attribute `_ZarrIO__file` is set to a `zarr.group` on open."""
+    import zarr  # relative import since extra requirement
+
+    return isinstance(io._ZarrIO__file, zarr.Group)
diff --git a/tests/read_nwbfile_tests.py b/tests/read_nwbfile_tests.py
new file mode 100644
index 000000000..28fca9c3b
--- /dev/null
+++ b/tests/read_nwbfile_tests.py
@@ -0,0 +1,163 @@
+"""Temporary tests for thorough testing and evaluation of the proposed `read_nwbfile` helper function."""
+from pathlib import Path
+
+import pytest
+from hdmf_zarr import NWBZarrIO
+from pynwb import NWBHDF5IO
+from pynwb.testing.mock.file import mock_NWBFile
+from pynwb.testing.mock.base import mock_TimeSeries
+
+from nwbinspector.tools import read_nwbfile
+from nwbinspector.testing import (
+    check_streaming_tests_enabled,
+    check_hdf5_io_open,
+    check_zarr_io_open,
+)
+
+STREAMING_TESTS_ENABLED, DISABLED_STREAMING_TESTS_REASON = check_streaming_tests_enabled()
+
+
+@pytest.fixture(scope="session")
+def hdf5_nwbfile_path(tmpdir_factory):
+    nwbfile_path = tmpdir_factory.mktemp("data").join("test_read_nwbfile_hdf5.nwb")
+    if not Path(nwbfile_path).exists():
+        nwbfile = mock_NWBFile()
+        nwbfile.add_acquisition(mock_TimeSeries())
+        with NWBHDF5IO(path=str(nwbfile_path), mode="w") as io:
+            io.write(nwbfile)
+    return nwbfile_path
+
+
+@pytest.fixture(scope="session")
+def zarr_nwbfile_path(tmpdir_factory):
+    nwbfile_path = tmpdir_factory.mktemp("data").join("test_read_nwbfile_zarr.nwb")
+    if not Path(nwbfile_path).exists():
+        nwbfile = mock_NWBFile()
+        nwbfile.add_acquisition(mock_TimeSeries())
+        with NWBZarrIO(path=str(nwbfile_path), mode="w") as io:
+            io.write(nwbfile)
+    return nwbfile_path
+
+
+# Assertion tests
+def test_incorrect_backend_set_on_hdf5(hdf5_nwbfile_path):
+    with pytest.raises(IOError) as excinfo:
+        read_nwbfile(nwbfile_path=hdf5_nwbfile_path, backend="zarr")
+    assert "The chosen backend (zarr) is unable to read the file! Please select a different backend." in str(
+        excinfo.value
+    )
+
+
+def test_incorrect_backend_set_on_zarr(zarr_nwbfile_path):
+    with pytest.raises(IOError) as excinfo:
+        read_nwbfile(nwbfile_path=zarr_nwbfile_path, backend="hdf5")
+    assert "The chosen backend (hdf5) is unable to read the file! Please select a different backend." in str(
+        excinfo.value
+    )
+
+
+def test_incorrect_method_set_on_hdf5(hdf5_nwbfile_path):
+    with pytest.raises(ValueError) as excinfo:
+        read_nwbfile(nwbfile_path=hdf5_nwbfile_path, method="fsspec")
+    assert (
+        f"The file ({hdf5_nwbfile_path}) is a local path on your system, but the method (fsspec) was selected! "
+        "Please set method='local'."
+    ) in str(excinfo.value)
+
+
+def test_incorrect_method_set_on_remote_hdf5():
+    nwbfile_path = (
+        "https://dandi-api-staging-dandisets.s3.amazonaws.com/blobs/6a6/1ba/6a61bab5-0662-49e5-be46-0b9ee9a27297"
+    )
+
+    with pytest.raises(ValueError) as excinfo:
+        read_nwbfile(
+            nwbfile_path=nwbfile_path,
+            backend="hdf5",  # Currently unable to auto-determine backend with https
+            method="local",
+        )
+    assert (
+        f"The path ({nwbfile_path}) is an external URL, but the method (local) was selected! "
+        "Please set method='fsspec' or 'ros3' (for HDF5 only)."
+    ) in str(excinfo.value)
+
+
+def test_s3_url_set_on_ros3_hdf5():
+    nwbfile_path = "s3://dandi-api-staging-dandisets/blobs/80d/80f/80d80f55-f8a1-4318-b17e-ce55f4dd2620"
+    with pytest.raises(ValueError) as excinfo:
+        read_nwbfile(
+            nwbfile_path=nwbfile_path,
+            backend="hdf5",
+            method="ros3",
+        )
+    assert "The ROS3 method was selected, but the URL starts with 's3://'! Please switch to an 'https://' URL." in str(
+        excinfo.value
+    )
+
+
+# HDF5 tests
+def test_hdf5_explicit_closure(hdf5_nwbfile_path):
+    nwbfile = read_nwbfile(nwbfile_path=hdf5_nwbfile_path)
+    assert check_hdf5_io_open(io=nwbfile.read_io)
+
+    nwbfile.read_io.close()
+    assert not check_hdf5_io_open(io=nwbfile.read_io)
+
+
+def test_hdf5_object_deletion_does_not_close_io(hdf5_nwbfile_path):
+    """Deleting the `nwbfile` object should not trigger `io.close` if `io` is still being referenced independently."""
+    nwbfile = read_nwbfile(nwbfile_path=hdf5_nwbfile_path)
+    io = nwbfile.read_io  # keep reference in namespace so it persists after nwbfile deletion
+    assert check_hdf5_io_open(io=io)
+
+    del nwbfile
+    assert check_hdf5_io_open(io=io)
+
+
+def test_hdf5_object_replacement_does_not_close_io(hdf5_nwbfile_path):
+    """Deleting the `nwbfile` object should not trigger `io.close` if `io` is still being referenced independently."""
+    nwbfile_1 = read_nwbfile(nwbfile_path=hdf5_nwbfile_path)
+    io_1 = nwbfile_1.read_io
+    assert check_hdf5_io_open(io=io_1)
+
+    nwbfile_2 = read_nwbfile(nwbfile_path=hdf5_nwbfile_path)
+    io_2 = nwbfile_2.read_io
+    assert check_hdf5_io_open(io=io_2)
+
+    nwbfile_2 = nwbfile_1
+    assert check_hdf5_io_open(io=io_1)
+    assert check_hdf5_io_open(io=io_2)
+
+
+# Zarr tests
+def test_zarr_explicit_closure(zarr_nwbfile_path):
+    nwbfile = read_nwbfile(nwbfile_path=zarr_nwbfile_path)
+    assert check_zarr_io_open(io=nwbfile.read_io)
+
+    nwbfile.read_io.close()
+    assert not check_zarr_io_open(io=nwbfile.read_io)
+
+
+def test_zarr_object_deletion_does_not_close_io(zarr_nwbfile_path):
+    """Deleting the `nwbfile` object should not trigger `io.close` if `io` is still being referenced independently."""
+    nwbfile = read_nwbfile(nwbfile_path=zarr_nwbfile_path)
+    io = nwbfile.read_io  # keep reference in namespace so it persists after nwbfile deletion
+    assert check_zarr_io_open(io=io)
+
+    del nwbfile
+    assert check_zarr_io_open(io=io)
+
+
+def test_zarr_object_replacement_does_not_close_io(zarr_nwbfile_path):
+    """Deleting the `nwbfile` object should not trigger `io.close` if `io` is still being referenced independently."""
+    nwbfile_1 = read_nwbfile(nwbfile_path=zarr_nwbfile_path)
+    io_1 = nwbfile_1.read_io
+    assert check_zarr_io_open(io=io_1)
+
+    nwbfile_2 = read_nwbfile(nwbfile_path=zarr_nwbfile_path)
+    io_2 = nwbfile_2.read_io
+    assert check_zarr_io_open(io=io_2)
+
+    nwbfile_2 = nwbfile_1
+    assert check_zarr_io_open(io=io_1)
+    assert check_zarr_io_open(io=io_2)
diff --git a/tests/streaming_tests.py b/tests/streaming_tests.py
index af655e3f7..9f5bce0b3 100644
--- a/tests/streaming_tests.py
+++ b/tests/streaming_tests.py
@@ -1,3 +1,4 @@
+"""All tests that specifically require streaming to be enabled (i.e., ROS3 version of h5py, fsspec, etc.)."""
 import os
 import pytest
 from shutil import rmtree
@@ -8,10 +9,22 @@
 from nwbinspector import Importance
 from nwbinspector import inspect_all
 from nwbinspector.register_checks import InspectorMessage
-from nwbinspector.testing import check_streaming_tests_enabled
+from nwbinspector.tools import read_nwbfile
+from nwbinspector.testing import check_streaming_tests_enabled, check_hdf5_io_open
 from nwbinspector.utils import FilePathType
 
+
 STREAMING_TESTS_ENABLED, DISABLED_STREAMING_TESTS_REASON = check_streaming_tests_enabled()
+PERSISTENT_READ_NWBFILE_HDF5_EXAMPLE_HTTPS = (
+    "https://dandi-api-staging-dandisets.s3.amazonaws.com/blobs/80d/80f/80d80f55-f8a1-4318-b17e-ce55f4dd2620"
+)
+PERSISTENT_READ_NWBFILE_HDF5_EXAMPLE_S3 = (
+    "s3://dandi-api-staging-dandisets/blobs/80d/80f/80d80f55-f8a1-4318-b17e-ce55f4dd2620"
+)
+PERSISTENT_READ_NWBFILE_ZARR_EXAMPLE_HTTPS = (
+    "https://dandi-api-staging-dandisets.s3.amazonaws.com/zarr/63f06140-8d0c-4db4-81cc-812ed4e4db03"
+)
+PERSISTENT_READ_NWBFILE_ZARR_EXAMPLE_S3 = "s3://dandi-api-staging-dandisets/zarr/63f06140-8d0c-4db4-81cc-812ed4e4db03"
 
 
 @pytest.mark.skipif(not STREAMING_TESTS_ENABLED, reason=DISABLED_STREAMING_TESTS_REASON or "")
@@ -73,3 +86,74 @@ def test_dandiset_streaming_cli_parallel(self):
             f"> {console_output_file}"
         )
         self.assertFileExists(path=self.tempdir / "test_nwbinspector_streaming_report_7.txt")
+
+
+# These will move to PyNWB when the time is right
+@pytest.mark.skipif(not STREAMING_TESTS_ENABLED, reason=DISABLED_STREAMING_TESTS_REASON or "")
+def test_hdf5_fsspec_https():
+    nwbfile = read_nwbfile(
+        nwbfile_path=PERSISTENT_READ_NWBFILE_HDF5_EXAMPLE_HTTPS,
+        backend="hdf5",  # TODO: cannot current auto-detect backend when streaming
+        method="fsspec",
+    )
+    assert check_hdf5_io_open(io=nwbfile.read_io)
+
+    nwbfile.read_io.close()
+    assert not check_hdf5_io_open(io=nwbfile.read_io)
+
+
+@pytest.mark.skipif(not STREAMING_TESTS_ENABLED, reason=DISABLED_STREAMING_TESTS_REASON or "")
+def test_hdf5_fsspec_s3():
+    nwbfile = read_nwbfile(
+        nwbfile_path=PERSISTENT_READ_NWBFILE_HDF5_EXAMPLE_S3,
+        backend="hdf5",  # TODO: cannot current auto-detect backend when streaming
+        method="fsspec",
+    )
+    assert check_hdf5_io_open(io=nwbfile.read_io)
+
+    nwbfile.read_io.close()
+    assert not check_hdf5_io_open(io=nwbfile.read_io)
+
+
+@pytest.mark.skipif(not STREAMING_TESTS_ENABLED, reason=DISABLED_STREAMING_TESTS_REASON or "")
+def test_hdf5_ros3_https():
+    nwbfile = read_nwbfile(
+        nwbfile_path=PERSISTENT_READ_NWBFILE_HDF5_EXAMPLE_HTTPS,
+        backend="hdf5",  # TODO: cannot current auto-detect backend when streaming
+        method="ros3",
+    )
+    assert check_hdf5_io_open(io=nwbfile.read_io)
+
+    nwbfile.read_io.close()
+    assert not check_hdf5_io_open(io=nwbfile.read_io)
+
+
+# Zarr files not working yet with streaming
+
+# @pytest.mark.skipif(not STREAMING_TESTS_ENABLED, reason=DISABLED_STREAMING_TESTS_REASON or "")
+# def test_zarr_fsspec_streaming_https():
+#     nwbfile = read_nwbfile(
+#         nwbfile_path=PERSISTENT_READ_NWBFILE_ZARR_EXAMPLE_HTTPS,
+#         method="fsspec",
+#     )
+#     assert check_zarr_io_open(io=nwbfile.read_io)
+
+#     nwbfile.read_io.close()
+#     assert not check_zarr_io_open(io=nwbfile.read_io)
+
+
+# @pytest.mark.skipif(not STREAMING_TESTS_ENABLED, reason=DISABLED_STREAMING_TESTS_REASON or "")
+# def test_zarr_fsspec_streaming_s3():
+#     nwbfile = read_nwbfile(
+#         nwbfile_path=PERSISTENT_READ_NWBFILE_ZARR_EXAMPLE_S3,
+#         method="fsspec",
+#     )
+#     assert check_zarr_io_open(io=nwbfile.read_io)
+
+#     nwbfile.read_io.close()
+#     assert not check_zarr_io_open(io=nwbfile.read_io)
+
+
+# @pytest.mark.skipif(not STREAMING_TESTS_ENABLED, reason=DISABLED_STREAMING_TESTS_REASON or "")
+# def test_zarr_ros3_error():
+#     test that an error is raised with ros3 driver on zarr (not applicable)