Coverage for python/lsst/daf/butler/mapping_factory.py: 26%
60 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-06 10:53 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-06 10:53 +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 software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28from __future__ import annotations
30__all__ = ("MappingFactory",)
32from collections.abc import Iterable
33from typing import Any
35from lsst.utils.introspection import get_class_of
37from ._config import Config
38from ._config_support import LookupKey
41class MappingFactory:
42 """Register the mapping of some key to a python type and retrieve
43 instances.
45 Enables instances of these classes to be retrieved from the factory later.
46 The class can be specified as an object, class or string.
47 If the key is an object it is converted to a string by accessing
48 a ``name`` attribute.
50 Parameters
51 ----------
52 refType : `type`
53 Python reference `type` to use to ensure that items stored in the
54 registry create instance objects of the correct class. Subclasses
55 of this type are allowed. Using `None` disables the check.
57 """
59 def __init__(self, refType: type):
60 self._registry: dict[LookupKey, dict[str, Any]] = {}
61 self.refType = refType
63 def __contains__(self, key: Any) -> bool:
64 """Indicate whether the supplied key is present in the factory.
66 Parameters
67 ----------
68 key : `LookupKey`, `str` or objects with ``name`` attribute
69 Key to use to lookup whether a corresponding element exists
70 in this factory.
72 Returns
73 -------
74 in : `bool`
75 `True` if the supplied key is present in the factory.
76 """
77 key = self._getNameKey(key)
78 return key in self._registry
80 def getLookupKeys(self) -> set[LookupKey]:
81 """Retrieve the look up keys for all the registry entries.
83 Returns
84 -------
85 keys : `set` of `LookupKey`
86 The keys available for matching in the registry.
87 """
88 return set(self._registry)
90 def getClassFromRegistryWithMatch(
91 self, targetClasses: Iterable[Any]
92 ) -> tuple[LookupKey, type, dict[Any, Any]]:
93 """Get the class stored in the registry along with the matching key.
95 Parameters
96 ----------
97 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
98 Each item is tested in turn until a match is found in the registry.
99 Items with `None` value are skipped.
101 Returns
102 -------
103 matchKey : `LookupKey`
104 The key that resulted in the successful match.
105 cls : `type`
106 Class stored in registry associated with the first
107 matching target class.
108 kwargs: `dict`
109 Keyword arguments to be given to constructor.
111 Raises
112 ------
113 KeyError
114 Raised if none of the supplied target classes match an item in the
115 registry.
116 """
117 attempts: list[Any] = []
118 for t in targetClasses:
119 if t is None:
120 attempts.append(t)
121 else:
122 key = self._getNameKey(t)
123 attempts.append(key)
124 try:
125 entry = self._registry[key]
126 except KeyError:
127 pass
128 else:
129 return key, get_class_of(entry["type"]), entry["kwargs"]
131 # Convert list to a string for error reporting
132 msg = ", ".join(str(k) for k in attempts)
133 plural = "" if len(attempts) == 1 else "s"
134 raise KeyError(f"Unable to find item in registry with key{plural}: {msg}")
136 def getClassFromRegistry(self, targetClasses: Iterable[Any]) -> type:
137 """Get the matching class stored in the registry.
139 Parameters
140 ----------
141 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
142 Each item is tested in turn until a match is found in the registry.
143 Items with `None` value are skipped.
145 Returns
146 -------
147 cls : `type`
148 Class stored in registry associated with the first
149 matching target class.
151 Raises
152 ------
153 KeyError
154 Raised if none of the supplied target classes match an item in the
155 registry.
156 """
157 _, cls, _ = self.getClassFromRegistryWithMatch(targetClasses)
158 return cls
160 def getFromRegistryWithMatch(
161 self, targetClasses: Iterable[Any], *args: Any, **kwargs: Any
162 ) -> tuple[LookupKey, Any]:
163 """Get a new instance of the registry object along with matching key.
165 Parameters
166 ----------
167 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
168 Each item is tested in turn until a match is found in the registry.
169 Items with `None` value are skipped.
170 args : `tuple`
171 Positional arguments to use pass to the object constructor.
172 **kwargs
173 Keyword arguments to pass to object constructor.
175 Returns
176 -------
177 matchKey : `LookupKey`
178 The key that resulted in the successful match.
179 instance : `object`
180 Instance of class stored in registry associated with the first
181 matching target class.
183 Raises
184 ------
185 KeyError
186 Raised if none of the supplied target classes match an item in the
187 registry.
188 """
189 key, cls, registry_kwargs = self.getClassFromRegistryWithMatch(targetClasses)
191 # Supplied keyword args must overwrite registry defaults
192 # We want this overwriting to happen recursively since we expect
193 # some of these keyword arguments to be dicts.
194 # Simplest to use Config for this
195 config_kwargs = Config(registry_kwargs)
196 config_kwargs.update(kwargs)
197 merged_kwargs = config_kwargs.toDict()
199 return key, cls(*args, **merged_kwargs)
201 def getFromRegistry(self, targetClasses: Iterable[Any], *args: Any, **kwargs: Any) -> Any:
202 """Get a new instance of the object stored in the registry.
204 Parameters
205 ----------
206 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
207 Each item is tested in turn until a match is found in the registry.
208 Items with `None` value are skipped.
209 args : `tuple`
210 Positional arguments to use pass to the object constructor.
211 **kwargs
212 Keyword arguments to pass to object constructor.
214 Returns
215 -------
216 instance : `object`
217 Instance of class stored in registry associated with the first
218 matching target class.
220 Raises
221 ------
222 KeyError
223 Raised if none of the supplied target classes match an item in the
224 registry.
225 """
226 _, instance = self.getFromRegistryWithMatch(targetClasses, *args, **kwargs)
227 return instance
229 def placeInRegistry(
230 self, registryKey: Any, typeName: str | type, overwrite: bool = False, **kwargs: Any
231 ) -> None:
232 """Register a class name with the associated type.
234 Parameters
235 ----------
236 registryKey : `LookupKey`, `str` or object with ``name`` attribute.
237 Item to associate with the provided type.
238 typeName : `str` or Python type
239 Identifies a class to associate with the provided key.
240 overwrite : `bool`, optional
241 If `True`, an existing entry will be overwritten. This option
242 is expected to be used to simplify test suites.
243 Default is `False`.
244 **kwargs
245 Keyword arguments to always pass to object constructor when
246 retrieved.
248 Raises
249 ------
250 KeyError
251 Raised if item is already registered and has different value and
252 ``overwrite`` is `False`.
253 """
254 key = self._getNameKey(registryKey)
255 if key in self._registry and not overwrite:
256 # Compare the class strings since dynamic classes can be the
257 # same thing but be different.
258 if str(self._registry[key]) == str(typeName):
259 return
261 raise KeyError(
262 "Item with key {} already registered with different value ({} != {})".format(
263 key, self._registry[key], typeName
264 )
265 )
267 self._registry[key] = {
268 "type": typeName,
269 "kwargs": dict(**kwargs),
270 }
272 @staticmethod
273 def _getNameKey(typeOrName: Any) -> LookupKey:
274 """Extract name of supplied object as entity suitable for key use.
276 Parameters
277 ----------
278 typeOrName : `LookupKey, `str` or object supporting ``name`` attribute.
279 Item from which to extract a name.
281 Returns
282 -------
283 name : `LookupKey`
284 Extracted name as a string or
285 """
286 if isinstance(typeOrName, LookupKey):
287 return typeOrName
289 if isinstance(typeOrName, str):
290 name = typeOrName
291 elif hasattr(typeOrName, "name"):
292 name = typeOrName.name
293 else:
294 raise ValueError("Cannot extract name from type")
296 return LookupKey(name=name)