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

60 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-23 09:30 +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 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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("MappingFactory",) 

25 

26from collections.abc import Iterable 

27from typing import Any 

28 

29from lsst.utils.introspection import get_class_of 

30 

31from .config import Config 

32from .configSupport import LookupKey 

33 

34 

35class MappingFactory: 

36 """ 

37 Register the mapping of some key to a python type and retrieve instances. 

38 

39 Enables instances of these classes to be retrieved from the factory later. 

40 The class can be specified as an object, class or string. 

41 If the key is an object it is converted to a string by accessing 

42 a ``name`` attribute. 

43 

44 Parameters 

45 ---------- 

46 refType : `type` 

47 Python reference `type` to use to ensure that items stored in the 

48 registry create instance objects of the correct class. Subclasses 

49 of this type are allowed. Using `None` disables the check. 

50 

51 """ 

52 

53 def __init__(self, refType: type): 

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

55 self.refType = refType 

56 

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

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

59 

60 Parameters 

61 ---------- 

62 key : `LookupKey`, `str` or objects with ``name`` attribute 

63 Key to use to lookup whether a corresponding element exists 

64 in this factory. 

65 

66 Returns 

67 ------- 

68 in : `bool` 

69 `True` if the supplied key is present in the factory. 

70 """ 

71 key = self._getNameKey(key) 

72 return key in self._registry 

73 

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

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

76 

77 Returns 

78 ------- 

79 keys : `set` of `LookupKey` 

80 The keys available for matching in the registry. 

81 """ 

82 return set(self._registry) 

83 

84 def getClassFromRegistryWithMatch( 

85 self, targetClasses: Iterable[Any] 

86 ) -> tuple[LookupKey, type, dict[Any, Any]]: 

87 """Get the class stored in the registry along with the matching key. 

88 

89 Parameters 

90 ---------- 

91 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute 

92 Each item is tested in turn until a match is found in the registry. 

93 Items with `None` value are skipped. 

94 

95 Returns 

96 ------- 

97 matchKey : `LookupKey` 

98 The key that resulted in the successful match. 

99 cls : `type` 

100 Class stored in registry associated with the first 

101 matching target class. 

102 kwargs: `dict` 

103 Keyword arguments to be given to constructor. 

104 

105 Raises 

106 ------ 

107 KeyError 

108 Raised if none of the supplied target classes match an item in the 

109 registry. 

110 """ 

111 attempts: list[Any] = [] 

112 for t in targetClasses: 

113 if t is None: 

114 attempts.append(t) 

115 else: 

116 key = self._getNameKey(t) 

117 attempts.append(key) 

118 try: 

119 entry = self._registry[key] 

120 except KeyError: 

121 pass 

122 else: 

123 return key, get_class_of(entry["type"]), entry["kwargs"] 

124 

125 # Convert list to a string for error reporting 

126 msg = ", ".join(str(k) for k in attempts) 

127 plural = "" if len(attempts) == 1 else "s" 

128 raise KeyError(f"Unable to find item in registry with key{plural}: {msg}") 

129 

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

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

132 

133 Parameters 

134 ---------- 

135 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute 

136 Each item is tested in turn until a match is found in the registry. 

137 Items with `None` value are skipped. 

138 

139 Returns 

140 ------- 

141 cls : `type` 

142 Class stored in registry associated with the first 

143 matching target class. 

144 

145 Raises 

146 ------ 

147 KeyError 

148 Raised if none of the supplied target classes match an item in the 

149 registry. 

150 """ 

151 _, cls, _ = self.getClassFromRegistryWithMatch(targetClasses) 

152 return cls 

153 

154 def getFromRegistryWithMatch( 

155 self, targetClasses: Iterable[Any], *args: Any, **kwargs: Any 

156 ) -> tuple[LookupKey, Any]: 

157 """Get a new instance of the registry object along with matching key. 

158 

159 Parameters 

160 ---------- 

161 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute 

162 Each item is tested in turn until a match is found in the registry. 

163 Items with `None` value are skipped. 

164 args : `tuple` 

165 Positional arguments to use pass to the object constructor. 

166 **kwargs 

167 Keyword arguments to pass to object constructor. 

168 

169 Returns 

170 ------- 

171 matchKey : `LookupKey` 

172 The key that resulted in the successful match. 

173 instance : `object` 

174 Instance of class stored in registry associated with the first 

175 matching target class. 

176 

177 Raises 

178 ------ 

179 KeyError 

180 Raised if none of the supplied target classes match an item in the 

181 registry. 

182 """ 

183 key, cls, registry_kwargs = self.getClassFromRegistryWithMatch(targetClasses) 

184 

185 # Supplied keyword args must overwrite registry defaults 

186 # We want this overwriting to happen recursively since we expect 

187 # some of these keyword arguments to be dicts. 

188 # Simplest to use Config for this 

189 config_kwargs = Config(registry_kwargs) 

190 config_kwargs.update(kwargs) 

191 merged_kwargs = config_kwargs.toDict() 

192 

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

194 

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

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

197 

198 Parameters 

199 ---------- 

200 targetClasses : `LookupKey`, `str` or objects with ``name`` attribute 

201 Each item is tested in turn until a match is found in the registry. 

202 Items with `None` value are skipped. 

203 args : `tuple` 

204 Positional arguments to use pass to the object constructor. 

205 **kwargs 

206 Keyword arguments to pass to object constructor. 

207 

208 Returns 

209 ------- 

210 instance : `object` 

211 Instance of class stored in registry associated with the first 

212 matching target class. 

213 

214 Raises 

215 ------ 

216 KeyError 

217 Raised if none of the supplied target classes match an item in the 

218 registry. 

219 """ 

220 _, instance = self.getFromRegistryWithMatch(targetClasses, *args, **kwargs) 

221 return instance 

222 

223 def placeInRegistry( 

224 self, registryKey: Any, typeName: str | type, overwrite: bool = False, **kwargs: Any 

225 ) -> None: 

226 """Register a class name with the associated type. 

227 

228 Parameters 

229 ---------- 

230 registryKey : `LookupKey`, `str` or object with ``name`` attribute. 

231 Item to associate with the provided type. 

232 typeName : `str` or Python type 

233 Identifies a class to associate with the provided key. 

234 overwrite : `bool`, optional 

235 If `True`, an existing entry will be overwritten. This option 

236 is expected to be used to simplify test suites. 

237 Default is `False`. 

238 **kwargs 

239 Keyword arguments to always pass to object constructor when 

240 retrieved. 

241 

242 Raises 

243 ------ 

244 KeyError 

245 Raised if item is already registered and has different value and 

246 ``overwrite`` is `False`. 

247 """ 

248 key = self._getNameKey(registryKey) 

249 if key in self._registry and not overwrite: 

250 # Compare the class strings since dynamic classes can be the 

251 # same thing but be different. 

252 if str(self._registry[key]) == str(typeName): 

253 return 

254 

255 raise KeyError( 

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

257 key, self._registry[key], typeName 

258 ) 

259 ) 

260 

261 self._registry[key] = { 

262 "type": typeName, 

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

264 } 

265 

266 @staticmethod 

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

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

269 

270 Parameters 

271 ---------- 

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

273 Item from which to extract a name. 

274 

275 Returns 

276 ------- 

277 name : `LookupKey` 

278 Extracted name as a string or 

279 """ 

280 if isinstance(typeOrName, LookupKey): 

281 return typeOrName 

282 

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

289 

290 return LookupKey(name=name)