Skip to content

Commit

Permalink
Merge branch 'dev' into release-3.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
stephprince authored Jan 27, 2025
2 parents fc2997e + 739ee54 commit 337c914
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- ``pynwb._get_resources`` is removed.

### 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
16 changes: 9 additions & 7 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 @@ -83,7 +82,7 @@
# .. seealso::
#
# Learn about all the different ways you can download data from the DANDI Archive
# `here <https://www.dandiarchive.org/handbook/12_download/>`_
# `here <https://docs.dandiarchive.org/12_download/>`_
#
# .. seealso:: Streaming data
#
Expand All @@ -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()
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def __call__(self, filename):
'pynwb': ('https://github.com/NeurodataWithoutBorders/pynwb/%s', '%s'),
'nwb_overview': ('https://nwb-overview.readthedocs.io/en/latest/%s', '%s'),
'hdmf-docs': ('https://hdmf.readthedocs.io/en/stable/%s', '%s'),
'dandi': ('https://www.dandiarchive.org/%s', '%s'),
'dandi': ('https://dandiarchive.org/%s', '%s'),
"nwbinspector": ("https://nwbinspector.readthedocs.io/en/dev/%s", "%s"),
'hdmf-zarr': ('https://hdmf-zarr.readthedocs.io/en/stable/%s', '%s'),
}
Expand Down
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.6.18
fsspec==2024.12.0
requests==2.32.3
aiohttp==3.11.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 @@ -546,6 +546,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 @@ -265,6 +265,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 337c914

Please sign in to comment.