Skip to content

Commit 739ee54

Browse files
h-mayorquinrlystephprince
authored
Add read_nwb_method for local paths in both hdf5 and zarr (#1994)
* 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]>
1 parent 3d04646 commit 739ee54

File tree

8 files changed

+156
-7
lines changed

8 files changed

+156
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## PyNWB 3.0.0 (Upcoming)
44

55
### Enhancements and minor changes
6+
- 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)
67
- Added support for NWB schema 2.8.0. @rly [#2001](https://github.com/NeurodataWithoutBorders/pynwb/pull/2001)
78
- 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)
89
- Added support for `NWBFile.was_generated_by` field. @stephprince [#1924](https://github.com/NeurodataWithoutBorders/pynwb/pull/1924)

docs/gallery/general/plot_read_basics.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import matplotlib.pyplot as plt
3131
import numpy as np
3232

33-
from pynwb import NWBHDF5IO
3433

3534
####################
3635
# We will access NWB data on the `DANDI Archive <https://gui.dandiarchive.org/>`_,
@@ -103,14 +102,17 @@
103102
# read the data into a :py:class:`~pynwb.file.NWBFile` object.
104103

105104
filepath = "sub-P11HMH_ses-20061101_ecephys+image.nwb"
106-
# Open the file in read mode "r",
107-
io = NWBHDF5IO(filepath, mode="r")
108-
nwbfile = io.read()
105+
from pynwb import read_nwb
106+
107+
nwbfile = read_nwb(filepath)
109108
nwbfile
110109

111110
#######################################
112-
# :py:class:`~pynwb.NWBHDF5IO` can also be used as a context manager:
111+
# For more advanced use cases, the :py:class:~pynwb.NWBHDF5IO class provides additional functionality.
112+
# Below, we demonstrate how :py:class:~pynwb.NWBHDF5IO can be used as a context manager
113+
# to read data from an NWB file in a more controlled manner:
113114

115+
from pynwb import NWBHDF5IO
114116
with NWBHDF5IO(filepath, mode="r") as io2:
115117
nwbfile2 = io2.read()
116118

@@ -291,4 +293,4 @@
291293
# -----------------------
292294
# It is good practice, especially on Windows, to close any files that you have opened.
293295

294-
io.close()
296+
nwbfile.get_read_io().close()

requirements-opt.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ oaklib==0.5.32
66
fsspec==2024.10.0
77
requests==2.32.3
88
aiohttp==3.10.11
9+
10+
# For read_nwb tests
11+
hdmf-zarr

src/pynwb/__init__.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,71 @@ def read_nwb(**kwargs):
531531

532532
return nwbfile
533533

534+
@docval({'name': 'path', 'type': (str, Path),
535+
'doc': 'Path to the NWB file. Can be either a local filesystem path to '
536+
'an HDF5 (.nwb) or Zarr (.zarr) file.'},
537+
is_method=False)
538+
def read_nwb(**kwargs):
539+
"""Read an NWB file from a local path.
540+
541+
High-level interface for reading NWB files. Automatically handles both HDF5
542+
and Zarr formats. For advanced use cases (parallel I/O, custom namespaces),
543+
use NWBHDF5IO or NWBZarrIO.
544+
545+
See also
546+
* :py:class:`~pynwb.NWBHDF5IO`: Core I/O class for HDF5 files with advanced options.
547+
* :py:class:`~hdmf_zarr.nwb.NWBZarrIO`: Core I/O class for Zarr files with advanced options.
548+
549+
Notes
550+
This function uses the following defaults:
551+
* Always opens in read-only mode
552+
* Automatically loads namespaces
553+
* Reads any backend (e.g. HDF5 or Zarr) if there is an IO class available.
554+
555+
Advanced features requiring direct use of IO classes (e.g. NWBHDF5IO NWBZarrIO) include:
556+
* Streaming data from s3
557+
* Custom namespace extensions
558+
* Parallel I/O with MPI
559+
* Custom build managers
560+
* Write or append modes
561+
* Pre-opened HDF5 file objects or Zarr stores
562+
* Remote file access configuration
563+
564+
Example usage reading a local NWB file:
565+
566+
.. code-block:: python
567+
568+
from pynwb import read_nwb
569+
nwbfile = read_nwb("path/to/file.nwb")
570+
571+
:Returns: pynwb.NWBFile The loaded NWB file object.
572+
"""
573+
574+
path = popargs('path', kwargs)
575+
# HDF5 is always available so we try that first
576+
backend_is_hdf5 = NWBHDF5IO.can_read(path=path)
577+
if backend_is_hdf5:
578+
return NWBHDF5IO.read_nwb(path=path)
579+
else:
580+
# If hdmf5 zarr is available we try that next
581+
try:
582+
from hdmf_zarr import NWBZarrIO
583+
backend_is_zarr = NWBZarrIO.can_read(path=path)
584+
if backend_is_zarr:
585+
return NWBZarrIO.read_nwb(path=path)
586+
else:
587+
raise ValueError(
588+
f"Unable to read file: '{path}'. The file is not recognized as "
589+
"either a valid HDF5 or Zarr NWB file. Please ensure the file exists and contains valid NWB data."
590+
)
591+
except ImportError:
592+
raise ValueError(
593+
f"Unable to read file: '{path}'. The file is not recognized as an HDF5 NWB file. "
594+
"If you are trying to read a Zarr file, please install hdmf-zarr using: pip install hdmf-zarr"
595+
)
596+
597+
598+
534599
from . import io as __io # noqa: F401,E402
535600
from .core import NWBContainer, NWBData # noqa: F401,E402
536601
from .base import TimeSeries, ProcessingModule # noqa: F401,E402

test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ def run_integration_tests(verbose=True):
264264
logging.info('all classes have integration tests')
265265

266266
run_test_suite("tests/integration/utils", "integration utils tests", verbose=verbose)
267+
run_test_suite("tests/integration/io", "integration io tests", verbose=verbose)
267268

268269

269270
def clean_up_tests():

tests/integration/hdf5/test_io.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ def test_read_nwb_method_file(self):
613613
io.write(self.nwbfile)
614614

615615
import h5py
616-
616+
617617
file = h5py.File(self.path, 'r')
618618

619619
read_nwbfile = NWBHDF5IO.read_nwb(file=file)

tests/integration/io/__init__.py

Whitespace-only changes.

tests/integration/io/test_read.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from pathlib import Path
2+
import tempfile
3+
4+
from pynwb import read_nwb
5+
from pynwb.testing.mock.file import mock_NWBFile
6+
from pynwb.testing import TestCase
7+
8+
import unittest
9+
try:
10+
from hdmf_zarr import NWBZarrIO # noqa f401
11+
HAVE_NWBZarrIO = True
12+
except ImportError:
13+
HAVE_NWBZarrIO = False
14+
15+
16+
class TestReadNWBMethod(TestCase):
17+
"""Test suite for the read_nwb function."""
18+
19+
def setUp(self):
20+
self.nwbfile = mock_NWBFile()
21+
22+
def test_read_nwb_hdf5(self):
23+
"""Test reading a valid HDF5 NWB file."""
24+
from pynwb import NWBHDF5IO
25+
26+
with tempfile.TemporaryDirectory() as temp_dir:
27+
path = Path(temp_dir) / "test.nwb"
28+
with NWBHDF5IO(path, 'w') as io:
29+
io.write(self.nwbfile)
30+
31+
read_nwbfile = read_nwb(path=path)
32+
self.assertContainerEqual(read_nwbfile, self.nwbfile)
33+
read_nwbfile.get_read_io().close()
34+
35+
@unittest.skipIf(not HAVE_NWBZarrIO, "NWBZarrIO library not available")
36+
def test_read_zarr(self):
37+
"""Test reading a valid Zarr NWB file."""
38+
with tempfile.TemporaryDirectory() as temp_dir:
39+
path = Path(temp_dir) / "test.zarr"
40+
with NWBZarrIO(path, 'w') as io:
41+
io.write(self.nwbfile)
42+
43+
read_nwbfile = read_nwb(path=path)
44+
self.assertContainerEqual(read_nwbfile, self.nwbfile)
45+
read_nwbfile.get_read_io().close()
46+
47+
def test_read_zarr_without_hdmf_zarr(self):
48+
"""Test attempting to read a Zarr file without hdmf_zarr installed."""
49+
if HAVE_NWBZarrIO:
50+
self.skipTest("hdmf_zarr is installed")
51+
52+
with tempfile.TemporaryDirectory() as temp_dir:
53+
path = Path(temp_dir) / "test.zarr"
54+
path.mkdir() # Create empty directory to simulate Zarr store
55+
56+
expected_message = (
57+
f"Unable to read file: '{path}'. The file is not recognized as an HDF5 NWB file. "
58+
"If you are trying to read a Zarr file, please install hdmf-zarr using: pip install hdmf-zarr"
59+
)
60+
61+
with self.assertRaisesWith(ValueError, expected_message):
62+
read_nwb(path=path)
63+
64+
@unittest.skipIf(not HAVE_NWBZarrIO, "NWBZarrIO library not available. Need for correct error message.")
65+
def test_read_invalid_file(self):
66+
"""Test attempting to read a file that exists but is neither HDF5 nor Zarr."""
67+
with tempfile.TemporaryDirectory() as temp_dir:
68+
path = Path(temp_dir) / "test.txt"
69+
path.write_text("Not an NWB file")
70+
71+
expected_message = (
72+
f"Unable to read file: '{path}'. The file is not recognized as either a valid HDF5 or Zarr NWB file. "
73+
"Please ensure the file exists and contains valid NWB data."
74+
)
75+
76+
with self.assertRaisesWith(ValueError, expected_message):
77+
read_nwb(path=path)

0 commit comments

Comments
 (0)