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

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/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ("MappingFactory",) 

31 

32from collections.abc import Iterable 

33from typing import Any 

34 

35from lsst.utils.introspection import get_class_of 

36 

37from ._config import Config 

38from ._config_support import LookupKey 

39 

40 

41class MappingFactory: 

42 """Register the mapping of some key to a python type and retrieve 

43 instances. 

44 

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. 

49 

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 

57 """ 

58 

59 def __init__(self, refType: type): 

60 self._registry: dict[LookupKey, dict[str, Any]] = {} 

61 self.refType = refType 

62 

63 def __contains__(self, key: Any) -> bool: 

64 """Indicate whether the supplied key is present in the factory. 

65 

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. 

71 

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 

79 

80 def getLookupKeys(self) -> set[LookupKey]: 

81 """Retrieve the look up keys for all the registry entries. 

82 

83 Returns 

84 ------- 

85 keys : `set` of `LookupKey` 

86 The keys available for matching in the registry. 

87 """ 

88 return set(self._registry) 

89 

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. 

94 

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. 

100 

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. 

110 

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"] 

130 

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}") 

135 

136 def getClassFromRegistry(self, targetClasses: Iterable[Any]) -> type: 

137 """Get the matching class stored in the registry. 

138 

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. 

144 

145 Returns 

146 ------- 

147 cls : `type` 

148 Class stored in registry associated with the first 

149 matching target class. 

150 

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 

159 

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. 

164 

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. 

174 

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. 

182 

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) 

190 

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() 

198 

199 return key, cls(*args, **merged_kwargs) 

200 

201 def getFromRegistry(self, targetClasses: Iterable[Any], *args: Any, **kwargs: Any) -> Any: 

202 """Get a new instance of the object stored in the registry. 

203 

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. 

213 

214 Returns 

215 ------- 

216 instance : `object` 

217 Instance of class stored in registry associated with the first 

218 matching target class. 

219 

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 

228 

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. 

233 

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. 

247 

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 

260 

261 raise KeyError( 

262 "Item with key {} already registered with different value ({} != {})".format( 

263 key, self._registry[key], typeName 

264 ) 

265 ) 

266 

267 self._registry[key] = { 

268 "type": typeName, 

269 "kwargs": dict(**kwargs), 

270 } 

271 

272 @staticmethod 

273 def _getNameKey(typeOrName: Any) -> LookupKey: 

274 """Extract name of supplied object as entity suitable for key use. 

275 

276 Parameters 

277 ---------- 

278 typeOrName : `LookupKey, `str` or object supporting ``name`` attribute. 

279 Item from which to extract a name. 

280 

281 Returns 

282 ------- 

283 name : `LookupKey` 

284 Extracted name as a string or 

285 """ 

286 if isinstance(typeOrName, LookupKey): 

287 return typeOrName 

288 

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") 

295 

296 return LookupKey(name=name)