diff --git a/CHANGELOG.md b/CHANGELOG.md index fc360b840..331ca9456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## PyNWB 3.0.0 (Upcoming) ### Enhancements and minor changes +- Added `pynwb.read_nwb` convenience method to simplify reading an NWBFile written with any backend @h-mayorquin [#1994](https://github.com/NeurodataWithoutBorders/pynwb/pull/1994) - Added support for NWB schema 2.8.0. @rly [#2001](https://github.com/NeurodataWithoutBorders/pynwb/pull/2001) - Removed `SpatialSeries.bounds` field that was not functional. This will be fixed in a future release. @rly [#1907](https://github.com/NeurodataWithoutBorders/pynwb/pull/1907), [#1996](https://github.com/NeurodataWithoutBorders/pynwb/pull/1996) - Added support for `NWBFile.was_generated_by` field. @stephprince [#1924](https://github.com/NeurodataWithoutBorders/pynwb/pull/1924) diff --git a/docs/gallery/general/plot_read_basics.py b/docs/gallery/general/plot_read_basics.py index d800adbe5..b13888ee0 100644 --- a/docs/gallery/general/plot_read_basics.py +++ b/docs/gallery/general/plot_read_basics.py @@ -30,7 +30,6 @@ import matplotlib.pyplot as plt import numpy as np -from pynwb import NWBHDF5IO #################### # We will access NWB data on the `DANDI Archive `_, @@ -103,14 +102,17 @@ # read the data into a :py:class:`~pynwb.file.NWBFile` object. filepath = "sub-P11HMH_ses-20061101_ecephys+image.nwb" -# Open the file in read mode "r", -io = NWBHDF5IO(filepath, mode="r") -nwbfile = io.read() +from pynwb import read_nwb + +nwbfile = read_nwb(filepath) nwbfile ####################################### -# :py:class:`~pynwb.NWBHDF5IO` can also be used as a context manager: +# For more advanced use cases, the :py:class:~pynwb.NWBHDF5IO class provides additional functionality. +# Below, we demonstrate how :py:class:~pynwb.NWBHDF5IO can be used as a context manager +# to read data from an NWB file in a more controlled manner: +from pynwb import NWBHDF5IO with NWBHDF5IO(filepath, mode="r") as io2: nwbfile2 = io2.read() @@ -291,4 +293,4 @@ # ----------------------- # It is good practice, especially on Windows, to close any files that you have opened. -io.close() +nwbfile.get_read_io().close() diff --git a/requirements-opt.txt b/requirements-opt.txt index fa6a18806..8934de4f0 100644 --- a/requirements-opt.txt +++ b/requirements-opt.txt @@ -6,3 +6,6 @@ oaklib==0.5.32 fsspec==2024.10.0 requests==2.32.3 aiohttp==3.10.11 + +# For read_nwb tests +hdmf-zarr diff --git a/src/pynwb/__init__.py b/src/pynwb/__init__.py index 32f9ed4d9..acd76a4e6 100644 --- a/src/pynwb/__init__.py +++ b/src/pynwb/__init__.py @@ -531,6 +531,71 @@ def read_nwb(**kwargs): return nwbfile +@docval({'name': 'path', 'type': (str, Path), 'doc': 'the path to the nwbfile'}, + is_method=False) +def read_nwb(**kwargs): + """Read an NWB file from a local path. + + High-level interface for reading NWB files. Automatically handles both HDF5 + and Zarr formats. For advanced use cases (parallel I/O, custom namespaces), + use NWBHDF5IO or NWBZarrIO. + + Parameters + ---------- + path : str or pathlib.Path + Path to the NWB file. Can be either a local filesystem path to an HDF5 (.nwb) + or Zarr (.zarr) file + + Returns + ------- + pynwb.NWBFile + The loaded NWB file object. + + See Also + -------- + pynwb.NWBHDF5IO : Core I/O class for HDF5 files with advanced options. + hdmf_zarr.nwb.NWBZarrIO : Core I/O class for Zarr files with advanced options. + + Notes + ----- + This function uses the following defaults: + * Always opens in read-only mode + * Automatically loads namespaces + * Detects file format based on extension + + Advanced features requiring direct use of IO classes include: + * Streaming data from s3 + * Custom namespace extensions + * Parallel I/O with MPI + * Custom build managers + * Write or append modes + * Pre-opened HDF5 file objects or Zarr stores + * Remote file access configuration + + Examples + -------- + Read a local NWB file: + + >>> from pynwb import read_nwb + >>> nwbfile = read_nwb("path/to/file.nwb") + + + """ + + path = popargs('path', kwargs) + backend_is_hdf5 = NWBHDF5IO.can_read(path=path) + if backend_is_hdf5: + return NWBHDF5IO.read_nwb(path=path) + else: + from hdmf_zarr import NWBZarrIO + backend_is_zarr = NWBZarrIO.can_read(path=path) + if backend_is_zarr: + return NWBZarrIO.read_nwb(path=path) + else: + raise ValueError(f"Unsupported backend for file: {path}") + + + from . import io as __io # noqa: F401,E402 from .core import NWBContainer, NWBData # noqa: F401,E402 from .base import TimeSeries, ProcessingModule # noqa: F401,E402 diff --git a/tests/integration/hdf5/test_io.py b/tests/integration/hdf5/test_io.py index 1e6ed0593..f17a7217a 100644 --- a/tests/integration/hdf5/test_io.py +++ b/tests/integration/hdf5/test_io.py @@ -613,7 +613,7 @@ def test_read_nwb_method_file(self): io.write(self.nwbfile) import h5py - + file = h5py.File(self.path, 'r') read_nwbfile = NWBHDF5IO.read_nwb(file=file) diff --git a/tests/integration/io/test_read.py b/tests/integration/io/test_read.py new file mode 100644 index 000000000..a70489d4f --- /dev/null +++ b/tests/integration/io/test_read.py @@ -0,0 +1,48 @@ +from pathlib import Path +import tempfile + +from pynwb import read_nwb +from pynwb.testing.mock.file import mock_NWBFile +from pynwb.testing import TestCase + +import unittest +try: + from hdmf_zarr import NWBZarrIO # noqa f401 + HAVE_NWBZarrIO = True +except ImportError: + HAVE_NWBZarrIO = False + + +class TestReadNWBMethod(TestCase): + """ + Test that H5DataIO functions correctly on round trip with the HDF5IO backend + """ + def setUp(self): + self.nwbfile = mock_NWBFile() + + + def test_read_nwb_hdf5(self): + from pynwb import NWBHDF5IO + + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "test.nwb" + with NWBHDF5IO(path, 'w') as io: + io.write(self.nwbfile) + + read_nwbfile = read_nwb(path=path) + self.assertContainerEqual(read_nwbfile, self.nwbfile) + read_nwbfile.get_read_io().close() + + @unittest.skipIf(not HAVE_NWBZarrIO, "NWBZarrIO library not available") + def test_read_zarr(self): + # from pynwb import NWBZarrIO + from hdmf_zarr import NWBZarrIO + + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "test.nwb" + with NWBZarrIO(path, 'w') as io: + io.write(self.nwbfile) + + read_nwbfile = read_nwb(path=path) + self.assertContainerEqual(read_nwbfile, self.nwbfile) + read_nwbfile.get_read_io().close() \ No newline at end of file