Coverage for python/lsst/daf/butler/core/storageClassDelegate.py: 20%
110 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-23 09:30 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-23 09:30 +0000
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", "StorageClassDelegate")
28import copy
29import logging
30from collections.abc import Iterable, Mapping
31from dataclasses import dataclass
32from typing import TYPE_CHECKING, Any
34from lsst.utils.introspection import get_full_type_name
36if TYPE_CHECKING:
37 from .storageClass import StorageClass
39log = logging.getLogger(__name__)
42@dataclass
43class DatasetComponent:
44 """Component of a dataset and associated information."""
46 name: str
47 """Name of the component.
48 """
50 storageClass: StorageClass
51 """StorageClass to be used when reading or writing this component.
52 """
54 component: Any
55 """Component extracted from the composite object.
56 """
59class StorageClassDelegate:
60 """Delegate class for StorageClass components and parameters.
62 This class delegates the handling of components and parameters for the
63 python type associated with a particular `StorageClass`.
65 A delegate is required for any storage class that defines components
66 (derived or otherwise) or support read parameters. It is used for
67 composite disassembly and assembly.
69 Attributes
70 ----------
71 storageClass : `StorageClass`
73 Parameters
74 ----------
75 storageClass : `StorageClass`
76 `StorageClass` to be used with this delegate.
77 """
79 def __init__(self, storageClass: StorageClass):
80 assert storageClass is not None
81 self.storageClass = storageClass
83 @staticmethod
84 def _attrNames(componentName: str, getter: bool = True) -> tuple[str, ...]:
85 """Return list of suitable attribute names to attempt to use.
87 Parameters
88 ----------
89 componentName : `str`
90 Name of component/attribute to look for.
91 getter : `bool`
92 If true, return getters, else return setters.
94 Returns
95 -------
96 attrs : `tuple(str)`
97 Tuple of strings to attempt.
98 """
99 root = "get" if getter else "set"
101 # Capitalized name for getXxx must only capitalize first letter and not
102 # downcase the rest. getVisitInfo and not getVisitinfo
103 first = componentName[0].upper()
104 if len(componentName) > 1:
105 tail = componentName[1:]
106 else:
107 tail = ""
108 capitalized = f"{root}{first}{tail}"
109 return (componentName, f"{root}_{componentName}", capitalized)
111 def assemble(self, components: dict[str, Any], pytype: type | None = None) -> Any:
112 """Construct an object from components based on storageClass.
114 This generic implementation assumes that instances of objects
115 can be created either by passing all the components to a constructor
116 or by calling setter methods with the name.
118 Parameters
119 ----------
120 components : `dict`
121 Collection of components from which to assemble a new composite
122 object. Keys correspond to composite names in the `StorageClass`.
123 pytype : `type`, optional
124 Override the type from the
125 :attr:`StorageClassDelegate.storageClass`
126 to use when assembling the final object.
128 Returns
129 -------
130 composite : `object`
131 New composite object assembled from components.
133 Raises
134 ------
135 ValueError
136 Some components could not be used to create the object or,
137 alternatively, some components were not defined in the associated
138 StorageClass.
139 """
140 if pytype is not None:
141 cls = pytype
142 else:
143 cls = self.storageClass.pytype
145 # Check that the storage class components are consistent
146 understood = set(self.storageClass.components)
147 requested = set(components.keys())
148 unknown = requested - understood
149 if unknown:
150 raise ValueError(f"Requested component(s) not known to StorageClass: {unknown}")
152 # First try to create an instance directly using keyword args
153 try:
154 obj = cls(**components)
155 except TypeError:
156 obj = None
158 # Now try to use setters if direct instantiation didn't work
159 if not obj:
160 obj = cls()
162 failed = []
163 for name, component in components.items():
164 if component is None:
165 continue
166 for attr in self._attrNames(name, getter=False):
167 if hasattr(obj, attr):
168 if attr == name: # Real attribute
169 setattr(obj, attr, component)
170 else:
171 setter = getattr(obj, attr)
172 setter(component)
173 break
174 else:
175 failed.append(name)
177 if failed:
178 raise ValueError(f"Unhandled components during assembly ({failed})")
180 return obj
182 def getComponent(self, composite: Any, componentName: str) -> Any:
183 """Attempt to retrieve component from composite object by heuristic.
185 Will attempt a direct attribute retrieval, or else getter methods of
186 the form "get_componentName" and "getComponentName".
188 Parameters
189 ----------
190 composite : `object`
191 Item to query for the component.
192 componentName : `str`
193 Name of component to retrieve.
195 Returns
196 -------
197 component : `object`
198 Component extracted from composite.
200 Raises
201 ------
202 AttributeError
203 The attribute could not be read from the composite.
204 """
205 component = None
207 if hasattr(composite, "__contains__") and componentName in composite:
208 component = composite[componentName]
209 return component
211 for attr in self._attrNames(componentName, getter=True):
212 if hasattr(composite, attr):
213 component = getattr(composite, attr)
214 if attr != componentName: # We have a method
215 component = component()
216 break
217 else:
218 raise AttributeError(f"Unable to get component {componentName}")
219 return component
221 def disassemble(
222 self, composite: Any, subset: Iterable | None = None, override: Any | None = None
223 ) -> dict[str, DatasetComponent]:
224 """Disassembler a composite.
226 This is a generic implementation of a disassembler.
227 This implementation attempts to extract components from the parent
228 by looking for attributes of the same name or getter methods derived
229 from the component name.
231 Parameters
232 ----------
233 composite : `object`
234 Parent composite object consisting of components to be extracted.
235 subset : iterable, optional
236 Iterable containing subset of components to extract from composite.
237 Must be a subset of those defined in
238 `StorageClassDelegate.storageClass`.
239 override : `object`, optional
240 Object to use for disassembly instead of parent. This can be useful
241 when called from subclasses that have composites in a hierarchy.
243 Returns
244 -------
245 components : `dict`
246 `dict` with keys matching the components defined in
247 `StorageClassDelegate.storageClass`
248 and values being `DatasetComponent` instances describing the
249 component.
251 Raises
252 ------
253 ValueError
254 A requested component can not be found in the parent using generic
255 lookups.
256 TypeError
257 The parent object does not match the supplied
258 `StorageClassDelegate.storageClass`.
259 """
260 if not self.storageClass.isComposite():
261 raise TypeError(
262 f"Can not disassemble something that is not a composite (storage class={self.storageClass})"
263 )
265 if not self.storageClass.validateInstance(composite):
266 raise TypeError(
267 "Unexpected type mismatch between parent and StorageClass "
268 f"({type(composite)} != {self.storageClass.pytype})"
269 )
271 requested = set(self.storageClass.components)
273 if subset is not None:
274 subset = set(subset)
275 diff = subset - requested
276 if diff:
277 raise ValueError(f"Requested subset is not a subset of supported components: {diff}")
278 requested = subset
280 if override is not None:
281 composite = override
283 components = {}
284 for c in list(requested):
285 # Try three different ways to get a value associated with the
286 # component name.
287 try:
288 component = self.getComponent(composite, c)
289 except AttributeError:
290 # Defer complaining so we get an idea of how many problems
291 # we have
292 pass
293 else:
294 # If we found a match store it in the results dict and remove
295 # it from the list of components we are still looking for.
296 if component is not None:
297 components[c] = DatasetComponent(c, self.storageClass.components[c], component)
298 requested.remove(c)
300 if requested:
301 raise ValueError(f"Unhandled components during disassembly ({requested})")
303 return components
305 def handleParameters(self, inMemoryDataset: Any, parameters: Mapping[str, Any] | None = None) -> Any:
306 """Modify the in-memory dataset using the supplied parameters.
308 Can return a possibly new object.
310 For safety, if any parameters are given to this method an
311 exception will be raised. This is to protect the user from
312 thinking that parameters have been applied when they have not been
313 applied.
315 Parameters
316 ----------
317 inMemoryDataset : `object`
318 Object to modify based on the parameters.
319 parameters : `dict`
320 Parameters to apply. Values are specific to the parameter.
321 Supported parameters are defined in the associated
322 `StorageClass`. If no relevant parameters are specified the
323 inMemoryDataset will be return unchanged.
325 Returns
326 -------
327 inMemoryDataset : `object`
328 Updated form of supplied in-memory dataset, after parameters
329 have been used.
331 Raises
332 ------
333 ValueError
334 Parameters have been provided to this default implementation.
335 """
336 if parameters:
337 raise ValueError(f"Parameters ({parameters}) provided to default implementation.")
339 return inMemoryDataset
341 @classmethod
342 def selectResponsibleComponent(cls, derivedComponent: str, fromComponents: set[str | None]) -> str:
343 """Select the best component for calculating a derived component.
345 Given a possible set of components to choose from, return the
346 component that should be used to calculate the requested derived
347 component.
349 Parameters
350 ----------
351 derivedComponent : `str`
352 The derived component that is being requested.
353 fromComponents : `set` of `str`
354 The available set of component options from which that derived
355 component can be computed. `None` can be included but should
356 be ignored.
358 Returns
359 -------
360 required : `str`
361 The component that should be used.
363 Raises
364 ------
365 NotImplementedError
366 Raised if this delegate refuses to answer the question.
367 ValueError
368 Raised if this delegate can not determine a relevant component
369 from the supplied options.
370 """
371 raise NotImplementedError("This delegate does not support derived components")
373 def copy(self, inMemoryDataset: Any) -> Any:
374 """Copy the supplied python type and return the copy.
376 Parameters
377 ----------
378 inMemoryDataset : `object`
379 Object to copy.
381 Returns
382 -------
383 copied : `object`
384 A copy of the supplied object. Can be the same object if the
385 object is known to be read-only.
387 Raises
388 ------
389 NotImplementedError
390 Raised if none of the default methods for copying work.
392 Notes
393 -----
394 The default implementation uses `copy.deepcopy()`.
395 It is generally expected that this method is the equivalent of a deep
396 copy. Subclasses can override this method if they already know the
397 optimal approach for deep copying.
398 """
399 try:
400 return copy.deepcopy(inMemoryDataset)
401 except Exception as e:
402 raise NotImplementedError(
403 f"Unable to deep copy the supplied python type ({get_full_type_name(inMemoryDataset)}) "
404 f"using default methods ({e})"
405 )