Coverage for python/lsst/daf/butler/core/mappingFactory.py: 23%
59 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-04 02:19 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-04 02:19 -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 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/>.
22from __future__ import annotations
24__all__ = ("MappingFactory",)
26from typing import Any, Dict, Iterable, List, Set, Tuple, Type, Union
28from lsst.utils.introspection import get_class_of
30from .config import Config
31from .configSupport import LookupKey
34class MappingFactory:
35 """
36 Register the mapping of some key to a python type and retrieve instances.
38 Enables instances of these classes to be retrieved from the factory later.
39 The class can be specified as an object, class or string.
40 If the key is an object it is converted to a string by accessing
41 a ``name`` attribute.
43 Parameters
44 ----------
45 refType : `type`
46 Python reference `type` to use to ensure that items stored in the
47 registry create instance objects of the correct class. Subclasses
48 of this type are allowed. Using `None` disables the check.
50 """
52 def __init__(self, refType: Type):
53 self._registry: Dict[LookupKey, Dict[str, Any]] = {}
54 self.refType = refType
56 def __contains__(self, key: Any) -> bool:
57 """Indicate whether the supplied key is present in the factory.
59 Parameters
60 ----------
61 key : `LookupKey`, `str` or objects with ``name`` attribute
62 Key to use to lookup whether a corresponding element exists
63 in this factory.
65 Returns
66 -------
67 in : `bool`
68 `True` if the supplied key is present in the factory.
69 """
70 key = self._getNameKey(key)
71 return key in self._registry
73 def getLookupKeys(self) -> Set[LookupKey]:
74 """Retrieve the look up keys for all the registry entries.
76 Returns
77 -------
78 keys : `set` of `LookupKey`
79 The keys available for matching in the registry.
80 """
81 return set(self._registry)
83 def getClassFromRegistryWithMatch(
84 self, targetClasses: Iterable[Any]
85 ) -> Tuple[LookupKey, Type, Dict[Any, Any]]:
86 """Get the class stored in the registry along with the matching key.
88 Parameters
89 ----------
90 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
91 Each item is tested in turn until a match is found in the registry.
92 Items with `None` value are skipped.
94 Returns
95 -------
96 matchKey : `LookupKey`
97 The key that resulted in the successful match.
98 cls : `type`
99 Class stored in registry associated with the first
100 matching target class.
101 kwargs: `dict`
102 Keyword arguments to be given to constructor.
104 Raises
105 ------
106 KeyError
107 Raised if none of the supplied target classes match an item in the
108 registry.
109 """
110 attempts: List[Any] = []
111 for t in targetClasses:
112 if t is None:
113 attempts.append(t)
114 else:
115 key = self._getNameKey(t)
116 attempts.append(key)
117 try:
118 entry = self._registry[key]
119 except KeyError:
120 pass
121 else:
122 return key, get_class_of(entry["type"]), entry["kwargs"]
124 # Convert list to a string for error reporting
125 msg = ", ".join(str(k) for k in attempts)
126 plural = "" if len(attempts) == 1 else "s"
127 raise KeyError(f"Unable to find item in registry with key{plural}: {msg}")
129 def getClassFromRegistry(self, targetClasses: Iterable[Any]) -> Type:
130 """Get the matching class stored in the registry.
132 Parameters
133 ----------
134 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
135 Each item is tested in turn until a match is found in the registry.
136 Items with `None` value are skipped.
138 Returns
139 -------
140 cls : `type`
141 Class stored in registry associated with the first
142 matching target class.
144 Raises
145 ------
146 KeyError
147 Raised if none of the supplied target classes match an item in the
148 registry.
149 """
150 _, cls, _ = self.getClassFromRegistryWithMatch(targetClasses)
151 return cls
153 def getFromRegistryWithMatch(
154 self, targetClasses: Iterable[Any], *args: Any, **kwargs: Any
155 ) -> Tuple[LookupKey, Any]:
156 """Get a new instance of the registry object along with matching key.
158 Parameters
159 ----------
160 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
161 Each item is tested in turn until a match is found in the registry.
162 Items with `None` value are skipped.
163 args : `tuple`
164 Positional arguments to use pass to the object constructor.
165 **kwargs
166 Keyword arguments to pass to object constructor.
168 Returns
169 -------
170 matchKey : `LookupKey`
171 The key that resulted in the successful match.
172 instance : `object`
173 Instance of class stored in registry associated with the first
174 matching target class.
176 Raises
177 ------
178 KeyError
179 Raised if none of the supplied target classes match an item in the
180 registry.
181 """
182 key, cls, registry_kwargs = self.getClassFromRegistryWithMatch(targetClasses)
184 # Supplied keyword args must overwrite registry defaults
185 # We want this overwriting to happen recursively since we expect
186 # some of these keyword arguments to be dicts.
187 # Simplest to use Config for this
188 config_kwargs = Config(registry_kwargs)
189 config_kwargs.update(kwargs)
190 merged_kwargs = config_kwargs.toDict()
192 return key, cls(*args, **merged_kwargs)
194 def getFromRegistry(self, targetClasses: Iterable[Any], *args: Any, **kwargs: Any) -> Any:
195 """Get a new instance of the object stored in the registry.
197 Parameters
198 ----------
199 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
200 Each item is tested in turn until a match is found in the registry.
201 Items with `None` value are skipped.
202 args : `tuple`
203 Positional arguments to use pass to the object constructor.
204 **kwargs
205 Keyword arguments to pass to object constructor.
207 Returns
208 -------
209 instance : `object`
210 Instance of class stored in registry associated with the first
211 matching target class.
213 Raises
214 ------
215 KeyError
216 Raised if none of the supplied target classes match an item in the
217 registry.
218 """
219 _, instance = self.getFromRegistryWithMatch(targetClasses, *args, **kwargs)
220 return instance
222 def placeInRegistry(
223 self, registryKey: Any, typeName: Union[str, Type], overwrite: bool = False, **kwargs: Any
224 ) -> None:
225 """Register a class name with the associated type.
227 Parameters
228 ----------
229 registryKey : `LookupKey`, `str` or object with ``name`` attribute.
230 Item to associate with the provided type.
231 typeName : `str` or Python type
232 Identifies a class to associate with the provided key.
233 overwrite : `bool`, optional
234 If `True`, an existing entry will be overwritten. This option
235 is expected to be used to simplify test suites.
236 Default is `False`.
237 **kwargs
238 Keyword arguments to always pass to object constructor when
239 retrieved.
241 Raises
242 ------
243 KeyError
244 Raised if item is already registered and has different value and
245 ``overwrite`` is `False`.
246 """
247 key = self._getNameKey(registryKey)
248 if key in self._registry and not overwrite:
249 # Compare the class strings since dynamic classes can be the
250 # same thing but be different.
251 if str(self._registry[key]) == str(typeName):
252 return
254 raise KeyError(
255 "Item with key {} already registered with different value"
256 " ({} != {})".format(key, self._registry[key], typeName)
257 )
259 self._registry[key] = {
260 "type": typeName,
261 "kwargs": dict(**kwargs),
262 }
264 @staticmethod
265 def _getNameKey(typeOrName: Any) -> LookupKey:
266 """Extract name of supplied object as entity suitable for key use.
268 Parameters
269 ----------
270 typeOrName : `LookupKey, `str` or object supporting ``name`` attribute.
271 Item from which to extract a name.
273 Returns
274 -------
275 name : `LookupKey`
276 Extracted name as a string or
277 """
278 if isinstance(typeOrName, LookupKey):
279 return typeOrName
281 if isinstance(typeOrName, str):
282 name = typeOrName
283 elif hasattr(typeOrName, "name"):
284 name = typeOrName.name
285 else:
286 raise ValueError("Cannot extract name from type")
288 return LookupKey(name=name)