Hide keyboard shortcuts

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

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) 

36 

37from .config import Config 

38from .utils import getClassOf 

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

94 the matching key. 

95 

96 Parameters 

97 ---------- 

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

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

100 Items with `None` value are skipped. 

101 

102 Returns 

103 ------- 

104 matchKey : `LookupKey` 

105 The key that resulted in the successful match. 

106 cls : `type` 

107 Class stored in registry associated with the first 

108 matching target class. 

109 kwargs: `dict` 

110 Keyword arguments to be given to constructor. 

111 

112 Raises 

113 ------ 

114 KeyError 

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

116 registry. 

117 """ 

118 attempts: List[Any] = [] 

119 for t in (targetClasses): 

120 if t is None: 

121 attempts.append(t) 

122 else: 

123 key = self._getNameKey(t) 

124 attempts.append(key) 

125 try: 

126 entry = self._registry[key] 

127 except KeyError: 

128 pass 

129 else: 

130 return key, getClassOf(entry["type"]), entry["kwargs"] 

131 

132 # Convert list to a string for error reporting 

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

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

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

136 

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

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

139 

140 Parameters 

141 ---------- 

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

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

144 Items with `None` value are skipped. 

145 

146 Returns 

147 ------- 

148 cls : `type` 

149 Class stored in registry associated with the first 

150 matching target class. 

151 

152 Raises 

153 ------ 

154 KeyError 

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

156 registry. 

157 """ 

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

159 return cls 

160 

161 def getFromRegistryWithMatch(self, targetClasses: Iterable[Any], *args: Any, 

162 **kwargs: Any) -> Tuple[LookupKey, Any]: 

163 """Get a new instance of the object stored in the registry along with 

164 the matching key. 

165 

166 Parameters 

167 ---------- 

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

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

170 Items with `None` value are skipped. 

171 args : `tuple` 

172 Positional arguments to use pass to the object constructor. 

173 kwargs : `dict` 

174 Keyword arguments to pass to object constructor. 

175 

176 Returns 

177 ------- 

178 matchKey : `LookupKey` 

179 The key that resulted in the successful match. 

180 instance : `object` 

181 Instance of class stored in registry associated with the first 

182 matching target class. 

183 

184 Raises 

185 ------ 

186 KeyError 

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

188 registry. 

189 """ 

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

191 

192 # Supplied keyword args must overwrite registry defaults 

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

194 # some of these keyword arguments to be dicts. 

195 # Simplest to use Config for this 

196 config_kwargs = Config(registry_kwargs) 

197 config_kwargs.update(kwargs) 

198 merged_kwargs = config_kwargs.toDict() 

199 

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

201 

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

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

204 

205 Parameters 

206 ---------- 

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

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

209 Items with `None` value are skipped. 

210 args : `tuple` 

211 Positional arguments to use pass to the object constructor. 

212 kwargs : `dict` 

213 Keyword arguments to pass to object constructor. 

214 

215 Returns 

216 ------- 

217 instance : `object` 

218 Instance of class stored in registry associated with the first 

219 matching target class. 

220 

221 Raises 

222 ------ 

223 KeyError 

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

225 registry. 

226 """ 

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

228 return instance 

229 

230 def placeInRegistry(self, registryKey: Any, typeName: Union[str, Type], 

231 overwrite: bool = False, **kwargs: Any) -> 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 : `dict` 

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("Item with key {} already registered with different value" 

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

263 

264 self._registry[key] = {"type": typeName, 

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

266 } 

267 

268 @staticmethod 

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

270 """Extract name of supplied object as string or entity suitable for 

271 using as key. 

272 

273 Parameters 

274 ---------- 

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

276 Item from which to extract a name. 

277 

278 Returns 

279 ------- 

280 name : `LookupKey` 

281 Extracted name as a string or 

282 """ 

283 if isinstance(typeOrName, LookupKey): 

284 return typeOrName 

285 

286 if isinstance(typeOrName, str): 

287 name = typeOrName 

288 elif hasattr(typeOrName, "name"): 

289 name = typeOrName.name 

290 else: 

291 raise ValueError("Cannot extract name from type") 

292 

293 return LookupKey(name=name)