diff --git a/CHANGELOG.md b/CHANGELOG.md index 50f721cc2..79fd06efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/src/pynwb/__init__.py b/src/pynwb/__init__.py index 3dfef28ff..c39e89e04 100644 --- a/src/pynwb/__init__.py +++ b/src/pynwb/__init__.py @@ -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}, @@ -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: @@ -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: @@ -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 @@ -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. @@ -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) @@ -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 @@ -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', @@ -653,11 +663,11 @@ def read_nwb(**kwargs): 'unload_type_config', 'read_nwb', 'get_nwbfile_version', - + # Classes 'NWBHDF5IO', 'NWBContainer', - 'NWBData', + 'NWBData', 'TimeSeries', 'ProcessingModule', 'NWBFile', diff --git a/src/pynwb/core.py b/src/pynwb/core.py index fc04915ed..4994b1dff 100644 --- a/src/pynwb/core.py +++ b/src/pynwb/core.py @@ -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): diff --git a/tests/integration/helpers/utils.py b/tests/integration/helpers/utils.py index c8c9770a6..17f2dde1c 100644 --- a/tests/integration/helpers/utils.py +++ b/tests/integration/helpers/utils.py @@ -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: diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 925d57879..5ad35a0e8 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -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) diff --git a/tests/unit/test_extension.py b/tests/unit/test_extension.py index 50eb5b0c5..6f08d4b93 100644 --- a/tests/unit/test_extension.py +++ b/tests/unit/test_extension.py @@ -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): @@ -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') @@ -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__): @@ -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):