Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
### Fixed
- Fixed parsing of the nwb_version attribute which followed the previous suggestion to have a `NWB-` prefix.
@t-b [#2118](https://github.com/NeurodataWithoutBorders/pynwb/pull/2118)
- Fixed a performance regression introduced in pynwb 2.8.0 that affected reading NWB files with a large
number of objects or fields of objects. @rly [#2121](https://github.com/NeurodataWithoutBorders/pynwb/pull/2121)
- Fixed `load_type_config`, `unload_type_config`, and `get_loaded_type_config` acting on a copy of the global type map
instead of the global type map itself. @rly [#2121](https://github.com/NeurodataWithoutBorders/pynwb/pull/2121)

### Changed
- Added an argument `copy` to `get_type_map` to control whether a copy of the type map is returned or not.
If `copy=False`, the returned type map will be a direct reference to the global type map. @rly
[#2121](https://github.com/NeurodataWithoutBorders/pynwb/pull/2121)
- Deprecated calling `get_type_map` with the `extensions` argument. Call `load_namespaces` on the returned `TypeMap`
instead. @rly [#2121](https://github.com/NeurodataWithoutBorders/pynwb/pull/2121)

## PyNWB 3.1.1 (July 22, 2025)

Expand Down
52 changes: 31 additions & 21 deletions src/pynwb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ def load_type_config(**kwargs):
This method will either load the default config or the config provided by the path.
"""
config_path = kwargs['config_path']
type_map = kwargs['type_map'] or get_type_map()
type_map = kwargs['type_map'] or __TYPE_MAP

hdmf_load_type_config(config_path=config_path, type_map=type_map)

@docval({'name': 'type_map', 'type': TypeMap, 'doc': 'The TypeMap.', 'default': None},
is_method=False)
def get_loaded_type_config(**kwargs):
type_map = kwargs['type_map'] or get_type_map()
type_map = kwargs['type_map'] or __TYPE_MAP
return hdmf_get_loaded_type_config(type_map=type_map)

@docval({'name': 'type_map', 'type': TypeMap, 'doc': 'The TypeMap.', 'default': None},
Expand All @@ -62,7 +62,7 @@ def unload_type_config(**kwargs):
"""
Remove validation.
"""
type_map = kwargs['type_map'] or get_type_map()
type_map = kwargs['type_map'] or __TYPE_MAP
hdmf_unload_type_config(type_map=type_map)

def __get_resources() -> dict:
Expand Down Expand Up @@ -101,18 +101,28 @@ def __get_resources() -> dict:
@docval({'name': 'extensions', 'type': (str, TypeMap, list),
'doc': 'a path to a namespace, a TypeMap, or a list consisting of paths to namespaces and TypeMaps',
'default': None},
returns="TypeMap loaded for the given extension or NWB core namespace", rtype=tuple,
{
'name': 'copy', 'type': bool,
'doc': 'Whether to return a deepcopy of the TypeMap. '
'If False, a direct reference may be returned (use with caution).',
'default': True
},
returns="TypeMap loaded for the given extension or NWB core namespace", rtype=TypeMap,
is_method=False)
def get_type_map(**kwargs):
'''
Get the TypeMap for the given extensions. If no extensions are provided,
return the TypeMap for the core namespace
'''
extensions = getargs('extensions', kwargs)
extensions, copy_map = getargs('extensions', 'copy', kwargs)
type_map = None
if extensions is None:
type_map = deepcopy(__TYPE_MAP)
if copy_map:
type_map = deepcopy(__TYPE_MAP)
else:
type_map = __TYPE_MAP
else:
warn("The 'extensions' argument is deprecated and will be removed in PyNWB 4.0", DeprecationWarning)
if isinstance(extensions, TypeMap):
type_map = extensions
else:
Expand Down Expand Up @@ -538,7 +548,7 @@ def read_nwb(**kwargs):
# Retrieve the filepath
path = popargs('path', kwargs)
file = popargs('file', kwargs)

path = str(path) if path is not None else None

# Streaming case
Expand All @@ -556,18 +566,18 @@ def read_nwb(**kwargs):

return nwbfile

@docval({'name': 'path', 'type': (str, Path),
@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.'},
'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),
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
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.

Expand All @@ -585,17 +595,17 @@ def read_nwb(**kwargs):
* 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")
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)
Expand All @@ -607,18 +617,18 @@ def read_nwb(**kwargs):
from hdmf_zarr import NWBZarrIO
backend_is_zarr = NWBZarrIO.can_read(path=path)
if backend_is_zarr:
return NWBZarrIO.read_nwb(path=path)
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
Expand All @@ -642,7 +652,7 @@ def read_nwb(**kwargs):
# Functions
'get_type_map',
'get_manager',
'load_namespaces',
'load_namespaces',
'available_namespaces',
'clear_cache_dir',
'register_class',
Expand All @@ -653,11 +663,11 @@ def read_nwb(**kwargs):
'unload_type_config',
'read_nwb',
'get_nwbfile_version',

# Classes
'NWBHDF5IO',
'NWBContainer',
'NWBData',
'NWBData',
'TimeSeries',
'ProcessingModule',
'NWBFile',
Expand Down
2 changes: 1 addition & 1 deletion src/pynwb/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def _error_on_new_pass_on_construct(self, error_msg: str):
raise ValueError(error_msg)

def _get_type_map(self):
return get_type_map()
return get_type_map(copy=False)

@property
def data_type(self):
Expand Down
3 changes: 2 additions & 1 deletion tests/integration/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def create_test_extension(specs, container_classes, mappers=None):
export_spec(ns_builder, specs, output_dir.name)

# this will copy the global pynwb TypeMap and add the extension types to the copy
type_map = get_type_map(f"{output_dir.name}/{NAMESPACE_NAME}.namespace.yaml")
type_map = get_type_map()
type_map.load_namespaces(f"{output_dir.name}/{NAMESPACE_NAME}.namespace.yaml")
for type_name, container_cls in container_classes.items():
type_map.register_container_type(NAMESPACE_NAME, type_name, container_cls)
if mappers:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class TestPyNWBTypeConfig(TestCase):
def setUp(self):
if not REQUIREMENTS_INSTALLED:
self.skipTest("optional LinkML module is not installed")
CUR_DIR = os.path.dirname(os.path.realpath(__file__))
CUR_DIR = os.path.dirname(os.path.realpath(__file__))
path_to_config = os.path.join(CUR_DIR, 'test_config/test_nwb_config.yaml')
load_type_config(config_path=path_to_config)

Expand Down
15 changes: 10 additions & 5 deletions tests/unit/test_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ def test_export(self):

def test_load_namespace(self):
self.test_export()
get_type_map(extensions=os.path.join(self.tempdir, self.ns_path))
type_map = get_type_map()
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path))

def test_get_class(self):
self.test_export()
type_map = get_type_map(extensions=os.path.join(self.tempdir, self.ns_path))
type_map = get_type_map()
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path))
type_map.get_dt_container_cls('TetrodeSeries', self.prefix)

def test_load_namespace_with_reftype_attribute(self):
Expand All @@ -62,7 +64,8 @@ def test_load_namespace_with_reftype_attribute(self):
neurodata_type_def='my_new_type')
ns_builder.add_spec(self.ext_source, test_ds_ext)
ns_builder.export(self.ns_path, outdir=self.tempdir)
get_type_map(extensions=os.path.join(self.tempdir, self.ns_path))
type_map = get_type_map()
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path))

def test_load_namespace_with_reftype_attribute_check_autoclass_const(self):
ns_builder = NWBNamespaceBuilder('Extension for use in my Lab', self.prefix, version='0.1.0')
Expand All @@ -74,7 +77,8 @@ def test_load_namespace_with_reftype_attribute_check_autoclass_const(self):
neurodata_type_def='my_new_type')
ns_builder.add_spec(self.ext_source, test_ds_ext)
ns_builder.export(self.ns_path, outdir=self.tempdir)
type_map = get_type_map(extensions=os.path.join(self.tempdir, self.ns_path))
type_map = get_type_map()
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path))
my_new_type = type_map.get_dt_container_cls('my_new_type', self.prefix)
docval = None
for tmp in get_docval(my_new_type.__init__):
Expand Down Expand Up @@ -172,7 +176,8 @@ def test_catch_dup_name(self):
neurodata_type_def='TetrodeSeries')
ns_builder2.add_spec(self.ext_source2, ext2)
ns_builder2.export(self.ns_path2, outdir=self.tempdir)
type_map = get_type_map(extensions=os.path.join(self.tempdir, self.ns_path1))
type_map = get_type_map()
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path1))
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path2))

def test_catch_dup_name_core_newer(self):
Expand Down
Loading