Coverage for python/lsst/daf/butler/core/assembler.py : 10%

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/>.
22"""Support for reading and writing composite objects."""
24__all__ = ("DatasetComponent", "CompositeAssembler")
26import collections
27import logging
29log = logging.getLogger(__name__)
32class DatasetComponent:
34 """Component of a dataset and associated information.
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.
45 """
47 def __init__(self, name, storageClass, component):
48 self.name = name
49 self.storageClass = storageClass
50 self.component = component
53class CompositeAssembler:
54 """Class for providing assembler and disassembler support for composites.
56 Attributes
57 ----------
58 storageClass : `StorageClass`
60 Parameters
61 ----------
62 storageClass : `StorageClass`
63 `StorageClass` to be used with this assembler.
64 """
66 def __init__(self, storageClass):
67 self.storageClass = storageClass
69 @staticmethod
70 def _attrNames(componentName, getter=True):
71 """Return list of suitable attribute names to attempt to use.
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.
80 Returns
81 -------
82 attrs : `tuple(str)`
83 Tuple of strings to attempt.
84 """
85 root = "get" if getter else "set"
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)
97 def assemble(self, components, pytype=None):
98 """Construct an object from components based on storageClass.
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.
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.
113 Returns
114 -------
115 composite : `object`
116 New composite object assembled from components.
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
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))
137 # First try to create an instance directly using keyword args
138 try:
139 obj = cls(**components)
140 except TypeError:
141 obj = None
143 # Now try to use setters if direct instantiation didn't work
144 if not obj:
145 obj = cls()
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)
162 if failed:
163 raise ValueError("Unhandled components during assembly ({})".format(failed))
165 return obj
167 def getValidComponents(self, composite):
168 """Extract all non-None components from a composite.
170 Parameters
171 ----------
172 composite : `object`
173 Composite from which to extract components.
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
197 def getComponent(self, composite, componentName):
198 """Attempt to retrieve component from composite object by heuristic.
200 Will attempt a direct attribute retrieval, or else getter methods of
201 the form "get_componentName" and "getComponentName".
203 Parameters
204 ----------
205 composite : `object`
206 Item to query for the component.
207 componentName : `str`
208 Name of component to retrieve.
210 Returns
211 -------
212 component : `object`
213 Component extracted from composite.
215 Raises
216 ------
217 AttributeError
218 The attribute could not be read from the composite.
219 """
220 component = None
222 if hasattr(composite, "__contains__") and componentName in composite:
223 component = composite[componentName]
224 return component
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
236 def disassemble(self, composite, subset=None, override=None):
237 """Generic implementation of a disassembler.
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.
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.
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`.
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
276 if not self.storageClass.validateInstance(composite):
277 raise TypeError("Unexpected type mismatch between parent and StorageClass"
278 " ({} != {})".format(type(composite), self.storageClass.pytype))
280 requested = set(self.storageClass.components)
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
289 if override is not None:
290 composite = override
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)
309 if requested:
310 raise ValueError("Unhandled components during disassembly ({})".format(requested))
312 return components
314 def handleParameters(self, inMemoryDataset, parameters=None):
315 """Modify the in-memory dataset using the supplied parameters,
316 returning a possibly new object.
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.
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.
333 Returns
334 -------
335 inMemoryDataset : `object`
336 Updated form of supplied in-memory dataset, after parameters
337 have been used.
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.")
347 return inMemoryDataset