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

104 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-20 11:16 +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 Parameters 

44 ---------- 

45 target : configurable class 

46 Target class. 

47 ConfigClass : `type` 

48 Config class. 

49 """ 

50 

51 def __init__(self, target, ConfigClass): 

52 self.ConfigClass = ConfigClass 

53 self._target = target 

54 

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

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

57 

58 

59class Registry(collections.abc.Mapping): 

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

61 

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

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

64 *Notes*). 

65 

66 Parameters 

67 ---------- 

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

69 The base class for config classes in the registry. 

70 

71 Notes 

72 ----- 

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

74 Configurables typically create an algorithm or are themselves the 

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

76 this is not required. 

77 

78 A ``Registry`` has these requirements: 

79 

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

81 signature. 

82 - All configurables in a registry typically share something important 

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

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

85 particular call signature. 

86 

87 Examples 

88 -------- 

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

90 registry. First, creating the configurable: 

91 

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

93 >>> class FooConfig(Config): 

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

95 ... 

96 >>> class Foo: 

97 ... ConfigClass = FooConfig 

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

99 ... self.config = config 

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

101 ... return self.config.val + num 

102 ... 

103 

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

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

106 

107 >>> registry = Registry() 

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

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

110 ["foo"] 

111 

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

113 

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

115 instance of it: 

116 

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

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

119 >>> foo.addVal(5) 

120 8 

121 """ 

122 

123 def __init__(self, configBaseType=Config): 

124 if not issubclass(configBaseType, Config): 

125 raise TypeError( 

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

127 % _typeStr( 

128 configBaseType, 

129 ) 

130 ) 

131 self._configBaseType = configBaseType 

132 self._dict = {} 

133 

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

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

136 

137 Parameters 

138 ---------- 

139 name : `str` 

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

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

142 the key. 

143 target : obj 

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

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

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

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

148 attribute is used. 

149 

150 Raises 

151 ------ 

152 RuntimeError 

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

154 AttributeError 

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

156 a ``ConfigClass`` attribute. 

157 

158 Notes 

159 ----- 

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

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

162 the original ``target`` is stored. 

163 """ 

164 if name in self._dict: 

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

166 if ConfigClass is None: 

167 wrapper = target 

168 else: 

169 wrapper = ConfigurableWrapper(target, ConfigClass) 

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

171 raise TypeError( 

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

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

174 ) 

175 self._dict[name] = wrapper 

176 

177 def __getitem__(self, key): 

178 return self._dict[key] 

179 

180 def __len__(self): 

181 return len(self._dict) 

182 

183 def __iter__(self): 

184 return iter(self._dict) 

185 

186 def __contains__(self, key): 

187 return key in self._dict 

188 

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

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

191 

192 Parameters 

193 ---------- 

194 doc : `str` 

195 A description of the field. 

196 default : object, optional 

197 The default target for the field. 

198 optional : `bool`, optional 

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

200 field's value is `None`. 

201 multi : `bool`, optional 

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

203 `True`. 

204 on_none : `Callable`, optional 

205 A callable that should be invoked when ``apply`` is called but the 

206 selected name or names is `None`. Will be passed the field 

207 attribute proxy (`RegistryInstanceDict`) and then all positional 

208 and keyword arguments passed to ``apply``. 

209 

210 Returns 

211 ------- 

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

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

214 """ 

215 return RegistryField(doc, self, default, optional, multi, on_none=on_none) 

216 

217 

218class RegistryAdaptor(collections.abc.Mapping): 

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

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

221 

222 Parameters 

223 ---------- 

224 registry : `Registry` 

225 `Registry` instance. 

226 """ 

227 

228 def __init__(self, registry): 

229 self.registry = registry 

230 

231 def __getitem__(self, k): 

232 return self.registry[k].ConfigClass 

233 

234 def __iter__(self): 

235 return iter(self.registry) 

236 

237 def __len__(self): 

238 return len(self.registry) 

239 

240 def __contains__(self, k): 

241 return k in self.registry 

242 

243 

244class RegistryInstanceDict(ConfigInstanceDict): 

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

246 

247 Parameters 

248 ---------- 

249 config : `lsst.pex.config.Config` 

250 Configuration instance. 

251 field : `RegistryField` 

252 Configuration field. 

253 """ 

254 

255 def __init__(self, config, field): 

256 ConfigInstanceDict.__init__(self, config, field) 

257 self.registry = field.registry 

258 

259 def _getTarget(self): 

260 if self._field.multi: 

261 raise FieldValidationError( 

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

263 ) 

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

265 

266 target = property(_getTarget) 

267 

268 def _getTargets(self): 

269 if not self._field.multi: 

270 raise FieldValidationError( 

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

272 ) 

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

274 

275 targets = property(_getTargets) 

276 

277 def apply(self, *args, **kwargs): 

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

279 

280 Parameters 

281 ---------- 

282 *args, **kwargs : `~typing.Any 

283 Additional arguments will be passed on to the configurable 

284 target(s). 

285 

286 Returns 

287 ------- 

288 result 

289 If this is a single-selection field, the return value from calling 

290 the target. If this is a multi-selection field, a list thereof. 

291 """ 

292 if self.active is None: 

293 if self._field._on_none is not None: 

294 return self._field._on_none(self, *args, **kwargs) 

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

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

297 return self.apply_with(self._selection, *args, **kwargs) 

298 

299 def apply_with(self, selection, *args, **kwargs): 

300 """Call named target(s) with the corresponding config as a keyword 

301 arg. 

302 

303 Parameters 

304 ---------- 

305 selection : `str` or `~collections.abc.Iterable` [ `str` ] 

306 Name or names of targets, depending on whether ``multi=True``. 

307 *args, **kwargs 

308 Additional arguments will be passed on to the configurable 

309 target(s). 

310 

311 Returns 

312 ------- 

313 result 

314 If this is a single-selection field, the return value from calling 

315 the target. If this is a multi-selection field, a list thereof. 

316 

317 Notes 

318 ----- 

319 This method ignores the current selection in the ``name`` or ``names`` 

320 attribute, which is usually not what you want. This method is most 

321 useful in ``on_none`` callbacks provided at field construction, which 

322 allow a context-dependent default to be used when no selection is 

323 configured. 

324 """ 

325 if self._field.multi: 

326 retvals = [] 

327 for c in selection: 

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

329 return retvals 

330 else: 

331 return self.types.registry[selection](*args, config=self[selection], **kwargs) 

332 

333 def __setattr__(self, attr, value): 

334 if attr == "registry": 

335 object.__setattr__(self, attr, value) 

336 else: 

337 ConfigInstanceDict.__setattr__(self, attr, value) 

338 

339 

340class RegistryField(ConfigChoiceField): 

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

342 

343 Parameters 

344 ---------- 

345 doc : `str` 

346 A description of the field. 

347 registry : `Registry` 

348 The registry that contains this field. 

349 default : `str`, optional 

350 The default target key. 

351 optional : `bool`, optional 

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

353 value is `None`. 

354 multi : `bool`, optional 

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

356 `False`. 

357 on_none : `Callable`, optional 

358 A callable that should be invoked when ``apply`` is called but the 

359 selected name or names is `None`. Will be passed the field attribute 

360 proxy (`RegistryInstanceDict`) and then all positional and keyword 

361 arguments passed to ``apply``. 

362 

363 See Also 

364 -------- 

365 ChoiceField 

366 ConfigChoiceField 

367 ConfigDictField 

368 ConfigField 

369 ConfigurableField 

370 DictField 

371 Field 

372 ListField 

373 RangeField 

374 """ 

375 

376 instanceDictClass = RegistryInstanceDict 

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

378 """ 

379 

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

381 types = RegistryAdaptor(registry) 

382 self.registry = registry 

383 self._on_none = on_none 

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

385 

386 def __deepcopy__(self, memo): 

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

388 

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

390 constructor signature! 

391 """ 

392 other = type(self)( 

393 doc=self.doc, 

394 registry=self.registry, 

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

396 optional=self.optional, 

397 multi=self.multi, 

398 on_none=self._on_none, 

399 ) 

400 other.source = self.source 

401 return other 

402 

403 

404def makeRegistry(doc, configBaseType=Config): 

405 """Create a `Registry`. 

406 

407 Parameters 

408 ---------- 

409 doc : `str` 

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

411 attribute of the `Registry` instance. 

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

413 Base type of config classes in the `Registry`. 

414 

415 Returns 

416 ------- 

417 registry : `Registry` 

418 Registry with ``__doc__`` and ``_configBaseType`` attributes 

419 set. 

420 """ 

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

422 return cls(configBaseType=configBaseType) 

423 

424 

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

426 """Add a class as a configurable in a `Registry` instance. 

427 

428 Parameters 

429 ---------- 

430 name : `str` 

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

432 registry : `Registry` 

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

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

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

436 ``ConfigClass`` attribute is used instead. 

437 

438 See Also 

439 -------- 

440 registerConfig 

441 

442 Notes 

443 ----- 

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

445 """ 

446 

447 def decorate(cls): 

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

449 return cls 

450 

451 return decorate 

452 

453 

454def registerConfig(name, registry, target): 

455 """Add a class as a ``ConfigClass`` in a `Registry` and 

456 associate it with the given configurable. 

457 

458 Parameters 

459 ---------- 

460 name : `str` 

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

462 registry : `Registry` 

463 The registry containing the ``target``. 

464 target : obj 

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

466 

467 See Also 

468 -------- 

469 registerConfigurable 

470 

471 Notes 

472 ----- 

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

474 """ 

475 

476 def decorate(cls): 

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

478 return cls 

479 

480 return decorate