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

104 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 02:59 -0700

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, on_none=None): 

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 on_none: `Callable`, optional 

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

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

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

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

202 

203 Returns 

204 ------- 

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

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

207 """ 

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

209 

210 

211class RegistryAdaptor(collections.abc.Mapping): 

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

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

214 

215 Parameters 

216 ---------- 

217 registry : `Registry` 

218 `Registry` instance. 

219 """ 

220 

221 def __init__(self, registry): 

222 self.registry = registry 

223 

224 def __getitem__(self, k): 

225 return self.registry[k].ConfigClass 

226 

227 def __iter__(self): 

228 return iter(self.registry) 

229 

230 def __len__(self): 

231 return len(self.registry) 

232 

233 def __contains__(self, k): 

234 return k in self.registry 

235 

236 

237class RegistryInstanceDict(ConfigInstanceDict): 

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

239 

240 Parameters 

241 ---------- 

242 config : `lsst.pex.config.Config` 

243 Configuration instance. 

244 field : `RegistryField` 

245 Configuration field. 

246 """ 

247 

248 def __init__(self, config, field): 

249 ConfigInstanceDict.__init__(self, config, field) 

250 self.registry = field.registry 

251 

252 def _getTarget(self): 

253 if self._field.multi: 

254 raise FieldValidationError( 

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

256 ) 

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

258 

259 target = property(_getTarget) 

260 

261 def _getTargets(self): 

262 if not self._field.multi: 

263 raise FieldValidationError( 

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

265 ) 

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

267 

268 targets = property(_getTargets) 

269 

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

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

272 

273 Parameters 

274 ---------- 

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

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

277 *args, **kwargs 

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: %s" % " ".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 (`lsst.pex.config.Registry.configBaseType`). 

410 

411 Returns 

412 ------- 

413 registry : `Registry` 

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

415 set. 

416 """ 

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

418 return cls(configBaseType=configBaseType) 

419 

420 

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

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

423 instance. 

424 

425 Parameters 

426 ---------- 

427 name : `str` 

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

429 registry : `Registry` 

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

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

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

433 ``ConfigClass`` attribute is used instead. 

434 

435 See also 

436 -------- 

437 registerConfig 

438 

439 Notes 

440 ----- 

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

442 """ 

443 

444 def decorate(cls): 

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

446 return cls 

447 

448 return decorate 

449 

450 

451def registerConfig(name, registry, target): 

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

453 associates it with the given configurable. 

454 

455 Parameters 

456 ---------- 

457 name : `str` 

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

459 registry : `Registry` 

460 The registry containing the ``target``. 

461 target : obj 

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

463 

464 See also 

465 -------- 

466 registerConfigurable 

467 

468 Notes 

469 ----- 

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

471 """ 

472 

473 def decorate(cls): 

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

475 return cls 

476 

477 return decorate