Source code for pynwb.io.device

from warnings import warn

from .. import register_map
from ..device import Device, DeviceModel
from .core import NWBContainerMapper

def _name_has_invalid_chars(name):
    """Return True if ``name`` contains a character that is not allowed in an NWB object name."""
    return "/" in name or ":" in name


def _construct_legacy_device_model(**kwargs):
    """Construct a DeviceModel in HDMF construct mode.

    Reading a legacy file where ``Device.model`` is a string remaps that string to a ``DeviceModel``
    whose name is the original string. Construct mode bypasses the ``AbstractContainer`` name
    validation so legacy names containing ``/`` or ``:`` (which are not allowed in newly created
    object names) can still be read. Such a DeviceModel is read-only: writing or exporting it raises
    an error in ``DeviceModelMapper.build`` because the invalid name would silently create nested
    HDF5 groups.
    """
    model = DeviceModel.__new__(DeviceModel, in_construct_mode=True)
    model.__init__(**kwargs)
    model._in_construct_mode = False
    return model


[docs] @register_map(DeviceModel) class DeviceModelMapper(NWBContainerMapper): """Custom mapper that guards against writing a DeviceModel with an invalid name. A DeviceModel remapped from a legacy ``Device.model`` string (see :class:`DeviceMapper`) keeps the original string as its name, which can contain ``/`` or ``:``. Those characters are interpreted as HDF5 path separators on write, silently splitting the model into nested groups and corrupting the file. Building such a DeviceModel raises an actionable error instead. """
[docs] def build(self, *args, **kwargs): container = args[0] if args else kwargs.get("container") if container is not None and _name_has_invalid_chars(container.name): raise ValueError( f'Cannot write DeviceModel "{container.name}": its name contains a "/" or ":", ' "which are not allowed in NWB object names. This DeviceModel was likely remapped " "from a legacy Device.model string when reading an older file and is read-only. To " "write or export the data, create a new DeviceModel with a valid name and assign it to " 'Device.model. See the "Adding/Removing Containers from an NWB File" tutorial in the ' "PyNWB documentation for an example." ) return super().build(*args, **kwargs)
[docs] @register_map(Device) class DeviceMapper(NWBContainerMapper): """ Custom mapper for Device objects to handle known schema conflicts between core schema and extensions. This mapper detects when extensions define Device.model as a string attribute instead of a link to DeviceModel, or when extensions define their own DeviceModel type. """
[docs] @NWBContainerMapper.constructor_arg("model") def model_carg(self, builder, manager): """ Handle different model mapping strategies based on detected schema conflicts. Args: builder: The GroupBuilder for the Device manager: The BuildManager Returns: The appropriate model object or value based on the mapping strategy """ model_builder = builder.get('model') if isinstance(model_builder, str): msg = ( 'Device.model was detected as a string, but NWB 2.9 specifies Device.model as a link to a DeviceModel. ' f'Remapping "{model_builder}" to a new DeviceModel.' ) if _name_has_invalid_chars(model_builder): msg += ( ' Because the model name contains a "/" or ":", which are not allowed in NWB object names, the ' 'remapped DeviceModel is read-only and the file cannot be written or exported until it is ' 'replaced. To write/export the data, create a new DeviceModel with a valid name and assign it to ' 'Device.model. See the "Adding/Removing Containers from an NWB File" tutorial in the ' 'PyNWB documentation for an example.' ) warn(msg, stacklevel=3) # replace the model string with a DeviceModel object using the model name and device attributes device_model_attributes = dict(name=model_builder, description=builder.attributes.get('description'), manufacturer=builder.attributes.get('manufacturer', ''), model_number=builder.attributes.get('model_number')) model = _construct_legacy_device_model(**device_model_attributes) return model return None
def __new_container__(self, cls, container_source, parent, object_id, **kwargs): # Override ObjectMapper.__new_container__ to handle the case where the Device.model argument # is not a DeviceModel, which can happen in extensions written to be compatible with NWB<2.9. # The original Device.model object will be accessible under a new attribute name based on the # extension namespace. model = kwargs.get('model', None) if model is None or isinstance(model, DeviceModel): device_obj = super().__new_container__(cls, container_source, parent, object_id, **kwargs) else: # create device object without model kwargs.pop('model') device_obj = super().__new_container__(cls, container_source, parent, object_id, **kwargs) # add the conflicting Device.model object as a new attribute on Device # e.g. Device.model in the file -> Device.ndx_optogenetics_model in the python object warn(f'The model attribute of the Device "{device_obj.name}" was detected as a non-DeviceModel ' f'object. Data associated with this object can be accessed at ' f'"nwbfile.devices["{device_obj.name}"].{model.namespace.replace("-", "_")}_model"', stacklevel=2) setattr(device_obj, f"{model.namespace.replace('-', '_')}_model", model) return device_obj