Coverage for src/hdmf/spec/catalog.py: 96%
110 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-08-18 20:49 +0000
« prev ^ index » next coverage.py v7.2.5, created at 2023-08-18 20:49 +0000
1import copy
2from collections import OrderedDict
4from .spec import BaseStorageSpec, GroupSpec
5from ..utils import docval, getargs
8class SpecCatalog:
10 def __init__(self):
11 '''
12 Create a new catalog for storing specifications
14 ** Private Instance Variables **
16 :ivar __specs: Dict with the specification of each registered type
17 :ivar __parent_types: Dict with parent types for each registered type
18 :ivar __spec_source_files: Dict with the path to the source files (if available) for each registered type
19 :ivar __hierarchy: Dict describing the hierarchy for each registered type.
20 NOTE: Always use SpecCatalog.get_hierarchy(...) to retrieve the hierarchy
21 as this dictionary is used like a cache, i.e., to avoid repeated calculation
22 of the hierarchy but the contents are computed on first request by SpecCatalog.get_hierarchy(...)
23 '''
24 self.__specs = OrderedDict()
25 self.__parent_types = dict()
26 self.__hierarchy = dict()
27 self.__spec_source_files = dict()
29 @docval({'name': 'spec', 'type': BaseStorageSpec, 'doc': 'a Spec object'},
30 {'name': 'source_file', 'type': str,
31 'doc': 'path to the source file from which the spec was loaded', 'default': None})
32 def register_spec(self, **kwargs):
33 '''
34 Associate a specified object type with a specification
35 '''
36 spec, source_file = getargs('spec', 'source_file', kwargs)
37 ndt = spec.data_type_inc
38 ndt_def = spec.data_type_def
39 if ndt_def is None: 39 ↛ 40line 39 didn't jump to line 40, because the condition on line 39 was never true
40 raise ValueError('cannot register spec that has no data_type_def')
41 if ndt_def != ndt: 41 ↛ 43line 41 didn't jump to line 43, because the condition on line 41 was never false
42 self.__parent_types[ndt_def] = ndt
43 type_name = ndt_def if ndt_def is not None else ndt
44 if type_name in self.__specs:
45 if self.__specs[type_name] != spec or self.__spec_source_files[type_name] != source_file:
46 raise ValueError("'%s' - cannot overwrite existing specification" % type_name)
47 self.__specs[type_name] = spec
48 self.__spec_source_files[type_name] = source_file
50 @docval({'name': 'data_type', 'type': str, 'doc': 'the data_type to get the Spec for'},
51 returns="the specification for writing the given object type to HDF5 ", rtype='Spec')
52 def get_spec(self, **kwargs):
53 '''
54 Get the Spec object for the given type
55 '''
56 data_type = getargs('data_type', kwargs)
57 return self.__specs.get(data_type, None)
59 @docval(rtype=tuple)
60 def get_registered_types(self, **kwargs):
61 '''
62 Return all registered specifications
63 '''
64 # kwargs is not used here but is used by docval
65 return tuple(self.__specs.keys())
67 @docval({'name': 'data_type', 'type': str, 'doc': 'the data_type of the spec to get the source file for'},
68 returns="the path to source specification file from which the spec was originally loaded or None ",
69 rtype='str')
70 def get_spec_source_file(self, **kwargs):
71 '''
72 Return the path to the source file from which the spec for the given
73 type was loaded from. None is returned if no file path is available
74 for the spec. Note: The spec in the file may not be identical to the
75 object in case the spec is modified after load.
76 '''
77 data_type = getargs('data_type', kwargs)
78 return self.__spec_source_files.get(data_type, None)
80 @docval({'name': 'spec', 'type': BaseStorageSpec, 'doc': 'the Spec object to register'},
81 {'name': 'source_file',
82 'type': str,
83 'doc': 'path to the source file from which the spec was loaded', 'default': None},
84 rtype=tuple, returns='the types that were registered with this spec')
85 def auto_register(self, **kwargs):
86 '''
87 Register this specification and all sub-specification using data_type as object type name
88 '''
89 spec, source_file = getargs('spec', 'source_file', kwargs)
90 ndt = spec.data_type_def
91 ret = list()
92 if ndt is not None:
93 self.register_spec(spec, source_file)
94 ret.append(ndt)
95 if isinstance(spec, GroupSpec):
96 for dataset_spec in spec.datasets:
97 dset_ndt = dataset_spec.data_type_def
98 if dset_ndt is not None and not spec.is_inherited_type(dataset_spec):
99 ret.append(dset_ndt)
100 self.register_spec(dataset_spec, source_file)
101 for group_spec in spec.groups:
102 ret.extend(self.auto_register(group_spec, source_file))
103 return tuple(ret)
105 @docval({'name': 'data_type', 'type': (str, type),
106 'doc': 'the data_type to get the hierarchy of'},
107 returns="Tuple of strings with the names of the types the given data_type inherits from.",
108 rtype=tuple)
109 def get_hierarchy(self, **kwargs):
110 """
111 For a given type get the type inheritance hierarchy for that type.
113 E.g., if we have a type MyContainer that inherits from BaseContainer then
114 the result will be a tuple with the strings ('MyContainer', 'BaseContainer')
115 """
116 data_type = getargs('data_type', kwargs)
117 if isinstance(data_type, type): 117 ↛ 118line 117 didn't jump to line 118, because the condition on line 117 was never true
118 data_type = data_type.__name__
119 ret = self.__hierarchy.get(data_type)
120 if ret is None:
121 hierarchy = list()
122 parent = data_type
123 while parent is not None:
124 hierarchy.append(parent)
125 parent = self.__parent_types.get(parent)
126 # store the computed hierarchy for data_type and all types in between it and
127 # the top of the hierarchy
128 tmp_hier = tuple(hierarchy)
129 ret = tmp_hier
130 while len(tmp_hier) > 0:
131 self.__hierarchy[tmp_hier[0]] = tmp_hier
132 tmp_hier = tmp_hier[1:]
133 return tuple(ret)
135 @docval(returns="Hierarchically nested OrderedDict with the hierarchy of all the types",
136 rtype=OrderedDict)
137 def get_full_hierarchy(self):
138 """
139 Get the complete hierarchy of all types. The function attempts to sort types by name using
140 standard Python sorted.
141 """
142 # Get the list of all types
143 registered_types = self.get_registered_types()
144 type_hierarchy = OrderedDict()
146 # Internal helper function to recursively construct the hierarchy of types
147 def get_type_hierarchy(data_type, spec_catalog):
148 dtype_hier = OrderedDict()
149 for dtype in sorted(self.get_subtypes(data_type=data_type, recursive=False)):
150 dtype_hier[dtype] = get_type_hierarchy(dtype, spec_catalog)
151 return dtype_hier
153 # Compute the type hierarchy
154 for rt in sorted(registered_types):
155 rt_spec = self.get_spec(rt)
156 if isinstance(rt_spec, BaseStorageSpec): # Only BaseStorageSpec have data_type_inc/def keys 156 ↛ 154line 156 didn't jump to line 154, because the condition on line 156 was never false
157 if rt_spec.get(rt_spec.inc_key(), None) is None:
158 type_hierarchy[rt] = get_type_hierarchy(rt, self)
160 return type_hierarchy
162 @docval({'name': 'data_type', 'type': (str, type),
163 'doc': 'the data_type to get the subtypes for'},
164 {'name': 'recursive', 'type': bool,
165 'doc': 'recursively get all subtypes. Set to False to only get the direct subtypes',
166 'default': True},
167 returns="Tuple of strings with the names of all types of the given data_type.",
168 rtype=tuple)
169 def get_subtypes(self, **kwargs):
170 """
171 For a given data type recursively find all the subtypes that inherit from it.
173 E.g., assume we have the following inheritance hierarchy::
175 -BaseContainer--+-->AContainer--->ADContainer
176 |
177 +-->BContainer
179 In this case, the subtypes of BaseContainer would be (AContainer, ADContainer, BContainer),
180 the subtypes of AContainer would be (ADContainer), and the subtypes of BContainer would be empty ().
181 """
182 data_type, recursive = getargs('data_type', 'recursive', kwargs)
183 curr_spec = self.get_spec(data_type)
184 if isinstance(curr_spec, BaseStorageSpec): # Only BaseStorageSpec have data_type_inc/def keys
185 subtypes = []
186 spec_inc_key = curr_spec.inc_key()
187 spec_def_key = curr_spec.def_key()
188 for rt in self.get_registered_types():
189 rt_spec = self.get_spec(rt)
190 if rt_spec.get(spec_inc_key, None) == data_type and rt_spec.get(spec_def_key, None) != data_type:
191 subtypes.append(rt)
192 if recursive:
193 subtypes += self.get_subtypes(rt)
194 return tuple(set(subtypes)) # Convert to a set to make sure we don't have any duplicates
195 else:
196 return ()
198 def __copy__(self):
199 ret = SpecCatalog()
200 ret.__specs = copy.copy(self.__specs)
201 return ret
203 def __deepcopy__(self, memo):
204 ret = SpecCatalog()
205 ret.__specs = copy.deepcopy(self.__specs, memo)
206 return ret