Coverage for python/lsst/daf/butler/mapping_factory.py: 22%
66 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-27 03:00 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-27 03:00 -0700
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.
56 """
58 def __init__(self, refType: type):
59 self._registry: dict[LookupKey, dict[str, Any]] = {}
60 self.refType = refType
62 def __contains__(self, key: Any) -> bool:
63 """Indicate whether the supplied key is present in the factory.
65 Parameters
66 ----------
67 key : `LookupKey`, `str` or objects with ``name`` attribute
68 Key to use to lookup whether a corresponding element exists
69 in this factory.
71 Returns
72 -------
73 in : `bool`
74 `True` if the supplied key is present in the factory.
75 """
76 key = self._getNameKey(key)
77 return key in self._registry
79 def getLookupKeys(self) -> set[LookupKey]:
80 """Retrieve the look up keys for all the registry entries.
82 Returns
83 -------
84 keys : `set` of `LookupKey`
85 The keys available for matching in the registry.
86 """
87 return set(self._registry)
89 def getClassFromRegistryWithMatch(
90 self, targetClasses: Iterable[Any]
91 ) -> tuple[LookupKey, type, dict[Any, Any]]:
92 """Get the class stored in the registry along with the matching key.
94 Parameters
95 ----------
96 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
97 Each item is tested in turn until a match is found in the registry.
98 Items with `None` value are skipped.
100 Returns
101 -------
102 matchKey : `LookupKey`
103 The key that resulted in the successful match.
104 cls : `type`
105 Class stored in registry associated with the first
106 matching target class.
107 kwargs: `dict`
108 Keyword arguments to be given to constructor.
110 Raises
111 ------
112 KeyError
113 Raised if none of the supplied target classes match an item in the
114 registry.
115 """
116 attempts: list[Any] = []
117 for t in targetClasses:
118 if t is None:
119 attempts.append(t)
120 else:
121 key = self._getNameKey(t)
122 attempts.append(key)
123 try:
124 entry = self._registry[key]
125 except KeyError:
126 pass
127 else:
128 return key, get_class_of(entry["type"]), entry["kwargs"]
130 # Convert list to a string for error reporting
131 msg = ", ".join(str(k) for k in attempts)
132 plural = "" if len(attempts) == 1 else "s"
133 raise KeyError(f"Unable to find item in registry with key{plural}: {msg}")
135 def getClassFromRegistry(self, targetClasses: Iterable[Any]) -> type:
136 """Get the matching class stored in the registry.
138 Parameters
139 ----------
140 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
141 Each item is tested in turn until a match is found in the registry.
142 Items with `None` value are skipped.
144 Returns
145 -------
146 cls : `type`
147 Class stored in registry associated with the first
148 matching target class.
150 Raises
151 ------
152 KeyError
153 Raised if none of the supplied target classes match an item in the
154 registry.
155 """
156 _, cls, _ = self.getClassFromRegistryWithMatch(targetClasses)
157 return cls
159 def getFromRegistryWithMatch(
160 self, targetClasses: Iterable[Any], *args: Any, **kwargs: Any
161 ) -> tuple[LookupKey, Any]:
162 """Get a new instance of the registry object along with matching key.
164 Parameters
165 ----------
166 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
167 Each item is tested in turn until a match is found in the registry.
168 Items with `None` value are skipped.
169 *args : `tuple`
170 Positional arguments to use pass to the object constructor.
171 **kwargs
172 Keyword arguments to pass to object constructor.
174 Returns
175 -------
176 matchKey : `LookupKey`
177 The key that resulted in the successful match.
178 instance : `object`
179 Instance of class stored in registry associated with the first
180 matching target class.
182 Raises
183 ------
184 KeyError
185 Raised if none of the supplied target classes match an item in the
186 registry.
187 """
188 key, cls, registry_kwargs = self.getClassFromRegistryWithMatch(targetClasses)
190 # Supplied keyword args must overwrite registry defaults
191 # We want this overwriting to happen recursively since we expect
192 # some of these keyword arguments to be dicts.
193 # Simplest to use Config for this
194 merged_kwargs: dict[str, Any] = {}
195 if kwargs and registry_kwargs:
196 config_kwargs = Config(registry_kwargs)
197 config_kwargs.update(kwargs)
198 merged_kwargs = config_kwargs.toDict()
199 elif registry_kwargs:
200 merged_kwargs = registry_kwargs
201 elif kwargs:
202 merged_kwargs = kwargs
204 return key, cls(*args, **merged_kwargs)
206 def getFromRegistry(self, targetClasses: Iterable[Any], *args: Any, **kwargs: Any) -> Any:
207 """Get a new instance of the object stored in the registry.
209 Parameters
210 ----------
211 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
212 Each item is tested in turn until a match is found in the registry.
213 Items with `None` value are skipped.
214 *args : `tuple`
215 Positional arguments to use pass to the object constructor.
216 **kwargs
217 Keyword arguments to pass to object constructor.
219 Returns
220 -------
221 instance : `object`
222 Instance of class stored in registry associated with the first
223 matching target class.
225 Raises
226 ------
227 KeyError
228 Raised if none of the supplied target classes match an item in the
229 registry.
230 """
231 _, instance = self.getFromRegistryWithMatch(targetClasses, *args, **kwargs)
232 return instance
234 def placeInRegistry(
235 self, registryKey: Any, typeName: str | type, overwrite: bool = False, **kwargs: Any
236 ) -> None:
237 """Register a class name with the associated type.
239 Parameters
240 ----------
241 registryKey : `LookupKey`, `str` or object with ``name`` attribute
242 Item to associate with the provided type.
243 typeName : `str` or Python type
244 Identifies a class to associate with the provided key.
245 overwrite : `bool`, optional
246 If `True`, an existing entry will be overwritten. This option
247 is expected to be used to simplify test suites.
248 Default is `False`.
249 **kwargs
250 Keyword arguments to always pass to object constructor when
251 retrieved.
253 Raises
254 ------
255 KeyError
256 Raised if item is already registered and has different value and
257 ``overwrite`` is `False`.
258 """
259 key = self._getNameKey(registryKey)
260 if key in self._registry and not overwrite:
261 # Compare the class strings since dynamic classes can be the
262 # same thing but be different.
263 if str(self._registry[key]) == str(typeName):
264 return
266 raise KeyError(
267 f"Item with key {key} already registered with different value "
268 f"({self._registry[key]} != {typeName})"
269 )
271 self._registry[key] = {
272 "type": typeName,
273 "kwargs": dict(**kwargs),
274 }
276 @staticmethod
277 def _getNameKey(typeOrName: Any) -> LookupKey:
278 """Extract name of supplied object as entity suitable for key use.
280 Parameters
281 ----------
282 typeOrName : `LookupKey, `str` or object supporting ``name`` attribute.
283 Item from which to extract a name.
285 Returns
286 -------
287 name : `LookupKey`
288 Extracted name as a string or
289 """
290 if isinstance(typeOrName, LookupKey):
291 return typeOrName
293 if isinstance(typeOrName, str):
294 name = typeOrName
295 elif hasattr(typeOrName, "name"):
296 name = typeOrName.name
297 else:
298 raise ValueError("Cannot extract name from type")
300 return LookupKey(name=name)