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 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__ = ["ConfigChoiceField"] 

29 

30import copy 

31import collections.abc 

32 

33from .config import Config, Field, FieldValidationError, _typeStr, _joinNamePath 

34from .comparison import getComparisonName, compareScalars, compareConfigs 

35from .callStack import getCallStack, getStackFrame 

36 

37 

38class SelectionSet(collections.abc.MutableSet): 

39 """A mutable set class that tracks the selection of multi-select 

40 `~lsst.pex.config.ConfigChoiceField` objects. 

41 

42 Parameters 

43 ---------- 

44 dict_ : `ConfigInstanceDict` 

45 The dictionary of instantiated configs. 

46 value 

47 The selected key. 

48 at : `lsst.pex.config.callStack.StackFrame`, optional 

49 The call stack when the selection was made. 

50 label : `str`, optional 

51 Label for history tracking. 

52 setHistory : `bool`, optional 

53 Add this even to the history, if `True`. 

54 

55 Notes 

56 ----- 

57 This class allows a user of a multi-select 

58 `~lsst.pex.config.ConfigChoiceField` to add or discard items from the set 

59 of active configs. Each change to the selection is tracked in the field's 

60 history. 

61 """ 

62 

63 def __init__(self, dict_, value, at=None, label="assignment", setHistory=True): 

64 if at is None: 

65 at = getCallStack() 

66 self._dict = dict_ 

67 self._field = self._dict._field 

68 self._config = self._dict._config 

69 self.__history = self._config._history.setdefault(self._field.name, []) 

70 if value is not None: 

71 try: 

72 for v in value: 

73 if v not in self._dict: 

74 # invoke __getitem__ to ensure it's present 

75 self._dict.__getitem__(v, at=at) 

76 except TypeError: 

77 msg = "Value %s is of incorrect type %s. Sequence type expected" % (value, _typeStr(value)) 

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

79 self._set = set(value) 

80 else: 

81 self._set = set() 

82 

83 if setHistory: 

84 self.__history.append(("Set selection to %s" % self, at, label)) 

85 

86 def add(self, value, at=None): 

87 """Add a value to the selected set. 

88 """ 

89 if self._config._frozen: 

90 raise FieldValidationError(self._field, self._config, 

91 "Cannot modify a frozen Config") 

92 

93 if at is None: 

94 at = getCallStack() 

95 

96 if value not in self._dict: 

97 # invoke __getitem__ to make sure it's present 

98 self._dict.__getitem__(value, at=at) 

99 

100 self.__history.append(("added %s to selection" % value, at, "selection")) 

101 self._set.add(value) 

102 

103 def discard(self, value, at=None): 

104 """Discard a value from the selected set. 

105 """ 

106 if self._config._frozen: 

107 raise FieldValidationError(self._field, self._config, 

108 "Cannot modify a frozen Config") 

109 

110 if value not in self._dict: 

111 return 

112 

113 if at is None: 

114 at = getCallStack() 

115 

116 self.__history.append(("removed %s from selection" % value, at, "selection")) 

117 self._set.discard(value) 

118 

119 def __len__(self): 

120 return len(self._set) 

121 

122 def __iter__(self): 

123 return iter(self._set) 

124 

125 def __contains__(self, value): 

126 return value in self._set 

127 

128 def __repr__(self): 

129 return repr(list(self._set)) 

130 

131 def __str__(self): 

132 return str(list(self._set)) 

133 

134 

135class ConfigInstanceDict(collections.abc.Mapping): 

136 """Dictionary of instantiated configs, used to populate a 

137 `~lsst.pex.config.ConfigChoiceField`. 

138 

139 Parameters 

140 ---------- 

141 config : `lsst.pex.config.Config` 

142 A configuration instance. 

143 field : `lsst.pex.config.Field`-type 

144 A configuration field. Note that the `lsst.pex.config.Field.fieldmap` 

145 attribute must provide key-based access to configuration classes, 

146 (that is, ``typemap[name]``). 

147 """ 

148 def __init__(self, config, field): 

149 collections.abc.Mapping.__init__(self) 

150 self._dict = dict() 

151 self._selection = None 

152 self._config = config 

153 self._field = field 

154 self._history = config._history.setdefault(field.name, []) 

155 self.__doc__ = field.doc 

156 

157 types = property(lambda x: x._field.typemap) 157 ↛ exitline 157 didn't run the lambda on line 157

158 

159 def __contains__(self, k): 

160 return k in self._field.typemap 

161 

162 def __len__(self): 

163 return len(self._field.typemap) 

164 

165 def __iter__(self): 

166 return iter(self._field.typemap) 

167 

168 def _setSelection(self, value, at=None, label="assignment"): 

169 if self._config._frozen: 

170 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config") 

171 

172 if at is None: 

173 at = getCallStack(1) 

174 

175 if value is None: 

176 self._selection = None 

177 elif self._field.multi: 

178 self._selection = SelectionSet(self, value, setHistory=False) 

179 else: 

180 if value not in self._dict: 

181 self.__getitem__(value, at=at) # just invoke __getitem__ to make sure it's present 

182 self._selection = value 

183 self._history.append((value, at, label)) 

184 

185 def _getNames(self): 

186 if not self._field.multi: 

187 raise FieldValidationError(self._field, self._config, 

188 "Single-selection field has no attribute 'names'") 

189 return self._selection 

190 

191 def _setNames(self, value): 

192 if not self._field.multi: 

193 raise FieldValidationError(self._field, self._config, 

194 "Single-selection field has no attribute 'names'") 

195 self._setSelection(value) 

196 

197 def _delNames(self): 

198 if not self._field.multi: 

199 raise FieldValidationError(self._field, self._config, 

200 "Single-selection field has no attribute 'names'") 

201 self._selection = None 

202 

203 def _getName(self): 

204 if self._field.multi: 

205 raise FieldValidationError(self._field, self._config, 

206 "Multi-selection field has no attribute 'name'") 

207 return self._selection 

208 

209 def _setName(self, value): 

210 if self._field.multi: 

211 raise FieldValidationError(self._field, self._config, 

212 "Multi-selection field has no attribute 'name'") 

213 self._setSelection(value) 

214 

215 def _delName(self): 

216 if self._field.multi: 

217 raise FieldValidationError(self._field, self._config, 

218 "Multi-selection field has no attribute 'name'") 

219 self._selection = None 

220 

221 names = property(_getNames, _setNames, _delNames) 

222 """List of names of active items in a multi-selection 

223 ``ConfigInstanceDict``. Disabled in a single-selection ``_Registry``; use 

224 the `name` attribute instead. 

225 """ 

226 

227 name = property(_getName, _setName, _delName) 

228 """Name of the active item in a single-selection ``ConfigInstanceDict``. 

229 Disabled in a multi-selection ``_Registry``; use the ``names`` attribute 

230 instead. 

231 """ 

232 

233 def _getActive(self): 

234 if self._selection is None: 

235 return None 

236 

237 if self._field.multi: 

238 return [self[c] for c in self._selection] 

239 else: 

240 return self[self._selection] 

241 

242 active = property(_getActive) 

243 """The selected items. 

244 

245 For multi-selection, this is equivalent to: ``[self[name] for name in 

246 self.names]``. For single-selection, this is equivalent to: ``self[name]``. 

247 """ 

248 

249 def __getitem__(self, k, at=None, label="default"): 

250 try: 

251 value = self._dict[k] 

252 except KeyError: 

253 try: 

254 dtype = self._field.typemap[k] 

255 except Exception: 

256 raise FieldValidationError(self._field, self._config, 

257 "Unknown key %r in Registry/ConfigChoiceField" % k) 

258 name = _joinNamePath(self._config._name, self._field.name, k) 

259 if at is None: 

260 at = getCallStack() 

261 at.insert(0, dtype._source) 

262 value = self._dict.setdefault(k, dtype(__name=name, __at=at, __label=label)) 

263 return value 

264 

265 def __setitem__(self, k, value, at=None, label="assignment"): 

266 if self._config._frozen: 

267 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config") 

268 

269 try: 

270 dtype = self._field.typemap[k] 

271 except Exception: 

272 raise FieldValidationError(self._field, self._config, "Unknown key %r" % k) 

273 

274 if value != dtype and type(value) != dtype: 

275 msg = "Value %s at key %k is of incorrect type %s. Expected type %s" % \ 

276 (value, k, _typeStr(value), _typeStr(dtype)) 

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

278 

279 if at is None: 

280 at = getCallStack() 

281 name = _joinNamePath(self._config._name, self._field.name, k) 

282 oldValue = self._dict.get(k, None) 

283 if oldValue is None: 

284 if value == dtype: 

285 self._dict[k] = value(__name=name, __at=at, __label=label) 

286 else: 

287 self._dict[k] = dtype(__name=name, __at=at, __label=label, **value._storage) 

288 else: 

289 if value == dtype: 

290 value = value() 

291 oldValue.update(__at=at, __label=label, **value._storage) 

292 

293 def _rename(self, fullname): 

294 for k, v in self._dict.items(): 

295 v._rename(_joinNamePath(name=fullname, index=k)) 

296 

297 def __setattr__(self, attr, value, at=None, label="assignment"): 

298 if hasattr(getattr(self.__class__, attr, None), '__set__'): 

299 # This allows properties to work. 

300 object.__setattr__(self, attr, value) 

301 elif attr in self.__dict__ or attr in ["_history", "_field", "_config", "_dict", 

302 "_selection", "__doc__"]: 

303 # This allows specific private attributes to work. 

304 object.__setattr__(self, attr, value) 

305 else: 

306 # We throw everything else. 

307 msg = "%s has no attribute %s" % (_typeStr(self._field), attr) 

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

309 

310 

311class ConfigChoiceField(Field): 

312 """A configuration field (`~lsst.pex.config.Field` subclass) that allows a 

313 user to choose from a set of `~lsst.pex.config.Config` types. 

314 

315 Parameters 

316 ---------- 

317 doc : `str` 

318 Documentation string for the field. 

319 typemap : `dict`-like 

320 A mapping between keys and `~lsst.pex.config.Config`-types as values. 

321 See *Examples* for details. 

322 default : `str`, optional 

323 The default configuration name. 

324 optional : `bool`, optional 

325 When `False`, `lsst.pex.config.Config.validate` will fail if the 

326 field's value is `None`. 

327 multi : `bool`, optional 

328 If `True`, the field allows multiple selections. In this case, set the 

329 selections by assigning a sequence to the ``names`` attribute of the 

330 field. 

331 

332 If `False`, the field allows only a single selection. In this case, 

333 set the active config by assigning the config's key from the 

334 ``typemap`` to the field's ``name`` attribute (see *Examples*). 

335 deprecated : None or `str`, optional 

336 A description of why this Field is deprecated, including removal date. 

337 If not None, the string is appended to the docstring for this Field. 

338 

339 See also 

340 -------- 

341 ChoiceField 

342 ConfigDictField 

343 ConfigField 

344 ConfigurableField 

345 DictField 

346 Field 

347 ListField 

348 RangeField 

349 RegistryField 

350 

351 Notes 

352 ----- 

353 ``ConfigChoiceField`` instances can allow either single selections or 

354 multiple selections, depending on the ``multi`` parameter. For 

355 single-selection fields, set the selection with the ``name`` attribute. 

356 For multi-selection fields, set the selection though the ``names`` 

357 attribute. 

358 

359 This field is validated only against the active selection. If the 

360 ``active`` attribute is `None` and the field is not optional, validation 

361 will fail. 

362 

363 When saving a configuration with a ``ConfigChoiceField``, the entire set is 

364 saved, as well as the active selection. 

365 

366 Examples 

367 -------- 

368 While the ``typemap`` is shared by all instances of the field, each 

369 instance of the field has its own instance of a particular sub-config type. 

370 

371 For example, ``AaaConfig`` is a config object 

372 

373 >>> from lsst.pex.config import Config, ConfigChoiceField, Field 

374 >>> class AaaConfig(Config): 

375 ... somefield = Field("doc", int) 

376 ... 

377 

378 The ``MyConfig`` config has a ``ConfigChoiceField`` field called ``choice`` 

379 that maps the ``AaaConfig`` type to the ``"AAA"`` key: 

380 

381 >>> TYPEMAP = {"AAA", AaaConfig} 

382 >>> class MyConfig(Config): 

383 ... choice = ConfigChoiceField("doc for choice", TYPEMAP) 

384 ... 

385 

386 Creating an instance of ``MyConfig``: 

387 

388 >>> instance = MyConfig() 

389 

390 Setting value of the field ``somefield`` on the "AAA" key of the ``choice`` 

391 field: 

392 

393 >>> instance.choice['AAA'].somefield = 5 

394 

395 **Selecting the active configuration** 

396 

397 Make the ``"AAA"`` key the active configuration value for the ``choice`` 

398 field: 

399 

400 >>> instance.choice = "AAA" 

401 

402 Alternatively, the last line can be written: 

403 

404 >>> instance.choice.name = "AAA" 

405 

406 (If the config instance allows multiple selections, you'd assign a sequence 

407 to the ``names`` attribute instead.) 

408 

409 ``ConfigChoiceField`` instances also allow multiple values of the same 

410 type: 

411 

412 >>> TYPEMAP["CCC"] = AaaConfig 

413 >>> TYPEMAP["BBB"] = AaaConfig 

414 """ 

415 

416 instanceDictClass = ConfigInstanceDict 

417 

418 def __init__(self, doc, typemap, default=None, optional=False, multi=False, deprecated=None): 

419 source = getStackFrame() 

420 self._setup(doc=doc, dtype=self.instanceDictClass, default=default, check=None, optional=optional, 

421 source=source, deprecated=deprecated) 

422 self.typemap = typemap 

423 self.multi = multi 

424 

425 def _getOrMake(self, instance, label="default"): 

426 instanceDict = instance._storage.get(self.name) 

427 if instanceDict is None: 

428 at = getCallStack(1) 

429 instanceDict = self.dtype(instance, self) 

430 instanceDict.__doc__ = self.doc 

431 instance._storage[self.name] = instanceDict 

432 history = instance._history.setdefault(self.name, []) 

433 history.append(("Initialized from defaults", at, label)) 

434 

435 return instanceDict 

436 

437 def __get__(self, instance, owner=None): 

438 if instance is None or not isinstance(instance, Config): 

439 return self 

440 else: 

441 return self._getOrMake(instance) 

442 

443 def __set__(self, instance, value, at=None, label="assignment"): 

444 if instance._frozen: 

445 raise FieldValidationError(self, instance, "Cannot modify a frozen Config") 

446 if at is None: 

447 at = getCallStack() 

448 instanceDict = self._getOrMake(instance) 

449 if isinstance(value, self.instanceDictClass): 

450 for k, v in value.items(): 

451 instanceDict.__setitem__(k, v, at=at, label=label) 

452 instanceDict._setSelection(value._selection, at=at, label=label) 

453 

454 else: 

455 instanceDict._setSelection(value, at=at, label=label) 

456 

457 def rename(self, instance): 

458 instanceDict = self.__get__(instance) 

459 fullname = _joinNamePath(instance._name, self.name) 

460 instanceDict._rename(fullname) 

461 

462 def validate(self, instance): 

463 instanceDict = self.__get__(instance) 

464 if instanceDict.active is None and not self.optional: 

465 msg = "Required field cannot be None" 

466 raise FieldValidationError(self, instance, msg) 

467 elif instanceDict.active is not None: 

468 if self.multi: 

469 for a in instanceDict.active: 

470 a.validate() 

471 else: 

472 instanceDict.active.validate() 

473 

474 def toDict(self, instance): 

475 instanceDict = self.__get__(instance) 

476 

477 dict_ = {} 

478 if self.multi: 

479 dict_["names"] = instanceDict.names 

480 else: 

481 dict_["name"] = instanceDict.name 

482 

483 values = {} 

484 for k, v in instanceDict.items(): 

485 values[k] = v.toDict() 

486 dict_["values"] = values 

487 

488 return dict_ 

489 

490 def freeze(self, instance): 

491 # When a config is frozen it should not be affected by anything further 

492 # being added to a registry, so create a deep copy of the registry 

493 # typemap 

494 self.typemap = copy.deepcopy(self.typemap) 

495 instanceDict = self.__get__(instance) 

496 for v in instanceDict.values(): 

497 v.freeze() 

498 

499 def _collectImports(self, instance, imports): 

500 instanceDict = self.__get__(instance) 

501 for config in instanceDict.values(): 

502 config._collectImports() 

503 imports |= config._imports 

504 

505 def save(self, outfile, instance): 

506 instanceDict = self.__get__(instance) 

507 fullname = _joinNamePath(instance._name, self.name) 

508 for v in instanceDict.values(): 

509 v._save(outfile) 

510 if self.multi: 

511 outfile.write(u"{}.names={!r}\n".format(fullname, instanceDict.names)) 

512 else: 

513 outfile.write(u"{}.name={!r}\n".format(fullname, instanceDict.name)) 

514 

515 def __deepcopy__(self, memo): 

516 """Customize deep-copying, because we always want a reference to the 

517 original typemap. 

518 

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

520 constructor signature! 

521 """ 

522 other = type(self)(doc=self.doc, typemap=self.typemap, default=copy.deepcopy(self.default), 

523 optional=self.optional, multi=self.multi) 

524 other.source = self.source 

525 return other 

526 

527 def _compare(self, instance1, instance2, shortcut, rtol, atol, output): 

528 """Compare two fields for equality. 

529 

530 Used by `lsst.pex.ConfigChoiceField.compare`. 

531 

532 Parameters 

533 ---------- 

534 instance1 : `lsst.pex.config.Config` 

535 Left-hand side config instance to compare. 

536 instance2 : `lsst.pex.config.Config` 

537 Right-hand side config instance to compare. 

538 shortcut : `bool` 

539 If `True`, this function returns as soon as an inequality if found. 

540 rtol : `float` 

541 Relative tolerance for floating point comparisons. 

542 atol : `float` 

543 Absolute tolerance for floating point comparisons. 

544 output : callable 

545 A callable that takes a string, used (possibly repeatedly) to 

546 report inequalities. 

547 

548 Returns 

549 ------- 

550 isEqual : bool 

551 `True` if the fields are equal, `False` otherwise. 

552 

553 Notes 

554 ----- 

555 Only the selected configurations are compared, as the parameters of any 

556 others do not matter. 

557 

558 Floating point comparisons are performed by `numpy.allclose`. 

559 """ 

560 d1 = getattr(instance1, self.name) 

561 d2 = getattr(instance2, self.name) 

562 name = getComparisonName( 

563 _joinNamePath(instance1._name, self.name), 

564 _joinNamePath(instance2._name, self.name) 

565 ) 

566 if not compareScalars("selection for %s" % name, d1._selection, d2._selection, output=output): 

567 return False 

568 if d1._selection is None: 

569 return True 

570 if self.multi: 

571 nested = [(k, d1[k], d2[k]) for k in d1._selection] 

572 else: 

573 nested = [(d1._selection, d1[d1._selection], d2[d1._selection])] 

574 equal = True 

575 for k, c1, c2 in nested: 

576 result = compareConfigs("%s[%r]" % (name, k), c1, c2, shortcut=shortcut, 

577 rtol=rtol, atol=atol, output=output) 

578 if not result and shortcut: 

579 return False 

580 equal = equal and result 

581 return equal