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

104 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:41 +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", "RegistryField", "makeRegistry", "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 : `lsst.pipe.base.Task` or other 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 >>> class Foo: 

96 ... ConfigClass = FooConfig 

97 ... 

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

99 ... self.config = config 

100 ... 

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

102 ... return self.config.val + num 

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(f"configBaseType={_typeStr(configBaseType)} must be a subclass of Config") 

126 self._configBaseType = configBaseType 

127 self._dict = {} 

128 

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

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

131 

132 Parameters 

133 ---------- 

134 name : `str` 

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

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

137 the key. 

138 target : `lsst.pipe.base.Task` or other configurable type 

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

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

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

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

143 attribute is used. 

144 

145 Raises 

146 ------ 

147 RuntimeError 

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

149 AttributeError 

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

151 a ``ConfigClass`` attribute. 

152 

153 Notes 

154 ----- 

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

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

157 the original ``target`` is stored. 

158 """ 

159 if name in self._dict: 

160 raise RuntimeError(f"An item with name {name!r} already exists") 

161 if ConfigClass is None: 

162 wrapper = target 

163 else: 

164 wrapper = ConfigurableWrapper(target, ConfigClass) 

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

166 raise TypeError( 

167 f"ConfigClass={_typeStr(wrapper.ConfigClass)} is not a subclass of " 

168 f"{_typeStr(self._configBaseType)!r}" 

169 ) 

170 self._dict[name] = wrapper 

171 

172 def __getitem__(self, key): 

173 return self._dict[key] 

174 

175 def __len__(self): 

176 return len(self._dict) 

177 

178 def __iter__(self): 

179 return iter(self._dict) 

180 

181 def __contains__(self, key): 

182 return key in self._dict 

183 

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

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

186 

187 Parameters 

188 ---------- 

189 doc : `str` 

190 A description of the field. 

191 default : `object`, optional 

192 The default target for the field. 

193 optional : `bool`, optional 

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

195 field's value is `None`. 

196 multi : `bool`, optional 

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

198 `True`. 

199 on_none : `Callable`, optional 

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

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

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

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

204 

205 Returns 

206 ------- 

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

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

209 """ 

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

211 

212 

213class RegistryAdaptor(collections.abc.Mapping): 

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

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

216 

217 Parameters 

218 ---------- 

219 registry : `Registry` 

220 `Registry` instance. 

221 """ 

222 

223 def __init__(self, registry): 

224 self.registry = registry 

225 

226 def __getitem__(self, k): 

227 return self.registry[k].ConfigClass 

228 

229 def __iter__(self): 

230 return iter(self.registry) 

231 

232 def __len__(self): 

233 return len(self.registry) 

234 

235 def __contains__(self, k): 

236 return k in self.registry 

237 

238 

239class RegistryInstanceDict(ConfigInstanceDict): 

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

241 

242 Parameters 

243 ---------- 

244 config : `lsst.pex.config.Config` 

245 Configuration instance. 

246 field : `RegistryField` 

247 Configuration field. 

248 """ 

249 

250 def __init__(self, config, field): 

251 ConfigInstanceDict.__init__(self, config, field) 

252 self.registry = field.registry 

253 

254 def _getTarget(self): 

255 if self._field.multi: 

256 raise FieldValidationError( 

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

258 ) 

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

260 

261 target = property(_getTarget) 

262 

263 def _getTargets(self): 

264 if not self._field.multi: 

265 raise FieldValidationError( 

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

267 ) 

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

269 

270 targets = property(_getTargets) 

271 

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

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

274 

275 Parameters 

276 ---------- 

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

278 Additional arguments will be passed on to the configurable 

279 target(s). 

280 

281 Returns 

282 ------- 

283 result 

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

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

286 """ 

287 if self.active is None: 

288 if self._field._on_none is not None: 

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

290 msg = "No selection has been made. Options: {}".format(" ".join(self.types.registry.keys())) 

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

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

293 

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

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

296 arg. 

297 

298 Parameters 

299 ---------- 

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

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

302 *args, **kwargs 

303 Additional arguments will be passed on to the configurable 

304 target(s). 

305 

306 Returns 

307 ------- 

308 result 

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

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

311 

312 Notes 

313 ----- 

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

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

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

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

318 configured. 

319 """ 

320 if self._field.multi: 

321 retvals = [] 

322 for c in selection: 

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

324 return retvals 

325 else: 

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

327 

328 def __setattr__(self, attr, value): 

329 if attr == "registry": 

330 object.__setattr__(self, attr, value) 

331 else: 

332 ConfigInstanceDict.__setattr__(self, attr, value) 

333 

334 

335class RegistryField(ConfigChoiceField): 

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

337 

338 Parameters 

339 ---------- 

340 doc : `str` 

341 A description of the field. 

342 registry : `Registry` 

343 The registry that contains this field. 

344 default : `str`, optional 

345 The default target key. 

346 optional : `bool`, optional 

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

348 value is `None`. 

349 multi : `bool`, optional 

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

351 `False`. 

352 on_none : `Callable`, optional 

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

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

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

356 arguments passed to ``apply``. 

357 

358 See Also 

359 -------- 

360 ChoiceField 

361 ConfigChoiceField 

362 ConfigDictField 

363 ConfigField 

364 ConfigurableField 

365 DictField 

366 Field 

367 ListField 

368 RangeField 

369 """ 

370 

371 instanceDictClass = RegistryInstanceDict 

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

373 """ 

374 

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

376 types = RegistryAdaptor(registry) 

377 self.registry = registry 

378 self._on_none = on_none 

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

380 

381 def __deepcopy__(self, memo): 

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

383 

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

385 constructor signature! 

386 """ 

387 other = type(self)( 

388 doc=self.doc, 

389 registry=self.registry, 

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

391 optional=self.optional, 

392 multi=self.multi, 

393 on_none=self._on_none, 

394 ) 

395 other.source = self.source 

396 return other 

397 

398 

399def makeRegistry(doc, configBaseType=Config): 

400 """Create a `Registry`. 

401 

402 Parameters 

403 ---------- 

404 doc : `str` 

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

406 attribute of the `Registry` instance. 

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

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

409 

410 Returns 

411 ------- 

412 registry : `Registry` 

413 Registry with ``__doc__`` and ``_configBaseType`` attributes 

414 set. 

415 """ 

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

417 return cls(configBaseType=configBaseType) 

418 

419 

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

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

422 

423 Parameters 

424 ---------- 

425 name : `str` 

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

427 registry : `Registry` 

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

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

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

431 ``ConfigClass`` attribute is used instead. 

432 

433 See Also 

434 -------- 

435 registerConfig 

436 

437 Notes 

438 ----- 

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

440 """ 

441 

442 def decorate(cls): 

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

444 return cls 

445 

446 return decorate 

447 

448 

449def registerConfig(name, registry, target): 

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

451 associate it with the given configurable. 

452 

453 Parameters 

454 ---------- 

455 name : `str` 

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

457 registry : `Registry` 

458 The registry containing the ``target``. 

459 target : `lsst.pipe.base.Task` or other configurable type 

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

461 

462 See Also 

463 -------- 

464 registerConfigurable 

465 

466 Notes 

467 ----- 

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

469 """ 

470 

471 def decorate(cls): 

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

473 return cls 

474 

475 return decorate