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

59 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-25 02:36 -0800

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 typing import Any, Dict, Iterable, List, Set, Tuple, Type, Union 

27 

28from lsst.utils.introspection import get_class_of 

29 

30from .config import Config 

31from .configSupport import LookupKey 

32 

33 

34class MappingFactory: 

35 """ 

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

37 

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. 

42 

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. 

49 

50 """ 

51 

52 def __init__(self, refType: Type): 

53 self._registry: Dict[LookupKey, Dict[str, Any]] = {} 

54 self.refType = refType 

55 

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

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

58 

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. 

64 

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 

72 

73 def getLookupKeys(self) -> Set[LookupKey]: 

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

75 

76 Returns 

77 ------- 

78 keys : `set` of `LookupKey` 

79 The keys available for matching in the registry. 

80 """ 

81 return set(self._registry) 

82 

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. 

87 

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. 

93 

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. 

103 

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

123 

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

128 

129 def getClassFromRegistry(self, targetClasses: Iterable[Any]) -> Type: 

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

131 

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. 

137 

138 Returns 

139 ------- 

140 cls : `type` 

141 Class stored in registry associated with the first 

142 matching target class. 

143 

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 

152 

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. 

157 

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. 

167 

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. 

175 

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) 

183 

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

191 

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

193 

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

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

196 

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. 

206 

207 Returns 

208 ------- 

209 instance : `object` 

210 Instance of class stored in registry associated with the first 

211 matching target class. 

212 

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 

221 

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. 

226 

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. 

240 

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 

253 

254 raise KeyError( 

255 "Item with key {} already registered with different value" 

256 " ({} != {})".format(key, self._registry[key], typeName) 

257 ) 

258 

259 self._registry[key] = { 

260 "type": typeName, 

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

262 } 

263 

264 @staticmethod 

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

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

267 

268 Parameters 

269 ---------- 

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

271 Item from which to extract a name. 

272 

273 Returns 

274 ------- 

275 name : `LookupKey` 

276 Extracted name as a string or 

277 """ 

278 if isinstance(typeOrName, LookupKey): 

279 return typeOrName 

280 

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

287 

288 return LookupKey(name=name)