Coverage for python/lsst/daf/butler/core/mappingFactory.py : 23%

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/>.
22from __future__ import annotations
24__all__ = ("MappingFactory", )
26from typing import (
27 Any,
28 Dict,
29 Iterable,
30 List,
31 Set,
32 Tuple,
33 Type,
34 Union,
35)
37from .config import Config
38from .utils import getClassOf
39from .configSupport import LookupKey
42class MappingFactory:
43 """
44 Register the mapping of some key to a python type and retrieve instances.
46 Enables instances of these classes to be retrieved from the factory later.
47 The class can be specified as an object, class or string.
48 If the key is an object it is converted to a string by accessing
49 a ``name`` attribute.
51 Parameters
52 ----------
53 refType : `type`
54 Python reference `type` to use to ensure that items stored in the
55 registry create instance objects of the correct class. Subclasses
56 of this type are allowed. Using `None` disables the check.
58 """
60 def __init__(self, refType: Type):
61 self._registry: Dict[LookupKey, Dict[str, Any]] = {}
62 self.refType = refType
64 def __contains__(self, key: Any) -> bool:
65 """Indicate whether the supplied key is present in the factory.
67 Parameters
68 ----------
69 key : `LookupKey`, `str` or objects with ``name`` attribute
70 Key to use to lookup whether a corresponding element exists
71 in this factory.
73 Returns
74 -------
75 in : `bool`
76 `True` if the supplied key is present in the factory.
77 """
78 key = self._getNameKey(key)
79 return key in self._registry
81 def getLookupKeys(self) -> Set[LookupKey]:
82 """Retrieve the look up keys for all the registry entries.
84 Returns
85 -------
86 keys : `set` of `LookupKey`
87 The keys available for matching in the registry.
88 """
89 return set(self._registry)
91 def getClassFromRegistryWithMatch(self, targetClasses: Iterable[Any]) -> Tuple[LookupKey, Type,
92 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, getClassOf(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(self, targetClasses: Iterable[Any], *args: Any,
161 **kwargs: Any) -> 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 : `dict`
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 config_kwargs = Config(registry_kwargs)
195 config_kwargs.update(kwargs)
196 merged_kwargs = config_kwargs.toDict()
198 return key, cls(*args, **merged_kwargs)
200 def getFromRegistry(self, targetClasses: Iterable[Any], *args: Any, **kwargs: Any) -> Any:
201 """Get a new instance of the object stored in the registry.
203 Parameters
204 ----------
205 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute
206 Each item is tested in turn until a match is found in the registry.
207 Items with `None` value are skipped.
208 args : `tuple`
209 Positional arguments to use pass to the object constructor.
210 kwargs : `dict`
211 Keyword arguments to pass to object constructor.
213 Returns
214 -------
215 instance : `object`
216 Instance of class stored in registry associated with the first
217 matching target class.
219 Raises
220 ------
221 KeyError
222 Raised if none of the supplied target classes match an item in the
223 registry.
224 """
225 _, instance = self.getFromRegistryWithMatch(targetClasses, *args, **kwargs)
226 return instance
228 def placeInRegistry(self, registryKey: Any, typeName: Union[str, Type],
229 overwrite: bool = False, **kwargs: Any) -> None:
230 """Register a class name with the associated type.
232 Parameters
233 ----------
234 registryKey : `LookupKey`, `str` or object with ``name`` attribute.
235 Item to associate with the provided type.
236 typeName : `str` or Python type
237 Identifies a class to associate with the provided key.
238 overwrite : `bool`, optional
239 If `True`, an existing entry will be overwritten. This option
240 is expected to be used to simplify test suites.
241 Default is `False`.
242 kwargs : `dict`
243 Keyword arguments to always pass to object constructor when
244 retrieved.
246 Raises
247 ------
248 KeyError
249 Raised if item is already registered and has different value and
250 ``overwrite`` is `False`.
251 """
252 key = self._getNameKey(registryKey)
253 if key in self._registry and not overwrite:
254 # Compare the class strings since dynamic classes can be the
255 # same thing but be different.
256 if str(self._registry[key]) == str(typeName):
257 return
259 raise KeyError("Item with key {} already registered with different value"
260 " ({} != {})".format(key, self._registry[key], typeName))
262 self._registry[key] = {"type": typeName,
263 "kwargs": dict(**kwargs),
264 }
266 @staticmethod
267 def _getNameKey(typeOrName: Any) -> LookupKey:
268 """Extract name of supplied object as entity suitable for key use.
270 Parameters
271 ----------
272 typeOrName : `LookupKey, `str` or object supporting ``name`` attribute.
273 Item from which to extract a name.
275 Returns
276 -------
277 name : `LookupKey`
278 Extracted name as a string or
279 """
280 if isinstance(typeOrName, LookupKey):
281 return typeOrName
283 if isinstance(typeOrName, str):
284 name = typeOrName
285 elif hasattr(typeOrName, "name"):
286 name = typeOrName.name
287 else:
288 raise ValueError("Cannot extract name from type")
290 return LookupKey(name=name)