Skip to content

[Bug]: Loading nwbs created with older pynwb version fails due to '/' or ':' in Device.model name #2185

@calderast

Description

@calderast

What happened?

I created this nwbfile with an older pynwb version, where the device model name string has special characters ('MFC_200/250-0.66_40mm_MF2.5_FLT'). When the file tries to be loaded with a newer pynwb version, the remapping fails:

[/home/scrater/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1401](https://vscode-remote+ssh-002dremote-002bbreeze-002ecin-002eucsf-002eedu.vscode-resource.vscode-cdn.net/home/scrater/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1401): UserWarning: Device.model was detected as a string, but NWB 2.9 specifies Device.model as a link to a DeviceModel. Remapping "MFC_200/250-0.66_40mm_MF2.5_FLT" to a new DeviceModel.
  override = self.__get_override_carg(argname, builder, manager)

I patched this by adding this snippet to the init.py of my schema for loading these nwbs:

# PyNWB 3.x introduced DeviceModel, which crashes when reading NWB files whose
# device names contain '/' or ':' (e.g. 'MFC_200/250-0.66_40mm_MF2.5_FLT').
# Patch DeviceMapper to silently skip invalid device model names on read.
try:
    from pynwb.io.device import DeviceMapper

    _orig_model_fn = DeviceMapper.constructor_args.get("model")

    if _orig_model_fn:
        def _safe_model_fn(self, builder, manager):
            try:
                return _orig_model_fn(self, builder, manager)
            except ValueError:
                return None
        DeviceMapper.constructor_args["model"] = _safe_model_fn
except Exception:
    pass  # older pynwb without DeviceMapper — no patch needed

Steps to Reproduce

key = {"nwb_file_name": "IM-1478_20220725_.nwb"}
df = (HexMazeJunctionDecode & key).fetch1_dataframe()

Traceback

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 1
----> 1 df = (HexMazeJunctionDecode & key).fetch1_dataframe()

File ~/dev/Hex-maze-spyglass/spyglass_hexmaze/hex_maze_decoding.py:1415, in HexMazeJunctionDecode.fetch1_dataframe(self)
   1414 def fetch1_dataframe(self):
-> 1415     return self.fetch_nwb()[0]["junction_decode"].set_index("time")

File ~/dev/spyglass/src/spyglass/utils/mixins/fetch.py:336, in FetchMixin.fetch_nwb(self, *attrs, **kwargs)
    333 rec_dicts = self._execute_nwb_query(table, tbl_attr, *attrs, **kwargs)
    335 # Process object_id fields if present
--> 336 return self._process_object_ids(rec_dicts, *attrs)

File ~/dev/spyglass/src/spyglass/utils/mixins/fetch.py:261, in FetchMixin._process_object_ids(self, rec_dicts, *attrs)
    259 ret = []
    260 for rec_dict in rec_dicts:
--> 261     nwbf = get_nwb_file(rec_dict.pop("nwb2load_filepath"))
    262     # for each attr that contains substring 'object_id', store key-value:
    263     # attr name to NWB object
    264     # remove '_object_id' from attr name
    265     nwb_objs = {
    266         id_attr.replace("_object_id", ""): self._get_nwb_object(
    267             nwbf.objects, rec_dict[id_attr]
   (...)
    270         if "object_id" in id_attr and rec_dict[id_attr] != ""
    271     }

File ~/dev/spyglass/src/spyglass/utils/nwb_helper_fn.py:88, in get_nwb_file(nwb_file_path, query_expression)
     85     return nwbfile
     87 if os.path.exists(nwb_file_path):
---> 88     return _open_nwb_file(nwb_file_path)
     90 logger.info(
     91     f"NWB file not found locally; checking kachery for {nwb_file_path}"
     92 )
     94 from ..sharing.sharing_kachery import AnalysisNwbfileKachery

File ~/dev/spyglass/src/spyglass/utils/nwb_helper_fn.py:29, in _open_nwb_file(nwb_file_path, source)
     27 if source == "local":
     28     io = pynwb.NWBHDF5IO(path=nwb_file_path, mode="r", load_namespaces=True)
---> 29     nwbfile = io.read()
     30 elif source == "dandi":
     31     from ..common.common_dandi import DandiPath

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/pynwb/__init__.py:489, in NWBHDF5IO.read(self, **kwargs)
    486         raise TypeError("NWB version %s not supported. PyNWB supports NWB files version 2 and above." %
    487                         str(file_version_str))
    488 # read the file
--> 489 file = super().read(**kwargs)
    490 return file

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/backends/hdf5/h5tools.py:458, in HDF5IO.read(self, **kwargs)
    455     raise UnsupportedOperation("Cannot read from file %s in mode '%s'. Please use mode 'r', 'r+', or 'a'."
    456                                % (self.source, self.__mode))
    457 try:
--> 458     return super().read(**kwargs)
    459 except UnsupportedOperation as e:
    460     if str(e) == 'Cannot build data. There are no values.':  # pragma: no cover

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/backends/io.py:60, in HDMFIO.read(self, **kwargs)
     57 if all(len(v) == 0 for v in f_builder.values()):
     58     # TODO also check that the keys are appropriate. print a better error message
     59     raise UnsupportedOperation('Cannot build data. There are no values.')
---> 60 container = self.__manager.construct(f_builder)
     61 container.read_io = self
     62 if self.herd_path is not None:

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/manager.py:286, in BuildManager.construct(self, **kwargs)
    282     result = self.__type_map.construct(builder, self, parent)
    283 else:
    284     # we are at the top of the hierarchy,
    285     # so it must be time to resolve parents
--> 286     result = self.__type_map.construct(builder, self, None)
    287     self.__resolve_parents(result)
    288 self.prebuilt(result, builder)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/manager.py:814, in TypeMap.construct(self, **kwargs)
    812     raise ValueError('No ObjectMapper found for builder of type %s' % dt)
    813 else:
--> 814     return obj_mapper.construct(builder, build_manager, parent)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/pynwb/io/file.py:161, in NWBFileMap.construct(self, **kwargs)
    157         builder.groups['bands'].attributes['namespace'] = 'core'
    159 apply_to_child_builders(nwbfile_builder, [update_builder_frequency_bands_table])
--> 161 return super().construct(**kwargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1380, in ObjectMapper.construct(self, **kwargs)
   1378 cls = manager.get_cls(builder)
   1379 # gather all subspecs
-> 1380 subspecs = self.__get_subspec_values(builder, self.spec, manager)
   1381 # get the constructor argument that each specification corresponds to
   1382 const_args = dict()

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1309, in ObjectMapper.__get_subspec_values(self, builder, spec, manager)
   1307                 ret[subspec] = self.__flatten(sub_builder, subspec, manager)
   1308     # now process groups and datasets
-> 1309     self.__get_sub_builders(groups, spec.groups, manager, ret)
   1310     self.__get_sub_builders(datasets, spec.datasets, manager, ret)
   1311 elif isinstance(spec, DatasetSpec):

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1360, in ObjectMapper.__get_sub_builders(self, sub_builders, subspecs, manager, ret)
   1357     continue
   1358 if dt is None:
   1359     # recurse
-> 1360     ret.update(self.__get_subspec_values(sub_builder, subspec, manager))
   1361 else:
   1362     ret[subspec] = manager.construct(sub_builder)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1309, in ObjectMapper.__get_subspec_values(self, builder, spec, manager)
   1307                 ret[subspec] = self.__flatten(sub_builder, subspec, manager)
   1308     # now process groups and datasets
-> 1309     self.__get_sub_builders(groups, spec.groups, manager, ret)
   1310     self.__get_sub_builders(datasets, spec.datasets, manager, ret)
   1311 elif isinstance(spec, DatasetSpec):

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1360, in ObjectMapper.__get_sub_builders(self, sub_builders, subspecs, manager, ret)
   1357     continue
   1358 if dt is None:
   1359     # recurse
-> 1360     ret.update(self.__get_subspec_values(sub_builder, subspec, manager))
   1361 else:
   1362     ret[subspec] = manager.construct(sub_builder)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1309, in ObjectMapper.__get_subspec_values(self, builder, spec, manager)
   1307                 ret[subspec] = self.__flatten(sub_builder, subspec, manager)
   1308     # now process groups and datasets
-> 1309     self.__get_sub_builders(groups, spec.groups, manager, ret)
   1310     self.__get_sub_builders(datasets, spec.datasets, manager, ret)
   1311 elif isinstance(spec, DatasetSpec):

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1352, in ObjectMapper.__get_sub_builders(self, sub_builders, subspecs, manager, ret)
   1350     sub_builder = builder_dt.get(dt)
   1351     if sub_builder is not None:
-> 1352         sub_builder = self.__flatten(sub_builder, subspec, manager)
   1353         ret[subspec] = sub_builder
   1354 else:

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1365, in ObjectMapper.__flatten(self, sub_builder, subspec, manager)
   1364 def __flatten(self, sub_builder, subspec, manager):
-> 1365     tmp = [manager.construct(b) for b in sub_builder]
   1366     if len(tmp) == 1 and not subspec.is_many():
   1367         tmp = tmp[0]

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1365, in <listcomp>(.0)
   1364 def __flatten(self, sub_builder, subspec, manager):
-> 1365     tmp = [manager.construct(b) for b in sub_builder]
   1366     if len(tmp) == 1 and not subspec.is_many():
   1367         tmp = tmp[0]

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/manager.py:282, in BuildManager.construct(self, **kwargs)
    280 if parent_builder is not None:
    281     parent = self._get_proxy_builder(parent_builder)
--> 282     result = self.__type_map.construct(builder, self, parent)
    283 else:
    284     # we are at the top of the hierarchy,
    285     # so it must be time to resolve parents
    286     result = self.__type_map.construct(builder, self, None)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/manager.py:814, in TypeMap.construct(self, **kwargs)
    812     raise ValueError('No ObjectMapper found for builder of type %s' % dt)
    813 else:
--> 814     return obj_mapper.construct(builder, build_manager, parent)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:1401, in ObjectMapper.construct(self, **kwargs)
   1399 for const_arg in get_docval(cls.__init__):
   1400     argname = const_arg['name']
-> 1401     override = self.__get_override_carg(argname, builder, manager)
   1402     if override is not None:
   1403         val = override

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/build/objectmapper.py:590, in ObjectMapper.__get_override_carg(self, *args)
    588     self.logger.debug("        Calling override function for constructor argument '%s'" % name)
    589     func = self.constructor_args[name]
--> 590     return func(self, *remaining_args)
    591 return None

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/pynwb/io/device.py:41, in DeviceMapper.model_carg(self, builder, manager)
     36     # replace the model string with a DeviceModel object using the model name and device attributes 
     37     device_model_attributes = dict(name=model_builder,
     38                                    description=builder.attributes.get('description'),
     39                                    manufacturer=builder.attributes.get('manufacturer', ''),
     40                                    model_number=builder.attributes.get('model_number'))
---> 41     model = DeviceModel(**device_model_attributes)
     43     return model
     45 return None

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/pynwb/device.py:112, in DeviceModel.__init__(self, **kwargs)
     97 @docval(
     98     {'name': 'name', 'type': str, 'doc': 'The name of this device model'},
     99     {'name': 'manufacturer', 'type': str,
   (...)
    109 )
    110 def __init__(self, **kwargs):
    111     manufacturer, model_number, description = popargs('manufacturer', 'model_number', 'description', kwargs)
--> 112     super().__init__(**kwargs)
    113     self.manufacturer = manufacturer
    114     self.model_number = model_number

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/utils.py:592, in docval.<locals>.dec.<locals>.func_call(*args, **kwargs)
    590 def func_call(*args, **kwargs):
    591     pargs = _check_args(args, kwargs)
--> 592     return func(args[0], **pargs)

File ~/miniforge3/envs/spyglass/lib/python3.10/site-packages/hdmf/container.py:327, in AbstractContainer.__init__(self, **kwargs)
    325 name = getargs('name', kwargs)
    326 if ('/' in name or ':' in name) and not self._in_construct_mode:
--> 327     raise ValueError(f"name '{name}' cannot contain a '/' or ':'")
    328 self.__name = name
    329 self.__field_values = dict()

ValueError: name 'MFC_200/250-0.66_40mm_MF2.5_FLT' cannot contain a '/' or ':'

Operating System

Linux

Python Executable

Conda

Python Version

3.10

Package Versions

environment_for_issue.txt

Code of Conduct

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions