Skip to content

Commit 9e06719

Browse files
rlyCopilot
andauthored
Remove unnecessary calls to deepcopy(get_type_map) (#2121)
* Remove unnecessary calls to deepcopy(get_type_map) * Cache type map in container cls, use global map for type configs * Caching doesn't work that way. Refactor with copy arg in get_type_map * Discard changes to src/pynwb/legacy/__init__.py * Update changelog * Update src/pynwb/__init__.py Co-authored-by: Copilot <[email protected]> * Fixed rtype value in get_type_map --------- Co-authored-by: Copilot <[email protected]>
1 parent f9cd2a2 commit 9e06719

File tree

6 files changed

+56
-29
lines changed

6 files changed

+56
-29
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@
55
### Fixed
66
- Fixed parsing of the nwb_version attribute which followed the previous suggestion to have a `NWB-` prefix.
77
@t-b [#2118](https://github.com/NeurodataWithoutBorders/pynwb/pull/2118)
8+
- Fixed a performance regression introduced in pynwb 2.8.0 that affected reading NWB files with a large
9+
number of objects or fields of objects. @rly [#2121](https://github.com/NeurodataWithoutBorders/pynwb/pull/2121)
10+
- Fixed `load_type_config`, `unload_type_config`, and `get_loaded_type_config` acting on a copy of the global type map
11+
instead of the global type map itself. @rly [#2121](https://github.com/NeurodataWithoutBorders/pynwb/pull/2121)
12+
13+
### Changed
14+
- Added an argument `copy` to `get_type_map` to control whether a copy of the type map is returned or not.
15+
If `copy=False`, the returned type map will be a direct reference to the global type map. @rly
16+
[#2121](https://github.com/NeurodataWithoutBorders/pynwb/pull/2121)
17+
- Deprecated calling `get_type_map` with the `extensions` argument. Call `load_namespaces` on the returned `TypeMap`
18+
instead. @rly [#2121](https://github.com/NeurodataWithoutBorders/pynwb/pull/2121)
819

920
## PyNWB 3.1.1 (July 22, 2025)
1021

src/pynwb/__init__.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ def load_type_config(**kwargs):
4646
This method will either load the default config or the config provided by the path.
4747
"""
4848
config_path = kwargs['config_path']
49-
type_map = kwargs['type_map'] or get_type_map()
49+
type_map = kwargs['type_map'] or __TYPE_MAP
5050

5151
hdmf_load_type_config(config_path=config_path, type_map=type_map)
5252

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

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

6868
def __get_resources() -> dict:
@@ -101,18 +101,28 @@ def __get_resources() -> dict:
101101
@docval({'name': 'extensions', 'type': (str, TypeMap, list),
102102
'doc': 'a path to a namespace, a TypeMap, or a list consisting of paths to namespaces and TypeMaps',
103103
'default': None},
104-
returns="TypeMap loaded for the given extension or NWB core namespace", rtype=tuple,
104+
{
105+
'name': 'copy', 'type': bool,
106+
'doc': 'Whether to return a deepcopy of the TypeMap. '
107+
'If False, a direct reference may be returned (use with caution).',
108+
'default': True
109+
},
110+
returns="TypeMap loaded for the given extension or NWB core namespace", rtype=TypeMap,
105111
is_method=False)
106112
def get_type_map(**kwargs):
107113
'''
108114
Get the TypeMap for the given extensions. If no extensions are provided,
109115
return the TypeMap for the core namespace
110116
'''
111-
extensions = getargs('extensions', kwargs)
117+
extensions, copy_map = getargs('extensions', 'copy', kwargs)
112118
type_map = None
113119
if extensions is None:
114-
type_map = deepcopy(__TYPE_MAP)
120+
if copy_map:
121+
type_map = deepcopy(__TYPE_MAP)
122+
else:
123+
type_map = __TYPE_MAP
115124
else:
125+
warn("The 'extensions' argument is deprecated and will be removed in PyNWB 4.0", DeprecationWarning)
116126
if isinstance(extensions, TypeMap):
117127
type_map = extensions
118128
else:
@@ -538,7 +548,7 @@ def read_nwb(**kwargs):
538548
# Retrieve the filepath
539549
path = popargs('path', kwargs)
540550
file = popargs('file', kwargs)
541-
551+
542552
path = str(path) if path is not None else None
543553

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

557567
return nwbfile
558568

559-
@docval({'name': 'path', 'type': (str, Path),
569+
@docval({'name': 'path', 'type': (str, Path),
560570
'doc': 'Path to the NWB file. Can be either a local filesystem path to '
561-
'an HDF5 (.nwb) or Zarr (.zarr) file.'},
571+
'an HDF5 (.nwb) or Zarr (.zarr) file.'},
562572
is_method=False)
563573
def read_nwb(**kwargs):
564574
"""Read an NWB file from a local path.
565575
566-
High-level interface for reading NWB files. Automatically handles both HDF5
567-
and Zarr formats. For advanced use cases (parallel I/O, custom namespaces),
576+
High-level interface for reading NWB files. Automatically handles both HDF5
577+
and Zarr formats. For advanced use cases (parallel I/O, custom namespaces),
568578
use NWBHDF5IO or NWBZarrIO.
569579
570-
See also
580+
See also
571581
* :py:class:`~pynwb.NWBHDF5IO`: Core I/O class for HDF5 files with advanced options.
572582
* :py:class:`~hdmf_zarr.nwb.NWBZarrIO`: Core I/O class for Zarr files with advanced options.
573583
@@ -585,17 +595,17 @@ def read_nwb(**kwargs):
585595
* Write or append modes
586596
* Pre-opened HDF5 file objects or Zarr stores
587597
* Remote file access configuration
588-
598+
589599
Example usage reading a local NWB file:
590600
591601
.. code-block:: python
592602
593603
from pynwb import read_nwb
594-
nwbfile = read_nwb("path/to/file.nwb")
604+
nwbfile = read_nwb("path/to/file.nwb")
595605
596606
:Returns: pynwb.NWBFile The loaded NWB file object.
597607
"""
598-
608+
599609
path = popargs('path', kwargs)
600610
# HDF5 is always available so we try that first
601611
backend_is_hdf5 = NWBHDF5IO.can_read(path=path)
@@ -607,18 +617,18 @@ def read_nwb(**kwargs):
607617
from hdmf_zarr import NWBZarrIO
608618
backend_is_zarr = NWBZarrIO.can_read(path=path)
609619
if backend_is_zarr:
610-
return NWBZarrIO.read_nwb(path=path)
620+
return NWBZarrIO.read_nwb(path=path)
611621
else:
612622
raise ValueError(
613623
f"Unable to read file: '{path}'. The file is not recognized as "
614624
"either a valid HDF5 or Zarr NWB file. Please ensure the file exists and contains valid NWB data."
615-
)
625+
)
616626
except ImportError:
617627
raise ValueError(
618628
f"Unable to read file: '{path}'. The file is not recognized as an HDF5 NWB file. "
619629
"If you are trying to read a Zarr file, please install hdmf-zarr using: pip install hdmf-zarr"
620630
)
621-
631+
622632

623633

624634
from . import io as __io # noqa: F401,E402
@@ -642,7 +652,7 @@ def read_nwb(**kwargs):
642652
# Functions
643653
'get_type_map',
644654
'get_manager',
645-
'load_namespaces',
655+
'load_namespaces',
646656
'available_namespaces',
647657
'clear_cache_dir',
648658
'register_class',
@@ -653,11 +663,11 @@ def read_nwb(**kwargs):
653663
'unload_type_config',
654664
'read_nwb',
655665
'get_nwbfile_version',
656-
666+
657667
# Classes
658668
'NWBHDF5IO',
659669
'NWBContainer',
660-
'NWBData',
670+
'NWBData',
661671
'TimeSeries',
662672
'ProcessingModule',
663673
'NWBFile',

src/pynwb/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def _error_on_new_pass_on_construct(self, error_msg: str):
6161
raise ValueError(error_msg)
6262

6363
def _get_type_map(self):
64-
return get_type_map()
64+
return get_type_map(copy=False)
6565

6666
@property
6767
def data_type(self):

tests/integration/helpers/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ def create_test_extension(specs, container_classes, mappers=None):
1919
export_spec(ns_builder, specs, output_dir.name)
2020

2121
# this will copy the global pynwb TypeMap and add the extension types to the copy
22-
type_map = get_type_map(f"{output_dir.name}/{NAMESPACE_NAME}.namespace.yaml")
22+
type_map = get_type_map()
23+
type_map.load_namespaces(f"{output_dir.name}/{NAMESPACE_NAME}.namespace.yaml")
2324
for type_name, container_cls in container_classes.items():
2425
type_map.register_container_type(NAMESPACE_NAME, type_name, container_cls)
2526
if mappers:

tests/unit/test_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class TestPyNWBTypeConfig(TestCase):
2020
def setUp(self):
2121
if not REQUIREMENTS_INSTALLED:
2222
self.skipTest("optional LinkML module is not installed")
23-
CUR_DIR = os.path.dirname(os.path.realpath(__file__))
23+
CUR_DIR = os.path.dirname(os.path.realpath(__file__))
2424
path_to_config = os.path.join(CUR_DIR, 'test_config/test_nwb_config.yaml')
2525
load_type_config(config_path=path_to_config)
2626

tests/unit/test_extension.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ def test_export(self):
4545

4646
def test_load_namespace(self):
4747
self.test_export()
48-
get_type_map(extensions=os.path.join(self.tempdir, self.ns_path))
48+
type_map = get_type_map()
49+
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path))
4950

5051
def test_get_class(self):
5152
self.test_export()
52-
type_map = get_type_map(extensions=os.path.join(self.tempdir, self.ns_path))
53+
type_map = get_type_map()
54+
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path))
5355
type_map.get_dt_container_cls('TetrodeSeries', self.prefix)
5456

5557
def test_load_namespace_with_reftype_attribute(self):
@@ -62,7 +64,8 @@ def test_load_namespace_with_reftype_attribute(self):
6264
neurodata_type_def='my_new_type')
6365
ns_builder.add_spec(self.ext_source, test_ds_ext)
6466
ns_builder.export(self.ns_path, outdir=self.tempdir)
65-
get_type_map(extensions=os.path.join(self.tempdir, self.ns_path))
67+
type_map = get_type_map()
68+
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path))
6669

6770
def test_load_namespace_with_reftype_attribute_check_autoclass_const(self):
6871
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):
7477
neurodata_type_def='my_new_type')
7578
ns_builder.add_spec(self.ext_source, test_ds_ext)
7679
ns_builder.export(self.ns_path, outdir=self.tempdir)
77-
type_map = get_type_map(extensions=os.path.join(self.tempdir, self.ns_path))
80+
type_map = get_type_map()
81+
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path))
7882
my_new_type = type_map.get_dt_container_cls('my_new_type', self.prefix)
7983
docval = None
8084
for tmp in get_docval(my_new_type.__init__):
@@ -172,7 +176,8 @@ def test_catch_dup_name(self):
172176
neurodata_type_def='TetrodeSeries')
173177
ns_builder2.add_spec(self.ext_source2, ext2)
174178
ns_builder2.export(self.ns_path2, outdir=self.tempdir)
175-
type_map = get_type_map(extensions=os.path.join(self.tempdir, self.ns_path1))
179+
type_map = get_type_map()
180+
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path1))
176181
type_map.load_namespaces(os.path.join(self.tempdir, self.ns_path2))
177182

178183
def test_catch_dup_name_core_newer(self):

0 commit comments

Comments
 (0)