Skip to content

Commit

Permalink
Add read_nwb_method for local paths in both hdf5 and zarr (#1994)
Browse files Browse the repository at this point in the history
* add read_nwb_method

* add tests

* docstring and tutorial editions

* more docstring corrections

* CHANGELOG.md

* steph suggestions

* codespell

* Apply suggestions from code review

Co-authored-by: Steph Prince <[email protected]>

* Update src/pynwb/__init__.py

Co-authored-by: Steph Prince <[email protected]>

* fix test after Steph commit

* add io integration tests to test.py

---------

Co-authored-by: Ryan Ly <[email protected]>
Co-authored-by: Steph Prince <[email protected]>
  • Loading branch information
3 people authored Jan 23, 2025
1 parent 3d04646 commit 739ee54
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 8 additions & 6 deletions docs/gallery/general/plot_read_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://gui.dandiarchive.org/>`_,
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()
3 changes: 3 additions & 0 deletions requirements-opt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
65 changes: 65 additions & 0 deletions src/pynwb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,71 @@ def read_nwb(**kwargs):

return nwbfile

@docval({'name': 'path', 'type': (str, Path),
'doc': 'Path to the NWB file. Can be either a local filesystem path to '
'an HDF5 (.nwb) or Zarr (.zarr) file.'},
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.
See also
* :py:class:`~pynwb.NWBHDF5IO`: Core I/O class for HDF5 files with advanced options.
* :py:class:`~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
* Reads any backend (e.g. HDF5 or Zarr) if there is an IO class available.
Advanced features requiring direct use of IO classes (e.g. NWBHDF5IO NWBZarrIO) 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
Example usage reading a local NWB file:
.. code-block:: python
from pynwb import read_nwb
nwbfile = read_nwb("path/to/file.nwb")
:Returns: pynwb.NWBFile The loaded NWB file object.
"""

path = popargs('path', kwargs)
# HDF5 is always available so we try that first
backend_is_hdf5 = NWBHDF5IO.can_read(path=path)
if backend_is_hdf5:
return NWBHDF5IO.read_nwb(path=path)
else:
# If hdmf5 zarr is available we try that next
try:
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"Unable to read file: '{path}'. The file is not recognized as "
"either a valid HDF5 or Zarr NWB file. Please ensure the file exists and contains valid NWB data."
)
except ImportError:
raise ValueError(
f"Unable to read file: '{path}'. The file is not recognized as an HDF5 NWB file. "
"If you are trying to read a Zarr file, please install hdmf-zarr using: pip install hdmf-zarr"
)



from . import io as __io # noqa: F401,E402
from .core import NWBContainer, NWBData # noqa: F401,E402
from .base import TimeSeries, ProcessingModule # noqa: F401,E402
Expand Down
1 change: 1 addition & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ def run_integration_tests(verbose=True):
logging.info('all classes have integration tests')

run_test_suite("tests/integration/utils", "integration utils tests", verbose=verbose)
run_test_suite("tests/integration/io", "integration io tests", verbose=verbose)


def clean_up_tests():
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/hdf5/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Empty file.
77 changes: 77 additions & 0 deletions tests/integration/io/test_read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 suite for the read_nwb function."""

def setUp(self):
self.nwbfile = mock_NWBFile()

def test_read_nwb_hdf5(self):
"""Test reading a valid HDF5 NWB file."""
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):
"""Test reading a valid Zarr NWB file."""
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "test.zarr"
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()

def test_read_zarr_without_hdmf_zarr(self):
"""Test attempting to read a Zarr file without hdmf_zarr installed."""
if HAVE_NWBZarrIO:
self.skipTest("hdmf_zarr is installed")

with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "test.zarr"
path.mkdir() # Create empty directory to simulate Zarr store

expected_message = (
f"Unable to read file: '{path}'. The file is not recognized as an HDF5 NWB file. "
"If you are trying to read a Zarr file, please install hdmf-zarr using: pip install hdmf-zarr"
)

with self.assertRaisesWith(ValueError, expected_message):
read_nwb(path=path)

@unittest.skipIf(not HAVE_NWBZarrIO, "NWBZarrIO library not available. Need for correct error message.")
def test_read_invalid_file(self):
"""Test attempting to read a file that exists but is neither HDF5 nor Zarr."""
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "test.txt"
path.write_text("Not an NWB file")

expected_message = (
f"Unable to read file: '{path}'. The file is not recognized as either a valid HDF5 or Zarr NWB file. "
"Please ensure the file exists and contains valid NWB data."
)

with self.assertRaisesWith(ValueError, expected_message):
read_nwb(path=path)

0 comments on commit 739ee54

Please sign in to comment.