Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add read_nwb_method for local paths in both hdf5 and zarr #1994

Open
wants to merge 16 commits into
base: dev
Choose a base branch
from
Open
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
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)
stephprince marked this conversation as resolved.
Show resolved Hide resolved
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
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
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")
stephprince marked this conversation as resolved.
Show resolved Hide resolved
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)
Loading