Skip to content

Commit b27e54c

Browse files
authored
Fix copying of typemap and refactor TypeConfigurator (#1302)
1 parent da877eb commit b27e54c

File tree

7 files changed

+110
-89
lines changed

7 files changed

+110
-89
lines changed

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
# HDMF Changelog
22

3-
## HDMF 4.1.1 (Upcoming)
4-
### Enhancements
3+
## HDMF 4.1.1 (August 12, 2025)
4+
5+
### Fixed
6+
- Fixed copying of `TypeMap` and `TypeConfigurator`. Previously, the same global `TypeConfigurator` instance was used in all copies of a `TypeMap`. @rly [#1302](https://github.com/hdmf-dev/hdmf/pull/1302)
7+
8+
### Added
59
- Added a check for a compound datatype that is not defined in the schema or spec. This is currently not supported. @mavaylon1 [#1276](https://github.com/hdmf-dev/hdmf/pull/1276)
610

11+
712
## HDMF 4.1.0 (May 28, 2025)
813

914
### Enhancements
@@ -25,6 +30,7 @@
2530
- Fixed `get_data_shape` returning the wrong shape for lists of Data objects. @rly [#1270](https://github.com/hdmf-dev/hdmf/pull/1270)
2631
- Added protection against complex numbers in arrays and other data types. @bendichter [#1279](https://github.com/hdmf-dev/hdmf/pull/1279)
2732

33+
2834
## HDMF 4.0.0 (January 22, 2025)
2935

3036
### Breaking changes
@@ -48,6 +54,7 @@
4854
### Fixed
4955
- Fixed issue with `DynamicTable.add_column` not allowing subclasses of `DynamicTableRegion` or `EnumData`. @rly [#1091](https://github.com/hdmf-dev/hdmf/pull/1091)
5056

57+
5158
## HDMF 3.14.6 (December 20, 2024)
5259

5360
### Enhancements

src/hdmf/build/manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ def container_types(self):
431431
return self.__container_types
432432

433433
def __copy__(self):
434-
ret = TypeMap(copy(self.__ns_catalog), self.__default_mapper_cls, self.type_config)
434+
ret = TypeMap(copy(self.__ns_catalog), self.__default_mapper_cls, TypeConfigurator(self.type_config.paths))
435435
ret.merge(self)
436436
return ret
437437

@@ -464,6 +464,7 @@ def merge(self, type_map, ns_catalog=False):
464464
for custom_generators in reversed(type_map.__class_generator_manager.custom_generators):
465465
# iterate in reverse order because generators are stored internally as a stack
466466
self.register_generator(custom_generators)
467+
# NOTE: the type config is not merged from the input type map to the new one. add if there is a clear use case
467468

468469
@docval({"name": "generator", "type": type, "doc": "the CustomClassGenerator class to register"})
469470
def register_generator(self, **kwargs):

src/hdmf/common/__init__.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os.path
55
from copy import deepcopy
66
from collections.abc import Callable
7+
import warnings
78

89
CORE_NAMESPACE = 'hdmf-common'
910
EXP_NAMESPACE = 'hdmf-experimental'
@@ -26,35 +27,34 @@
2627
is_method=False)
2728
def load_type_config(**kwargs):
2829
"""
29-
This method will either load the default config or the config provided by the path.
30-
NOTE: This config is global and shared across all type maps.
30+
This method will either load the config at the given path into either the global type map or a specific type map.
3131
"""
3232
config_path = kwargs['config_path']
33-
type_map = kwargs['type_map'] or get_type_map()
33+
type_map = kwargs['type_map'] or __TYPE_MAP
3434

3535
type_map.type_config.load_type_config(config_path)
3636

3737
@docval({'name': 'type_map', 'type': TypeMap, 'doc': 'The TypeMap.', 'default': None},
3838
is_method=False)
3939
def get_loaded_type_config(**kwargs):
4040
"""
41-
This method returns the entire config file.
41+
This method returns a dictionary with the configuration for each namespace and data type.
4242
"""
43-
type_map = kwargs['type_map'] or get_type_map()
43+
type_map = kwargs['type_map'] or __TYPE_MAP
4444

4545
if type_map.type_config.config is None:
4646
msg = "No configuration is loaded."
4747
raise ValueError(msg)
48-
else:
49-
return type_map.type_config.config
48+
49+
return type_map.type_config.config
5050

5151
@docval({'name': 'type_map', 'type': TypeMap, 'doc': 'The TypeMap.', 'default': None},
5252
is_method=False)
5353
def unload_type_config(**kwargs):
5454
"""
55-
Unload the configuration file.
55+
Unload all type configurations from the global type map or a specific type map.
5656
"""
57-
type_map = kwargs['type_map'] or get_type_map()
57+
type_map = kwargs['type_map'] or __TYPE_MAP
5858

5959
return type_map.type_config.unload_type_config()
6060

@@ -183,6 +183,7 @@ def get_type_map(**kwargs):
183183
if extensions is None:
184184
type_map = deepcopy(__TYPE_MAP)
185185
else:
186+
warnings.warn("The 'extensions' argument is deprecated and will be removed in HDMF 5.0", DeprecationWarning)
186187
if isinstance(extensions, TypeMap):
187188
type_map = extensions
188189
else:

src/hdmf/container.py

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ def setter(self, val):
9292
return setter
9393

9494
def _get_type_map(self):
95+
# TODO: refactor this so that it does not call get_type_map every time an attribute is set
96+
# and there is non circular import
9597
from hdmf.common import get_type_map # circular import
9698
return get_type_map()
9799

@@ -114,57 +116,55 @@ def _field_config(self, arg_name, val, type_map):
114116
"""
115117
configurator = type_map.type_config
116118

117-
if len(configurator.path)>0:
118-
# The type_map has a config always set; however, when toggled off, the config path is empty.
119-
CUR_DIR = os.path.dirname(os.path.realpath(configurator.path[0]))
120-
termset_config = configurator.config
121-
else:
119+
if not configurator.paths:
122120
return val
123121

122+
# The type_map has a config always set; however, when toggled off, the config path is empty.
123+
# TODO account for more than one different configurator path
124+
CUR_DIR = os.path.dirname(os.path.realpath(configurator.paths[0]))
125+
termset_config = configurator.config
126+
124127
# If the val has been manually wrapped then skip checking the config for the attr
125128
if isinstance(val, TermSetWrapper):
126129
msg = "Field value already wrapped with TermSetWrapper."
127130
warn(msg)
128131
return val
129132

130-
# check to see that the namespace for the container is in the config
133+
# return the value if the namespace for the container is not in the config
131134
if self.namespace not in termset_config['namespaces']:
132-
msg = "%s not found within loaded configuration." % self.namespace
135+
return val
136+
137+
# check to see that the container type is in the config under the namespace
138+
config_namespace = termset_config['namespaces'][self.namespace]
139+
data_type = self.data_type
140+
141+
# return the value if the data type for the container is not in the config
142+
if data_type not in config_namespace['data_types']:
143+
return val
144+
145+
# Get the ObjectMapper
146+
obj_mapper = type_map.get_map(self)
147+
148+
# Get the spec for the constructor arg
149+
spec = obj_mapper.get_carg_spec(arg_name)
150+
if spec is None:
151+
msg = "Spec not found for %s." % arg_name
133152
warn(msg)
134153
return val
135-
else:
136-
# check to see that the container type is in the config under the namespace
137-
config_namespace = termset_config['namespaces'][self.namespace]
138-
data_type = self.data_type
139-
140-
if data_type not in config_namespace['data_types']:
141-
msg = '%s not found within the configuration for %s' % (data_type, self.namespace)
142-
warn(msg)
143-
return val
144-
else:
145-
# Get the ObjectMapper
146-
obj_mapper = type_map.get_map(self)
147-
148-
# Get the spec for the constructor arg
149-
spec = obj_mapper.get_carg_spec(arg_name)
150-
if spec is None:
151-
msg = "Spec not found for %s." % arg_name
152-
warn(msg)
153-
return val
154-
155-
# Get spec attr name
156-
mapped_attr_name = obj_mapper.get_attribute(spec)
157-
158-
config_data_type = config_namespace['data_types'][data_type]
159-
try:
160-
config_termset_path = config_data_type[mapped_attr_name]
161-
except KeyError:
162-
return val
163-
164-
termset_path = os.path.join(CUR_DIR, config_termset_path['termset'])
165-
termset = TermSet(term_schema_path=termset_path)
166-
val = TermSetWrapper(value=val, termset=termset)
167-
return val
154+
155+
# Get spec attr name
156+
mapped_attr_name = obj_mapper.get_attribute(spec)
157+
158+
config_data_type = config_namespace['data_types'][data_type]
159+
try:
160+
config_termset_path = config_data_type[mapped_attr_name]
161+
except KeyError:
162+
return val
163+
164+
termset_path = os.path.join(CUR_DIR, config_termset_path['termset'])
165+
termset = TermSet(term_schema_path=termset_path)
166+
val = TermSetWrapper(value=val, termset=termset)
167+
return val
168168

169169
@classmethod
170170
def _getter(cls, field):

src/hdmf/term_set.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -355,14 +355,13 @@ class TypeConfigurator:
355355
When toggled on, every instance of a configuration file supported data type will be validated
356356
according to the corresponding TermSet.
357357
"""
358-
@docval({'name': 'path', 'type': str, 'doc': 'Path to the configuration file.', 'default': None})
358+
@docval({'name': 'paths', 'type': list, 'doc': 'Paths to configuration files.', 'default': None})
359359
def __init__(self, **kwargs):
360360
self.config = None
361-
if kwargs['path'] is None:
362-
self.path = []
363-
else:
364-
self.path = [kwargs['path']]
365-
self.load_type_config(config_path=self.path[0])
361+
self.paths = []
362+
if kwargs["paths"]:
363+
for p in kwargs["paths"]:
364+
self.load_type_config(p)
366365

367366
@docval({'name': 'data_type', 'type': str,
368367
'doc': 'The desired data type within the configuration file.'},
@@ -386,19 +385,19 @@ def get_config(self, data_type, namespace):
386385
raise ValueError(msg)
387386

388387
@docval({'name': 'config_path', 'type': str, 'doc': 'Path to the configuration file.'})
389-
def load_type_config(self,config_path):
388+
def load_type_config(self, config_path):
390389
"""
391390
Load the configuration file for validation on the fields defined for the objects within the file.
392391
"""
393392
with open(config_path, 'r') as config:
394-
yaml=YAML(typ='safe')
393+
yaml = YAML(typ='safe')
395394
termset_config = yaml.load(config)
396395
if self.config is None: # set the initial config/load after config has been unloaded
397396
self.config = termset_config
398-
if len(self.path)==0: # for loading after an unloaded config
399-
self.path.append(config_path)
397+
if len(self.paths) == 0: # for loading after an unloaded config
398+
self.paths.append(config_path)
400399
else: # append/replace to the existing config
401-
if config_path in self.path:
400+
if config_path in self.paths:
402401
msg = 'This configuration file path already exists within the configurator.'
403402
raise ValueError(msg)
404403
else:
@@ -415,12 +414,12 @@ def load_type_config(self,config_path):
415414
new_config = termset_config['namespaces'][namespace]['data_types'][data_type]
416415
self.config['namespaces'][namespace]['data_types'][data_type] = new_config
417416

418-
# append path to self.path
419-
self.path.append(config_path)
417+
# append path to self.paths
418+
self.paths.append(config_path)
420419

421420
def unload_type_config(self):
422421
"""
423-
Remove validation according to termset configuration file.
422+
Remove validation with all loaded termset configuration files. Effectively reset this instance.
424423
"""
425-
self.path = []
426424
self.config = None
425+
self.paths = []

tests/unit/common/test_common.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
class TestCommonTypeMap(TestCase):
77

8+
def tearDown(self):
9+
unload_type_config()
10+
811
def test_base_types(self):
912
tm = get_type_map()
1013
cls = tm.get_dt_container_cls('Container', 'hdmf-common')
@@ -24,5 +27,4 @@ def test_copy_ts_config(self):
2427
{'ExtensionContainer': {'description': None}}}}}
2528

2629
self.assertEqual(tm.type_config.config, config)
27-
self.assertEqual(tm.type_config.path, [path])
28-
unload_type_config()
30+
self.assertEqual(tm.type_config.paths, [path])

0 commit comments

Comments
 (0)