Skip to content

Commit 3d00833

Browse files
bendichterrly
andauthored
Rmv MultiContainerInterface (#1260)
Co-authored-by: Ben Dichter <[email protected]> Co-authored-by: Ryan Ly <[email protected]>
1 parent 0e6f55d commit 3d00833

File tree

9 files changed

+18
-405
lines changed

9 files changed

+18
-405
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# PyNWB Changelog
22

3-
## PyNWB 1.4.0 (August 10, 2020)
3+
## PyNWB 1.4.0 (August 11, 2020)
44

55
### Internal improvements:
6-
- Update requirements to use HDMF 2.0.1. @rly (#1256)
6+
- Update requirements to use HDMF 2.1.0. @rly (#1256)
77
- Start FAQ section in documentation. @rly (#1249)
88
- Improve deprecation warnings. @rly (#1261)
99
- Update CI to test Python 3.8, update requirements. @rly (#1267, #1275)
10+
- Make use of `MultiContainerInterface` and `LabelledDict` that have been moved to HDMF. @bendichter @rly (#1260)
1011

1112
### Bug fixes:
1213
- For `ImageSeries`, add check if `external_file` is provided without `starting_frame` in `__init__`. @rly (#1264)

requirements-min.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# these minimum requirements specify '==' for testing; setup.py replaces '==' with '>='
22
h5py==2.9 # support for setting attrs to lists of utf-8 added in 2.9
3-
hdmf==2.0.1,<3
3+
hdmf==2.1.0,<3
44
numpy==1.16
55
pandas==0.23
66
python-dateutil==2.7

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
h5py==2.10.0
2-
hdmf==2.0.1
2+
hdmf==2.1.0
33
numpy==1.18.5
44
pandas==0.25.3
55
python-dateutil==2.8.1

src/pynwb/core.py

Lines changed: 7 additions & 325 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
import numpy as np
33
import pandas as pd
44

5-
from hdmf.utils import docval, getargs, ExtenderMeta, call_docval_func, popargs, get_docval, fmt_docval_args
65
from hdmf import Container, Data, DataRegion, get_region_slicer
7-
from hdmf.container import AbstractContainer
6+
from hdmf.container import AbstractContainer, MultiContainerInterface as hdmf_MultiContainerInterface
87
from hdmf.common import DynamicTable, DynamicTableRegion # noqa: F401
98
from hdmf.common import VectorData, VectorIndex, ElementIdentifiers # noqa: F401
9+
from hdmf.utils import docval, getargs, ExtenderMeta, call_docval_func, popargs
10+
from hdmf.utils import LabelledDict # noqa: F401
1011

1112
from . import CORE_NAMESPACE, register_class
1213

@@ -39,28 +40,11 @@ class NWBContainer(NWBMixin, Container):
3940

4041
__nwbfields__ = tuple()
4142

42-
@docval({'name': 'name', 'type': str, 'doc': 'the name of this container'})
43-
def __init__(self, **kwargs):
44-
call_docval_func(super(NWBContainer, self).__init__, kwargs)
45-
46-
def _to_dict(self, arg, label="NULL"):
47-
return_dict = LabelledDict(label)
48-
if arg is None:
49-
return return_dict
50-
else:
51-
for i in arg:
52-
assert i.name is not None # If a container doesn't have a name, it gets lost!
53-
assert i.name not in return_dict
54-
return_dict[i.name] = i
55-
return return_dict
56-
5743

5844
@register_class('NWBDataInterface', CORE_NAMESPACE)
5945
class NWBDataInterface(NWBContainer):
6046

61-
@docval(*get_docval(NWBContainer.__init__))
62-
def __init__(self, **kwargs):
63-
call_docval_func(super(NWBDataInterface, self).__init__, kwargs)
47+
pass
6448

6549

6650
@register_class('NWBData', CORE_NAMESPACE)
@@ -332,309 +316,7 @@ def __getitem__(self, idx):
332316
return self.__regionslicer[idx]
333317

334318

335-
class MultiContainerInterface(NWBDataInterface):
336-
'''
337-
A class for dynamically defining a API classes that
338-
represent NWBDataInterfaces that contain multiple Containers
339-
of the same type
340-
341-
To use, extend this class, and create a dictionary as a class
342-
attribute with the following keys:
343-
344-
* 'add' to name the method for adding Container instances
345-
346-
* 'create' to name the method fo creating Container instances
347-
348-
* 'get' to name the method for getting Container instances
349-
350-
* 'attr' to name the attribute that stores the Container instances
351-
352-
* 'type' to provide the Container object type
353-
354-
See LFP or Position for an example of how to use this.
355-
'''
319+
class MultiContainerInterface(NWBDataInterface, hdmf_MultiContainerInterface):
320+
"""Defined in PyNWB for API backward compatibility. See HDMF MultiContainterInterface for details."""
356321

357-
@docval(*get_docval(NWBDataInterface.__init__))
358-
def __init__(self, **kwargs):
359-
call_docval_func(super(MultiContainerInterface, self).__init__, kwargs)
360-
if isinstance(self.__clsconf__, dict):
361-
attr_name = self.__clsconf__['attr']
362-
self.fields[attr_name] = LabelledDict(attr_name)
363-
else:
364-
for d in self.__clsconf__:
365-
attr_name = d['attr']
366-
self.fields[attr_name] = LabelledDict(attr_name)
367-
368-
@staticmethod
369-
def __add_article(noun):
370-
if isinstance(noun, tuple):
371-
noun = noun[0]
372-
if isinstance(noun, type):
373-
noun = noun.__name__
374-
if noun[0] in ('aeiouAEIOU'):
375-
return 'an %s' % noun
376-
return 'a %s' % noun
377-
378-
@staticmethod
379-
def __join(argtype):
380-
def tostr(x):
381-
return x.__name__ if isinstance(x, type) else x
382-
if isinstance(argtype, (list, tuple)):
383-
args = [tostr(x) for x in argtype]
384-
if len(args) == 1:
385-
return args[0].__name__
386-
else:
387-
", ".join(tostr(x) for x in args[:-1]) + ' or ' + args[-1]
388-
else:
389-
return tostr(argtype)
390-
391-
@classmethod
392-
def __make_get(cls, func_name, attr_name, container_type):
393-
doc = "Get %s from this %s" % (cls.__add_article(container_type), cls.__name__)
394-
395-
@docval({'name': 'name', 'type': str, 'doc': 'the name of the %s' % cls.__join(container_type),
396-
'default': None},
397-
rtype=container_type, returns='the %s with the given name' % cls.__join(container_type),
398-
func_name=func_name, doc=doc)
399-
def _func(self, **kwargs):
400-
name = getargs('name', kwargs)
401-
d = getattr(self, attr_name)
402-
ret = None
403-
if name is None:
404-
if len(d) > 1:
405-
msg = "more than one element in %s of %s '%s' -- must specify a name" % \
406-
(attr_name, cls.__name__, self.name)
407-
raise ValueError(msg)
408-
elif len(d) == 0:
409-
msg = "%s of %s '%s' is empty" % (attr_name, cls.__name__, self.name)
410-
raise ValueError(msg)
411-
elif len(d) == 1:
412-
for v in d.values():
413-
ret = v
414-
else:
415-
ret = d.get(name)
416-
if ret is None:
417-
msg = "'%s' not found in %s of %s '%s'" % (name, attr_name, cls.__name__, self.name)
418-
raise KeyError(msg)
419-
return ret
420-
421-
return _func
422-
423-
@classmethod
424-
def __make_add(cls, func_name, attr_name, container_type):
425-
doc = "Add %s to this %s" % (cls.__add_article(container_type), cls.__name__)
426-
427-
@docval({'name': attr_name, 'type': (list, tuple, dict, container_type),
428-
'doc': 'the %s to add' % cls.__join(container_type)},
429-
func_name=func_name, doc=doc)
430-
def _func(self, **kwargs):
431-
container = getargs(attr_name, kwargs)
432-
if isinstance(container, container_type):
433-
containers = [container]
434-
elif isinstance(container, dict):
435-
containers = container.values()
436-
else:
437-
containers = container
438-
d = getattr(self, attr_name)
439-
for tmp in containers:
440-
if not isinstance(tmp.parent, Container):
441-
tmp.parent = self
442-
# else, the ObjectMapper will create a link from self (parent) to tmp (child with existing parent)
443-
if tmp.name in d:
444-
msg = "'%s' already exists in '%s'" % (tmp.name, self.name)
445-
raise ValueError(msg)
446-
d[tmp.name] = tmp
447-
return container
448-
return _func
449-
450-
@classmethod
451-
def __make_create(cls, func_name, add_name, container_type):
452-
doc = "Create %s and add it to this %s" % (cls.__add_article(container_type), cls.__name__)
453-
454-
@docval(*filter(_not_parent, get_docval(container_type.__init__)), func_name=func_name, doc=doc,
455-
returns="the %s object that was created" % cls.__join(container_type), rtype=container_type)
456-
def _func(self, **kwargs):
457-
cargs, ckwargs = fmt_docval_args(container_type.__init__, kwargs)
458-
ret = container_type(*cargs, **ckwargs)
459-
getattr(self, add_name)(ret)
460-
return ret
461-
return _func
462-
463-
@classmethod
464-
def __make_constructor(cls, clsconf):
465-
args = list()
466-
for conf in clsconf:
467-
attr_name = conf['attr']
468-
container_type = conf['type']
469-
args.append({'name': attr_name, 'type': (list, tuple, dict, container_type),
470-
'doc': '%s to store in this interface' % cls.__join(container_type), 'default': dict()})
471-
472-
args.append({'name': 'name', 'type': str, 'doc': 'the name of this container', 'default': cls.__name__})
473-
474-
@docval(*args, func_name='__init__')
475-
def _func(self, **kwargs):
476-
call_docval_func(super(cls, self).__init__, kwargs)
477-
for conf in clsconf:
478-
attr_name = conf['attr']
479-
add_name = conf['add']
480-
container = popargs(attr_name, kwargs)
481-
add = getattr(self, add_name)
482-
add(container)
483-
return _func
484-
485-
@classmethod
486-
def __make_getitem(cls, attr_name, container_type):
487-
doc = "Get %s from this %s" % (cls.__add_article(container_type), cls.__name__)
488-
489-
@docval({'name': 'name', 'type': str, 'doc': 'the name of the %s' % cls.__join(container_type),
490-
'default': None},
491-
rtype=container_type, returns='the %s with the given name' % cls.__join(container_type),
492-
func_name='__getitem__', doc=doc)
493-
def _func(self, **kwargs):
494-
name = getargs('name', kwargs)
495-
d = getattr(self, attr_name)
496-
if len(d) == 0:
497-
msg = "%s '%s' is empty" % (cls.__name__, self.name)
498-
raise ValueError(msg)
499-
if len(d) > 1 and name is None:
500-
msg = "more than one %s in this %s -- must specify a name" % cls.__join(container_type), cls.__name__
501-
raise ValueError(msg)
502-
ret = None
503-
if len(d) == 1:
504-
for v in d.values():
505-
ret = v
506-
else:
507-
ret = d.get(name)
508-
if ret is None:
509-
msg = "'%s' not found in %s '%s'" % (name, cls.__name__, self.name)
510-
raise KeyError(msg)
511-
return ret
512-
513-
return _func
514-
515-
@classmethod
516-
def __make_setter(cls, nwbfield, add_name):
517-
518-
@docval({'name': 'val', 'type': (list, tuple, dict), 'doc': 'the sub items to add', 'default': None})
519-
def nwbbt_setter(self, **kwargs):
520-
val = getargs('val', kwargs)
521-
if val is None:
522-
return
523-
getattr(self, add_name)(val)
524-
525-
return nwbbt_setter
526-
527-
@ExtenderMeta.pre_init
528-
def __build_class(cls, name, bases, classdict):
529-
'''
530-
This classmethod will be called during class declaration in the metaclass to automatically
531-
create setters and getters for NWB fields that need to be exported
532-
'''
533-
if not hasattr(cls, '__clsconf__'):
534-
return
535-
multi = False
536-
if isinstance(cls.__clsconf__, dict):
537-
clsconf = [cls.__clsconf__]
538-
elif isinstance(cls.__clsconf__, list):
539-
multi = True
540-
clsconf = cls.__clsconf__
541-
else:
542-
raise TypeError("'__clsconf__' must be a dict or a list of dicts")
543-
544-
for i, d in enumerate(clsconf):
545-
# get add method name
546-
add = d.get('add')
547-
if add is None:
548-
msg = "MultiContainerInterface subclass '%s' is missing 'add' key in __clsconf__" % cls.__name__
549-
if multi:
550-
msg += " at element %d" % i
551-
raise ValueError(msg)
552-
553-
# get container attribute name
554-
attr = d.get('attr')
555-
if attr is None:
556-
msg = "MultiContainerInterface subclass '%s' is missing 'attr' key in __clsconf__" % cls.__name__
557-
if multi:
558-
msg += " at element %d" % i
559-
raise ValueError(msg)
560-
561-
# get container type
562-
container_type = d.get('type')
563-
if container_type is None:
564-
msg = "MultiContainerInterface subclass '%s' is missing 'type' key in __clsconf__" % cls.__name__
565-
if multi:
566-
msg += " at element %d" % i
567-
raise ValueError(msg)
568-
569-
# create property with the name given in 'attr'
570-
if not hasattr(cls, attr):
571-
aconf = cls._check_field_spec(attr)
572-
getter = cls._getter(aconf)
573-
doc = "a dictionary containing the %s in this %s container" % \
574-
(cls.__join(container_type), cls.__name__)
575-
setattr(cls, attr, property(getter, cls.__make_setter(aconf, add), None, doc))
576-
577-
# create the add method
578-
setattr(cls, add, cls.__make_add(add, attr, container_type))
579-
580-
# get create method name
581-
create = d.get('create')
582-
if create is not None:
583-
setattr(cls, create, cls.__make_create(create, add, container_type))
584-
585-
get = d.get('get')
586-
if get is not None:
587-
setattr(cls, get, cls.__make_get(get, attr, container_type))
588-
589-
if len(clsconf) == 1:
590-
setattr(cls, '__getitem__', cls.__make_getitem(attr, container_type))
591-
592-
# create the constructor, only if it has not been overridden
593-
# i.e. it is the same method as the parent class constructor
594-
if cls.__init__ == MultiContainerInterface.__init__:
595-
setattr(cls, '__init__', cls.__make_constructor(clsconf))
596-
597-
598-
class LabelledDict(dict):
599-
'''
600-
A dict wrapper class for aggregating Timeseries
601-
from the standard locations
602-
'''
603-
604-
@docval({'name': 'label', 'type': str, 'doc': 'the label on this dictionary'},
605-
{'name': 'def_key_name', 'type': str, 'doc': 'the default key name', 'default': 'name'})
606-
def __init__(self, **kwargs):
607-
label, def_key_name = getargs('label', 'def_key_name', kwargs)
608-
self.__label = label
609-
self.__defkey = def_key_name
610-
611-
@property
612-
def label(self):
613-
return self.__label
614-
615-
def __getitem__(self, args):
616-
key = args
617-
if '==' in args:
618-
key, val = args.split("==")
619-
key = key.strip()
620-
val = val.strip()
621-
if key != self.__defkey:
622-
ret = list()
623-
for item in self.values():
624-
if getattr(item, key, None) == val:
625-
ret.append(item)
626-
return ret if len(ret) else None
627-
key = val
628-
return super(LabelledDict, self).__getitem__(key)
629-
630-
@docval({'name': 'container', 'type': (NWBData, NWBContainer), 'doc': 'the container to add to this LabelledDict'})
631-
def add(self, **kwargs):
632-
'''
633-
Add a container to this LabelledDict
634-
'''
635-
container = getargs('container', kwargs)
636-
key = getattr(container, self.__defkey, None)
637-
if key is None:
638-
msg = "container '%s' does not have attribute '%s'" % (container.name, self.__defkey)
639-
raise ValueError(msg)
640-
self[key] = container
322+
pass

src/pynwb/file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ def __init__(self, **kwargs):
380380
def all_children(self):
381381
stack = [self]
382382
ret = list()
383-
self.__obj = LabelledDict(label='all_objects', def_key_name='object_id')
383+
self.__obj = LabelledDict(label='all_objects', key_attr='object_id')
384384
while len(stack):
385385
n = stack.pop()
386386
ret.append(n)

0 commit comments

Comments
 (0)