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

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."""
24from __future__ import annotations
26__all__ = ("DatasetComponent", "CompositeAssembler")
28import collections
29from dataclasses import dataclass
30import logging
31from typing import (
32 Any,
33 TYPE_CHECKING,
34)
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
39log = logging.getLogger(__name__)
42@dataclass
43class DatasetComponent:
44 """Component of a dataset and associated information.
45 """
47 name: str
48 """Name of the component.
49 """
51 storageClass: StorageClass
52 """StorageClass to be used when reading or writing this component.
53 """
55 component: Any
56 """Component extracted from the composite object.
57 """
60class CompositeAssembler:
61 """Class for providing assembler and disassembler support for composites.
63 Attributes
64 ----------
65 storageClass : `StorageClass`
67 Parameters
68 ----------
69 storageClass : `StorageClass`
70 `StorageClass` to be used with this assembler.
71 """
73 def __init__(self, storageClass):
74 self.storageClass = storageClass
76 @staticmethod
77 def _attrNames(componentName, getter=True):
78 """Return list of suitable attribute names to attempt to use.
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.
87 Returns
88 -------
89 attrs : `tuple(str)`
90 Tuple of strings to attempt.
91 """
92 root = "get" if getter else "set"
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)
104 def assemble(self, components, pytype=None):
105 """Construct an object from components based on storageClass.
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.
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.
120 Returns
121 -------
122 composite : `object`
123 New composite object assembled from components.
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
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))
144 # First try to create an instance directly using keyword args
145 try:
146 obj = cls(**components)
147 except TypeError:
148 obj = None
150 # Now try to use setters if direct instantiation didn't work
151 if not obj:
152 obj = cls()
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)
169 if failed:
170 raise ValueError("Unhandled components during assembly ({})".format(failed))
172 return obj
174 def getValidComponents(self, composite):
175 """Extract all non-None components from a composite.
177 Parameters
178 ----------
179 composite : `object`
180 Composite from which to extract components.
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
204 def getComponent(self, composite, componentName):
205 """Attempt to retrieve component from composite object by heuristic.
207 Will attempt a direct attribute retrieval, or else getter methods of
208 the form "get_componentName" and "getComponentName".
210 Parameters
211 ----------
212 composite : `object`
213 Item to query for the component.
214 componentName : `str`
215 Name of component to retrieve.
217 Returns
218 -------
219 component : `object`
220 Component extracted from composite.
222 Raises
223 ------
224 AttributeError
225 The attribute could not be read from the composite.
226 """
227 component = None
229 if hasattr(composite, "__contains__") and componentName in composite:
230 component = composite[componentName]
231 return component
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
243 def disassemble(self, composite, subset=None, override=None):
244 """Generic implementation of a disassembler.
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.
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.
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`.
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
283 if not self.storageClass.validateInstance(composite):
284 raise TypeError("Unexpected type mismatch between parent and StorageClass"
285 " ({} != {})".format(type(composite), self.storageClass.pytype))
287 requested = set(self.storageClass.components)
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
296 if override is not None:
297 composite = override
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)
316 if requested:
317 raise ValueError("Unhandled components during disassembly ({})".format(requested))
319 return components
321 def handleParameters(self, inMemoryDataset, parameters=None):
322 """Modify the in-memory dataset using the supplied parameters,
323 returning a possibly new object.
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.
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.
340 Returns
341 -------
342 inMemoryDataset : `object`
343 Updated form of supplied in-memory dataset, after parameters
344 have been used.
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.")
354 return inMemoryDataset