Coverage for python/lsst/pex/config/registry.py: 37%

99 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-06-02 11:08 +0000

1# This file is part of pex_config. 

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 

28__all__ = ("Registry", "makeRegistry", "RegistryField", "registerConfig", "registerConfigurable") 

29 

30import collections.abc 

31import copy 

32 

33from .config import Config, FieldValidationError, _typeStr 

34from .configChoiceField import ConfigChoiceField, ConfigInstanceDict 

35 

36 

37class ConfigurableWrapper: 

38 """A wrapper for configurables. 

39 

40 Used for configurables that don't contain a ``ConfigClass`` attribute, 

41 or contain one that is being overridden. 

42 """ 

43 

44 def __init__(self, target, ConfigClass): 

45 self.ConfigClass = ConfigClass 

46 self._target = target 

47 

48 def __call__(self, *args, **kwargs): 

49 return self._target(*args, **kwargs) 

50 

51 

52class Registry(collections.abc.Mapping): 

53 """A base class for global registries, which map names to configurables. 

54 

55 A registry acts like a read-only dictionary with an additional `register` 

56 method to add targets. Targets in the registry are configurables (see 

57 *Notes*). 

58 

59 Parameters 

60 ---------- 

61 configBaseType : `lsst.pex.config.Config`-type 

62 The base class for config classes in the registry. 

63 

64 Notes 

65 ----- 

66 A configurable is a callable with call signature ``(config, *args)`` 

67 Configurables typically create an algorithm or are themselves the 

68 algorithm. Often configurables are `lsst.pipe.base.Task` subclasses, but 

69 this is not required. 

70 

71 A ``Registry`` has these requirements: 

72 

73 - All configurables added to a particular registry have the same call 

74 signature. 

75 - All configurables in a registry typically share something important 

76 in common. For example, all configurables in ``psfMatchingRegistry`` 

77 return a PSF matching class that has a ``psfMatch`` method with a 

78 particular call signature. 

79 

80 Examples 

81 -------- 

82 This examples creates a configurable class ``Foo`` and adds it to a 

83 registry. First, creating the configurable: 

84 

85 >>> from lsst.pex.config import Registry, Config 

86 >>> class FooConfig(Config): 

87 ... val = Field(dtype=int, default=3, doc="parameter for Foo") 

88 ... 

89 >>> class Foo: 

90 ... ConfigClass = FooConfig 

91 ... def __init__(self, config): 

92 ... self.config = config 

93 ... def addVal(self, num): 

94 ... return self.config.val + num 

95 ... 

96 

97 Next, create a ``Registry`` instance called ``registry`` and register the 

98 ``Foo`` configurable under the ``"foo"`` key: 

99 

100 >>> registry = Registry() 

101 >>> registry.register("foo", Foo) 

102 >>> print(list(registry.keys())) 

103 ["foo"] 

104 

105 Now ``Foo`` is conveniently accessible from the registry itself. 

106 

107 Finally, use the registry to get the configurable class and create an 

108 instance of it: 

109 

110 >>> FooConfigurable = registry["foo"] 

111 >>> foo = FooConfigurable(FooConfigurable.ConfigClass()) 

112 >>> foo.addVal(5) 

113 8 

114 """ 

115 

116 def __init__(self, configBaseType=Config): 

117 if not issubclass(configBaseType, Config): 

118 raise TypeError( 

119 "configBaseType=%s must be a subclass of Config" 

120 % _typeStr( 

121 configBaseType, 

122 ) 

123 ) 

124 self._configBaseType = configBaseType 

125 self._dict = {} 

126 

127 def register(self, name, target, ConfigClass=None): 

128 """Add a new configurable target to the registry. 

129 

130 Parameters 

131 ---------- 

132 name : `str` 

133 Name that the ``target`` is registered under. The target can 

134 be accessed later with `dict`-like patterns using ``name`` as 

135 the key. 

136 target : obj 

137 A configurable type, usually a subclass of `lsst.pipe.base.Task`. 

138 ConfigClass : `lsst.pex.config.Config`-type, optional 

139 A subclass of `lsst.pex.config.Config` used to configure the 

140 configurable. If `None` then the configurable's ``ConfigClass`` 

141 attribute is used. 

142 

143 Raises 

144 ------ 

145 RuntimeError 

146 Raised if an item with ``name`` is already in the registry. 

147 AttributeError 

148 Raised if ``ConfigClass`` is `None` and ``target`` does not have 

149 a ``ConfigClass`` attribute. 

150 

151 Notes 

152 ----- 

153 If ``ConfigClass`` is provided then the ``target`` configurable is 

154 wrapped in a new object that forwards function calls to it. Otherwise 

155 the original ``target`` is stored. 

156 """ 

157 if name in self._dict: 

158 raise RuntimeError("An item with name %r already exists" % name) 

159 if ConfigClass is None: 

160 wrapper = target 

161 else: 

162 wrapper = ConfigurableWrapper(target, ConfigClass) 

163 if not issubclass(wrapper.ConfigClass, self._configBaseType): 

164 raise TypeError( 

165 "ConfigClass=%s is not a subclass of %r" 

166 % (_typeStr(wrapper.ConfigClass), _typeStr(self._configBaseType)) 

167 ) 

168 self._dict[name] = wrapper 

169 

170 def __getitem__(self, key): 

171 return self._dict[key] 

172 

173 def __len__(self): 

174 return len(self._dict) 

175 

176 def __iter__(self): 

177 return iter(self._dict) 

178 

179 def __contains__(self, key): 

180 return key in self._dict 

181 

182 def makeField(self, doc, default=None, optional=False, multi=False): 

183 """Create a `RegistryField` configuration field from this registry. 

184 

185 Parameters 

186 ---------- 

187 doc : `str` 

188 A description of the field. 

189 default : object, optional 

190 The default target for the field. 

191 optional : `bool`, optional 

192 When `False`, `lsst.pex.config.Config.validate` fails if the 

193 field's value is `None`. 

194 multi : `bool`, optional 

195 A flag to allow multiple selections in the `RegistryField` if 

196 `True`. 

197 

198 Returns 

199 ------- 

200 field : `lsst.pex.config.RegistryField` 

201 `~lsst.pex.config.RegistryField` Configuration field. 

202 """ 

203 return RegistryField(doc, self, default, optional, multi) 

204 

205 

206class RegistryAdaptor(collections.abc.Mapping): 

207 """Private class that makes a `Registry` behave like the thing a 

208 `~lsst.pex.config.ConfigChoiceField` expects. 

209 

210 Parameters 

211 ---------- 

212 registry : `Registry` 

213 `Registry` instance. 

214 """ 

215 

216 def __init__(self, registry): 

217 self.registry = registry 

218 

219 def __getitem__(self, k): 

220 return self.registry[k].ConfigClass 

221 

222 def __iter__(self): 

223 return iter(self.registry) 

224 

225 def __len__(self): 

226 return len(self.registry) 

227 

228 def __contains__(self, k): 

229 return k in self.registry 

230 

231 

232class RegistryInstanceDict(ConfigInstanceDict): 

233 """Dictionary of instantiated configs, used to populate a `RegistryField`. 

234 

235 Parameters 

236 ---------- 

237 config : `lsst.pex.config.Config` 

238 Configuration instance. 

239 field : `RegistryField` 

240 Configuration field. 

241 """ 

242 

243 def __init__(self, config, field): 

244 ConfigInstanceDict.__init__(self, config, field) 

245 self.registry = field.registry 

246 

247 def _getTarget(self): 

248 if self._field.multi: 

249 raise FieldValidationError( 

250 self._field, self._config, "Multi-selection field has no attribute 'target'" 

251 ) 

252 return self.types.registry[self._selection] 

253 

254 target = property(_getTarget) 

255 

256 def _getTargets(self): 

257 if not self._field.multi: 

258 raise FieldValidationError( 

259 self._field, self._config, "Single-selection field has no attribute 'targets'" 

260 ) 

261 return [self.types.registry[c] for c in self._selection] 

262 

263 targets = property(_getTargets) 

264 

265 def apply(self, *args, **kw): 

266 """Call the active target(s) with the active config as a keyword arg 

267 

268 If this is a multi-selection field, return a list obtained by calling 

269 each active target with its corresponding active config. 

270 

271 Additional arguments will be passed on to the configurable target(s) 

272 """ 

273 if self.active is None: 

274 msg = "No selection has been made. Options: %s" % " ".join(self.types.registry.keys()) 

275 raise FieldValidationError(self._field, self._config, msg) 

276 if self._field.multi: 

277 retvals = [] 

278 for c in self._selection: 

279 retvals.append(self.types.registry[c](*args, config=self[c], **kw)) 

280 return retvals 

281 else: 

282 return self.types.registry[self.name](*args, config=self[self.name], **kw) 

283 

284 def __setattr__(self, attr, value): 

285 if attr == "registry": 

286 object.__setattr__(self, attr, value) 

287 else: 

288 ConfigInstanceDict.__setattr__(self, attr, value) 

289 

290 

291class RegistryField(ConfigChoiceField): 

292 """A configuration field whose options are defined in a `Registry`. 

293 

294 Parameters 

295 ---------- 

296 doc : `str` 

297 A description of the field. 

298 registry : `Registry` 

299 The registry that contains this field. 

300 default : `str`, optional 

301 The default target key. 

302 optional : `bool`, optional 

303 When `False`, `lsst.pex.config.Config.validate` fails if the field's 

304 value is `None`. 

305 multi : `bool`, optional 

306 If `True`, the field allows multiple selections. The default is 

307 `False`. 

308 

309 See also 

310 -------- 

311 ChoiceField 

312 ConfigChoiceField 

313 ConfigDictField 

314 ConfigField 

315 ConfigurableField 

316 DictField 

317 Field 

318 ListField 

319 RangeField 

320 """ 

321 

322 instanceDictClass = RegistryInstanceDict 

323 """Class used to hold configurable instances in the field. 

324 """ 

325 

326 def __init__(self, doc, registry, default=None, optional=False, multi=False): 

327 types = RegistryAdaptor(registry) 

328 self.registry = registry 

329 ConfigChoiceField.__init__(self, doc, types, default, optional, multi) 

330 

331 def __deepcopy__(self, memo): 

332 """Customize deep-copying, want a reference to the original registry. 

333 

334 WARNING: this must be overridden by subclasses if they change the 

335 constructor signature! 

336 """ 

337 other = type(self)( 

338 doc=self.doc, 

339 registry=self.registry, 

340 default=copy.deepcopy(self.default), 

341 optional=self.optional, 

342 multi=self.multi, 

343 ) 

344 other.source = self.source 

345 return other 

346 

347 

348def makeRegistry(doc, configBaseType=Config): 

349 """Create a `Registry`. 

350 

351 Parameters 

352 ---------- 

353 doc : `str` 

354 Docstring for the created `Registry` (this is set as the ``__doc__`` 

355 attribute of the `Registry` instance. 

356 configBaseType : `lsst.pex.config.Config`-type 

357 Base type of config classes in the `Registry` 

358 (`lsst.pex.config.Registry.configBaseType`). 

359 

360 Returns 

361 ------- 

362 registry : `Registry` 

363 Registry with ``__doc__`` and `~Registry.configBaseType` attributes 

364 set. 

365 """ 

366 cls = type("Registry", (Registry,), {"__doc__": doc}) 

367 return cls(configBaseType=configBaseType) 

368 

369 

370def registerConfigurable(name, registry, ConfigClass=None): 

371 """A decorator that adds a class as a configurable in a `Registry` 

372 instance. 

373 

374 Parameters 

375 ---------- 

376 name : `str` 

377 Name of the target (the decorated class) in the ``registry``. 

378 registry : `Registry` 

379 The `Registry` instance that the decorated class is added to. 

380 ConfigClass : `lsst.pex.config.Config`-type, optional 

381 Config class associated with the configurable. If `None`, the class's 

382 ``ConfigClass`` attribute is used instead. 

383 

384 See also 

385 -------- 

386 registerConfig 

387 

388 Notes 

389 ----- 

390 Internally, this decorator runs `Registry.register`. 

391 """ 

392 

393 def decorate(cls): 

394 registry.register(name, target=cls, ConfigClass=ConfigClass) 

395 return cls 

396 

397 return decorate 

398 

399 

400def registerConfig(name, registry, target): 

401 """Decorator that adds a class as a ``ConfigClass`` in a `Registry` and 

402 associates it with the given configurable. 

403 

404 Parameters 

405 ---------- 

406 name : `str` 

407 Name of the ``target`` in the ``registry``. 

408 registry : `Registry` 

409 The registry containing the ``target``. 

410 target : obj 

411 A configurable type, such as a subclass of `lsst.pipe.base.Task`. 

412 

413 See also 

414 -------- 

415 registerConfigurable 

416 

417 Notes 

418 ----- 

419 Internally, this decorator runs `Registry.register`. 

420 """ 

421 

422 def decorate(cls): 

423 registry.register(name, target=target, ConfigClass=cls) 

424 return cls 

425 

426 return decorate