Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22"""Support for reading and writing composite objects.""" 

23 

24__all__ = ("DatasetComponent", "CompositeAssembler") 

25 

26import collections 

27import logging 

28 

29log = logging.getLogger(__name__) 

30 

31 

32class DatasetComponent: 

33 

34 """Component of a dataset and associated information. 

35 

36 Parameters 

37 ---------- 

38 name : `str` 

39 Name of the component. 

40 storageClass : `StorageClass` 

41 StorageClass to be used when reading or writing this component. 

42 component : `object` 

43 Component extracted from the composite object. 

44 

45 """ 

46 

47 def __init__(self, name, storageClass, component): 

48 self.name = name 

49 self.storageClass = storageClass 

50 self.component = component 

51 

52 

53class CompositeAssembler: 

54 """Class for providing assembler and disassembler support for composites. 

55 

56 Attributes 

57 ---------- 

58 storageClass : `StorageClass` 

59 

60 Parameters 

61 ---------- 

62 storageClass : `StorageClass` 

63 `StorageClass` to be used with this assembler. 

64 """ 

65 

66 def __init__(self, storageClass): 

67 self.storageClass = storageClass 

68 

69 @staticmethod 

70 def _attrNames(componentName, getter=True): 

71 """Return list of suitable attribute names to attempt to use. 

72 

73 Parameters 

74 ---------- 

75 componentName : `str` 

76 Name of component/attribute to look for. 

77 getter : `bool` 

78 If true, return getters, else return setters. 

79 

80 Returns 

81 ------- 

82 attrs : `tuple(str)` 

83 Tuple of strings to attempt. 

84 """ 

85 root = "get" if getter else "set" 

86 

87 # Capitalized name for getXxx must only capitalize first letter and not 

88 # downcase the rest. getVisitInfo and not getVisitinfo 

89 first = componentName[0].upper() 

90 if len(componentName) > 1: 

91 tail = componentName[1:] 

92 else: 

93 tail = "" 

94 capitalized = "{}{}{}".format(root, first, tail) 

95 return (componentName, "{}_{}".format(root, componentName), capitalized) 

96 

97 def assemble(self, components, pytype=None): 

98 """Construct an object from components based on storageClass. 

99 

100 This generic implementation assumes that instances of objects 

101 can be created either by passing all the components to a constructor 

102 or by calling setter methods with the name. 

103 

104 Parameters 

105 ---------- 

106 components : `dict` 

107 Collection of components from which to assemble a new composite 

108 object. Keys correspond to composite names in the `StorageClass`. 

109 pytype : `type`, optional 

110 Override the type from the :attr:`CompositeAssembler.storageClass` 

111 to use when assembling the final object. 

112 

113 Returns 

114 ------- 

115 composite : `object` 

116 New composite object assembled from components. 

117 

118 Raises 

119 ------ 

120 ValueError 

121 Some components could not be used to create the object or, 

122 alternatively, some components were not defined in the associated 

123 StorageClass. 

124 """ 

125 if pytype is not None: 

126 cls = pytype 

127 else: 

128 cls = self.storageClass.pytype 

129 

130 # Check that the storage class components are consistent 

131 understood = set(self.storageClass.components) 

132 requested = set(components.keys()) 

133 unknown = requested - understood 

134 if unknown: 

135 raise ValueError("Requested component(s) not known to StorageClass: {}".format(unknown)) 

136 

137 # First try to create an instance directly using keyword args 

138 try: 

139 obj = cls(**components) 

140 except TypeError: 

141 obj = None 

142 

143 # Now try to use setters if direct instantiation didn't work 

144 if not obj: 

145 obj = cls() 

146 

147 failed = [] 

148 for name, component in components.items(): 

149 if component is None: 

150 continue 

151 for attr in self._attrNames(name, getter=False): 

152 if hasattr(obj, attr): 

153 if attr == name: # Real attribute 

154 setattr(obj, attr, component) 

155 else: 

156 setter = getattr(obj, attr) 

157 setter(component) 

158 break 

159 else: 

160 failed.append(name) 

161 

162 if failed: 

163 raise ValueError("Unhandled components during assembly ({})".format(failed)) 

164 

165 return obj 

166 

167 def getValidComponents(self, composite): 

168 """Extract all non-None components from a composite. 

169 

170 Parameters 

171 ---------- 

172 composite : `object` 

173 Composite from which to extract components. 

174 

175 Returns 

176 ------- 

177 comps : `dict` 

178 Non-None components extracted from the composite, indexed by the 

179 component name as derived from the 

180 `CompositeAssembler.storageClass`. 

181 """ 

182 components = {} 

183 if self.storageClass is not None and self.storageClass.isComposite(): 

184 for c in self.storageClass.components: 

185 if isinstance(composite, collections.abc.Mapping): 

186 comp = composite[c] 

187 else: 

188 try: 

189 comp = self.getComponent(composite, c) 

190 except AttributeError: 

191 pass 

192 else: 

193 if comp is not None: 

194 components[c] = comp 

195 return components 

196 

197 def getComponent(self, composite, componentName): 

198 """Attempt to retrieve component from composite object by heuristic. 

199 

200 Will attempt a direct attribute retrieval, or else getter methods of 

201 the form "get_componentName" and "getComponentName". 

202 

203 Parameters 

204 ---------- 

205 composite : `object` 

206 Item to query for the component. 

207 componentName : `str` 

208 Name of component to retrieve. 

209 

210 Returns 

211 ------- 

212 component : `object` 

213 Component extracted from composite. 

214 

215 Raises 

216 ------ 

217 AttributeError 

218 The attribute could not be read from the composite. 

219 """ 

220 component = None 

221 

222 if hasattr(composite, "__contains__") and componentName in composite: 

223 component = composite[componentName] 

224 return component 

225 

226 for attr in self._attrNames(componentName, getter=True): 

227 if hasattr(composite, attr): 

228 component = getattr(composite, attr) 

229 if attr != componentName: # We have a method 

230 component = component() 

231 break 

232 else: 

233 raise AttributeError("Unable to get component {}".format(componentName)) 

234 return component 

235 

236 def disassemble(self, composite, subset=None, override=None): 

237 """Generic implementation of a disassembler. 

238 

239 This implementation attempts to extract components from the parent 

240 by looking for attributes of the same name or getter methods derived 

241 from the component name. 

242 

243 Parameters 

244 ---------- 

245 composite : `object` 

246 Parent composite object consisting of components to be extracted. 

247 subset : iterable, optional 

248 Iterable containing subset of components to extract from composite. 

249 Must be a subset of those defined in 

250 `CompositeAssembler.storageClass`. 

251 override : `object`, optional 

252 Object to use for disassembly instead of parent. This can be useful 

253 when called from subclasses that have composites in a hierarchy. 

254 

255 Returns 

256 ------- 

257 components : `dict` 

258 `dict` with keys matching the components defined in 

259 `CompositeAssembler.storageClass` 

260 and values being `DatasetComponent` instances describing the 

261 component. Returns None if this is not a composite 

262 `CompositeAssembler.storageClass`. 

263 

264 Raises 

265 ------ 

266 ValueError 

267 A requested component can not be found in the parent using generic 

268 lookups. 

269 TypeError 

270 The parent object does not match the supplied 

271 `CompositeAssembler.storageClass`. 

272 """ 

273 if self.storageClass.components is None: 

274 return 

275 

276 if not self.storageClass.validateInstance(composite): 

277 raise TypeError("Unexpected type mismatch between parent and StorageClass" 

278 " ({} != {})".format(type(composite), self.storageClass.pytype)) 

279 

280 requested = set(self.storageClass.components) 

281 

282 if subset is not None: 

283 subset = set(subset) 

284 diff = subset - requested 

285 if diff: 

286 raise ValueError("Requested subset is not a subset of supported components: {}".format(diff)) 

287 requested = subset 

288 

289 if override is not None: 

290 composite = override 

291 

292 components = {} 

293 for c in list(requested): 

294 # Try three different ways to get a value associated with the 

295 # component name. 

296 try: 

297 component = self.getComponent(composite, c) 

298 except AttributeError: 

299 # Defer complaining so we get an idea of how many problems 

300 # we have 

301 pass 

302 else: 

303 # If we found a match store it in the results dict and remove 

304 # it from the list of components we are still looking for. 

305 if component is not None: 

306 components[c] = DatasetComponent(c, self.storageClass.components[c], component) 

307 requested.remove(c) 

308 

309 if requested: 

310 raise ValueError("Unhandled components during disassembly ({})".format(requested)) 

311 

312 return components 

313 

314 def handleParameters(self, inMemoryDataset, parameters=None): 

315 """Modify the in-memory dataset using the supplied parameters, 

316 returning a possibly new object. 

317 

318 For safety, if any parameters are given to this method an 

319 exception will be raised. This is to protect the user from 

320 thinking that parameters have been applied when they have not been 

321 applied. 

322 

323 Parameters 

324 ---------- 

325 inMemoryDataset : `object` 

326 Object to modify based on the parameters. 

327 parameters : `dict` 

328 Parameters to apply. Values are specific to the parameter. 

329 Supported parameters are defined in the associated 

330 `StorageClass`. If no relevant parameters are specified the 

331 inMemoryDataset will be return unchanged. 

332 

333 Returns 

334 ------- 

335 inMemoryDataset : `object` 

336 Updated form of supplied in-memory dataset, after parameters 

337 have been used. 

338 

339 Raises 

340 ------ 

341 ValueError 

342 Parameters have been provided to this default implementation. 

343 """ 

344 if parameters: 

345 raise ValueError(f"Parameters ({parameters}) provided to default implementation.") 

346 

347 return inMemoryDataset