Coverage for src/hdmf/spec/catalog.py: 96%

110 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-07-10 23:48 +0000

1import copy 

2from collections import OrderedDict 

3 

4from .spec import BaseStorageSpec, GroupSpec 

5from ..utils import docval, getargs 

6 

7 

8class SpecCatalog: 

9 

10 def __init__(self): 

11 ''' 

12 Create a new catalog for storing specifications 

13 

14 ** Private Instance Variables ** 

15 

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() 

28 

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 

49 

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) 

58 

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()) 

66 

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) 

79 

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) 

104 

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. 

112 

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) 

134 

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() 

145 

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 

152 

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) 

159 

160 return type_hierarchy 

161 

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. 

172 

173 E.g., assume we have the following inheritance hierarchy:: 

174 

175 -BaseContainer--+-->AContainer--->ADContainer 

176 | 

177 +-->BContainer 

178 

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 () 

197 

198 def __copy__(self): 

199 ret = SpecCatalog() 

200 ret.__specs = copy.copy(self.__specs) 

201 return ret 

202 

203 def __deepcopy__(self, memo): 

204 ret = SpecCatalog() 

205 ret.__specs = copy.deepcopy(self.__specs, memo) 

206 return ret