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

Shortcuts 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

59 statements  

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 ( 

27 Any, 

28 Dict, 

29 Iterable, 

30 List, 

31 Set, 

32 Tuple, 

33 Type, 

34 Union, 

35) 

36from lsst.utils.introspection import get_class_of 

37 

38from .config import Config 

39from .configSupport import LookupKey 

40 

41 

42class MappingFactory: 

43 """ 

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

45 

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. 

50 

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. 

57 

58 """ 

59 

60 def __init__(self, refType: Type): 

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

62 self.refType = refType 

63 

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

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

66 

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. 

72 

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 

80 

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

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

83 

84 Returns 

85 ------- 

86 keys : `set` of `LookupKey` 

87 The keys available for matching in the registry. 

88 """ 

89 return set(self._registry) 

90 

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. 

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

163 

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 

172 Keyword arguments to pass to object constructor. 

173 

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. 

181 

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) 

189 

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

197 

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

199 

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

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

202 

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 

211 Keyword arguments to pass to object constructor. 

212 

213 Returns 

214 ------- 

215 instance : `object` 

216 Instance of class stored in registry associated with the first 

217 matching target class. 

218 

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 

227 

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. 

231 

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 

243 Keyword arguments to always pass to object constructor when 

244 retrieved. 

245 

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 

258 

259 raise KeyError("Item with key {} already registered with different value" 

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

261 

262 self._registry[key] = {"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)