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 

24from __future__ import annotations 

25 

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

27 

28import collections 

29from dataclasses import dataclass 

30import logging 

31from typing import ( 

32 Any, 

33 TYPE_CHECKING, 

34) 

35 

36if TYPE_CHECKING: 36 ↛ 37line 36 didn't jump to line 37, because the condition on line 36 was never true

37 from .storageClass import StorageClass 

38 

39log = logging.getLogger(__name__) 

40 

41 

42@dataclass 

43class DatasetComponent: 

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

45 """ 

46 

47 name: str 

48 """Name of the component. 

49 """ 

50 

51 storageClass: StorageClass 

52 """StorageClass to be used when reading or writing this component. 

53 """ 

54 

55 component: Any 

56 """Component extracted from the composite object. 

57 """ 

58 

59 

60class CompositeAssembler: 

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

62 

63 Attributes 

64 ---------- 

65 storageClass : `StorageClass` 

66 

67 Parameters 

68 ---------- 

69 storageClass : `StorageClass` 

70 `StorageClass` to be used with this assembler. 

71 """ 

72 

73 def __init__(self, storageClass): 

74 self.storageClass = storageClass 

75 

76 @staticmethod 

77 def _attrNames(componentName, getter=True): 

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

79 

80 Parameters 

81 ---------- 

82 componentName : `str` 

83 Name of component/attribute to look for. 

84 getter : `bool` 

85 If true, return getters, else return setters. 

86 

87 Returns 

88 ------- 

89 attrs : `tuple(str)` 

90 Tuple of strings to attempt. 

91 """ 

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

93 

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

95 # downcase the rest. getVisitInfo and not getVisitinfo 

96 first = componentName[0].upper() 

97 if len(componentName) > 1: 

98 tail = componentName[1:] 

99 else: 

100 tail = "" 

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

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

103 

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

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

106 

107 This generic implementation assumes that instances of objects 

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

109 or by calling setter methods with the name. 

110 

111 Parameters 

112 ---------- 

113 components : `dict` 

114 Collection of components from which to assemble a new composite 

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

116 pytype : `type`, optional 

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

118 to use when assembling the final object. 

119 

120 Returns 

121 ------- 

122 composite : `object` 

123 New composite object assembled from components. 

124 

125 Raises 

126 ------ 

127 ValueError 

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

129 alternatively, some components were not defined in the associated 

130 StorageClass. 

131 """ 

132 if pytype is not None: 

133 cls = pytype 

134 else: 

135 cls = self.storageClass.pytype 

136 

137 # Check that the storage class components are consistent 

138 understood = set(self.storageClass.components) 

139 requested = set(components.keys()) 

140 unknown = requested - understood 

141 if unknown: 

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

143 

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

145 try: 

146 obj = cls(**components) 

147 except TypeError: 

148 obj = None 

149 

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

151 if not obj: 

152 obj = cls() 

153 

154 failed = [] 

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

156 if component is None: 

157 continue 

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

159 if hasattr(obj, attr): 

160 if attr == name: # Real attribute 

161 setattr(obj, attr, component) 

162 else: 

163 setter = getattr(obj, attr) 

164 setter(component) 

165 break 

166 else: 

167 failed.append(name) 

168 

169 if failed: 

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

171 

172 return obj 

173 

174 def getValidComponents(self, composite): 

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

176 

177 Parameters 

178 ---------- 

179 composite : `object` 

180 Composite from which to extract components. 

181 

182 Returns 

183 ------- 

184 comps : `dict` 

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

186 component name as derived from the 

187 `CompositeAssembler.storageClass`. 

188 """ 

189 components = {} 

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

191 for c in self.storageClass.components: 

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

193 comp = composite[c] 

194 else: 

195 try: 

196 comp = self.getComponent(composite, c) 

197 except AttributeError: 

198 pass 

199 else: 

200 if comp is not None: 

201 components[c] = comp 

202 return components 

203 

204 def getComponent(self, composite, componentName): 

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

206 

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

208 the form "get_componentName" and "getComponentName". 

209 

210 Parameters 

211 ---------- 

212 composite : `object` 

213 Item to query for the component. 

214 componentName : `str` 

215 Name of component to retrieve. 

216 

217 Returns 

218 ------- 

219 component : `object` 

220 Component extracted from composite. 

221 

222 Raises 

223 ------ 

224 AttributeError 

225 The attribute could not be read from the composite. 

226 """ 

227 component = None 

228 

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

230 component = composite[componentName] 

231 return component 

232 

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

234 if hasattr(composite, attr): 

235 component = getattr(composite, attr) 

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

237 component = component() 

238 break 

239 else: 

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

241 return component 

242 

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

244 """Generic implementation of a disassembler. 

245 

246 This implementation attempts to extract components from the parent 

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

248 from the component name. 

249 

250 Parameters 

251 ---------- 

252 composite : `object` 

253 Parent composite object consisting of components to be extracted. 

254 subset : iterable, optional 

255 Iterable containing subset of components to extract from composite. 

256 Must be a subset of those defined in 

257 `CompositeAssembler.storageClass`. 

258 override : `object`, optional 

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

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

261 

262 Returns 

263 ------- 

264 components : `dict` 

265 `dict` with keys matching the components defined in 

266 `CompositeAssembler.storageClass` 

267 and values being `DatasetComponent` instances describing the 

268 component. Returns None if this is not a composite 

269 `CompositeAssembler.storageClass`. 

270 

271 Raises 

272 ------ 

273 ValueError 

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

275 lookups. 

276 TypeError 

277 The parent object does not match the supplied 

278 `CompositeAssembler.storageClass`. 

279 """ 

280 if self.storageClass.components is None: 

281 return 

282 

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

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

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

286 

287 requested = set(self.storageClass.components) 

288 

289 if subset is not None: 

290 subset = set(subset) 

291 diff = subset - requested 

292 if diff: 

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

294 requested = subset 

295 

296 if override is not None: 

297 composite = override 

298 

299 components = {} 

300 for c in list(requested): 

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

302 # component name. 

303 try: 

304 component = self.getComponent(composite, c) 

305 except AttributeError: 

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

307 # we have 

308 pass 

309 else: 

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

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

312 if component is not None: 

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

314 requested.remove(c) 

315 

316 if requested: 

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

318 

319 return components 

320 

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

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

323 returning a possibly new object. 

324 

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

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

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

328 applied. 

329 

330 Parameters 

331 ---------- 

332 inMemoryDataset : `object` 

333 Object to modify based on the parameters. 

334 parameters : `dict` 

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

336 Supported parameters are defined in the associated 

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

338 inMemoryDataset will be return unchanged. 

339 

340 Returns 

341 ------- 

342 inMemoryDataset : `object` 

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

344 have been used. 

345 

346 Raises 

347 ------ 

348 ValueError 

349 Parameters have been provided to this default implementation. 

350 """ 

351 if parameters: 

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

353 

354 return inMemoryDataset