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 self._typemap = None 

157 

158 @property 

159 def types(self): 

160 return self._typemap if self._typemap is not None else self._field.typemap 

161 

162 def __contains__(self, k): 

163 return k in self.types 

164 

165 def __len__(self): 

166 return len(self.types) 

167 

168 def __iter__(self): 

169 return iter(self.types) 

170 

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

172 if self._config._frozen: 

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

174 

175 if at is None: 

176 at = getCallStack(1) 

177 

178 if value is None: 

179 self._selection = None 

180 elif self._field.multi: 

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

182 else: 

183 if value not in self._dict: 

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

185 self._selection = value 

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

187 

188 def _getNames(self): 

189 if not self._field.multi: 

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

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

192 return self._selection 

193 

194 def _setNames(self, value): 

195 if not self._field.multi: 

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

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

198 self._setSelection(value) 

199 

200 def _delNames(self): 

201 if not self._field.multi: 

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

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

204 self._selection = None 

205 

206 def _getName(self): 

207 if self._field.multi: 

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

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

210 return self._selection 

211 

212 def _setName(self, value): 

213 if self._field.multi: 

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

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

216 self._setSelection(value) 

217 

218 def _delName(self): 

219 if self._field.multi: 

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

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

222 self._selection = None 

223 

224 names = property(_getNames, _setNames, _delNames) 

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

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

227 the `name` attribute instead. 

228 """ 

229 

230 name = property(_getName, _setName, _delName) 

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

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

233 instead. 

234 """ 

235 

236 def _getActive(self): 

237 if self._selection is None: 

238 return None 

239 

240 if self._field.multi: 

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

242 else: 

243 return self[self._selection] 

244 

245 active = property(_getActive) 

246 """The selected items. 

247 

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

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

250 """ 

251 

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

253 try: 

254 value = self._dict[k] 

255 except KeyError: 

256 try: 

257 dtype = self.types[k] 

258 except Exception: 

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

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

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

262 if at is None: 

263 at = getCallStack() 

264 at.insert(0, dtype._source) 

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

266 return value 

267 

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

269 if self._config._frozen: 

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

271 

272 try: 

273 dtype = self.types[k] 

274 except Exception: 

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

276 

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

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

279 (value, k, _typeStr(value), _typeStr(dtype)) 

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

281 

282 if at is None: 

283 at = getCallStack() 

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

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

286 if oldValue is None: 

287 if value == dtype: 

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

289 else: 

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

291 else: 

292 if value == dtype: 

293 value = value() 

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

295 

296 def _rename(self, fullname): 

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

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

299 

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

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

302 # This allows properties to work. 

303 object.__setattr__(self, attr, value) 

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

305 "_selection", "__doc__", "_typemap"]: 

306 # This allows specific private attributes to work. 

307 object.__setattr__(self, attr, value) 

308 else: 

309 # We throw everything else. 

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

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

312 

313 def freeze(self): 

314 """Invoking this freeze method will create a local copy of the field 

315 attribute's typemap. This decouples this instance dict from the 

316 underlying objects type map ensuring that and subsequent changes to the 

317 typemap will not be reflected in this instance (i.e imports adding 

318 additional registry entries). 

319 """ 

320 if self._typemap is None: 

321 self._typemap = copy.deepcopy(self.types) 

322 

323 

324class ConfigChoiceField(Field): 

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

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

327 

328 Parameters 

329 ---------- 

330 doc : `str` 

331 Documentation string for the field. 

332 typemap : `dict`-like 

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

334 See *Examples* for details. 

335 default : `str`, optional 

336 The default configuration name. 

337 optional : `bool`, optional 

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

339 field's value is `None`. 

340 multi : `bool`, optional 

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

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

343 field. 

344 

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

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

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

348 deprecated : None or `str`, optional 

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

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

351 

352 See also 

353 -------- 

354 ChoiceField 

355 ConfigDictField 

356 ConfigField 

357 ConfigurableField 

358 DictField 

359 Field 

360 ListField 

361 RangeField 

362 RegistryField 

363 

364 Notes 

365 ----- 

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

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

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

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

370 attribute. 

371 

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

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

374 will fail. 

375 

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

377 saved, as well as the active selection. 

378 

379 Examples 

380 -------- 

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

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

383 

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

385 

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

387 >>> class AaaConfig(Config): 

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

389 ... 

390 

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

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

393 

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

395 >>> class MyConfig(Config): 

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

397 ... 

398 

399 Creating an instance of ``MyConfig``: 

400 

401 >>> instance = MyConfig() 

402 

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

404 field: 

405 

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

407 

408 **Selecting the active configuration** 

409 

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

411 field: 

412 

413 >>> instance.choice = "AAA" 

414 

415 Alternatively, the last line can be written: 

416 

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

418 

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

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

421 

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

423 type: 

424 

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

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

427 """ 

428 

429 instanceDictClass = ConfigInstanceDict 

430 

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

432 source = getStackFrame() 

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

434 source=source, deprecated=deprecated) 

435 self.typemap = typemap 

436 self.multi = multi 

437 

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

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

440 if instanceDict is None: 

441 at = getCallStack(1) 

442 instanceDict = self.dtype(instance, self) 

443 instanceDict.__doc__ = self.doc 

444 instance._storage[self.name] = instanceDict 

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

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

447 

448 return instanceDict 

449 

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

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

452 return self 

453 else: 

454 return self._getOrMake(instance) 

455 

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

457 if instance._frozen: 

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

459 if at is None: 

460 at = getCallStack() 

461 instanceDict = self._getOrMake(instance) 

462 if isinstance(value, self.instanceDictClass): 

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

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

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

466 

467 else: 

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

469 

470 def rename(self, instance): 

471 instanceDict = self.__get__(instance) 

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

473 instanceDict._rename(fullname) 

474 

475 def validate(self, instance): 

476 instanceDict = self.__get__(instance) 

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

478 msg = "Required field cannot be None" 

479 raise FieldValidationError(self, instance, msg) 

480 elif instanceDict.active is not None: 

481 if self.multi: 

482 for a in instanceDict.active: 

483 a.validate() 

484 else: 

485 instanceDict.active.validate() 

486 

487 def toDict(self, instance): 

488 instanceDict = self.__get__(instance) 

489 

490 dict_ = {} 

491 if self.multi: 

492 dict_["names"] = instanceDict.names 

493 else: 

494 dict_["name"] = instanceDict.name 

495 

496 values = {} 

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

498 values[k] = v.toDict() 

499 dict_["values"] = values 

500 

501 return dict_ 

502 

503 def freeze(self, instance): 

504 instanceDict = self.__get__(instance) 

505 instanceDict.freeze() 

506 for v in instanceDict.values(): 

507 v.freeze() 

508 

509 def _collectImports(self, instance, imports): 

510 instanceDict = self.__get__(instance) 

511 for config in instanceDict.values(): 

512 config._collectImports() 

513 imports |= config._imports 

514 

515 def save(self, outfile, instance): 

516 instanceDict = self.__get__(instance) 

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

518 for v in instanceDict.values(): 

519 v._save(outfile) 

520 if self.multi: 

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

522 else: 

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

524 

525 def __deepcopy__(self, memo): 

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

527 original typemap. 

528 

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

530 constructor signature! 

531 """ 

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

533 optional=self.optional, multi=self.multi) 

534 other.source = self.source 

535 return other 

536 

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

538 """Compare two fields for equality. 

539 

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

541 

542 Parameters 

543 ---------- 

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

545 Left-hand side config instance to compare. 

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

547 Right-hand side config instance to compare. 

548 shortcut : `bool` 

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

550 rtol : `float` 

551 Relative tolerance for floating point comparisons. 

552 atol : `float` 

553 Absolute tolerance for floating point comparisons. 

554 output : callable 

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

556 report inequalities. 

557 

558 Returns 

559 ------- 

560 isEqual : bool 

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

562 

563 Notes 

564 ----- 

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

566 others do not matter. 

567 

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

569 """ 

570 d1 = getattr(instance1, self.name) 

571 d2 = getattr(instance2, self.name) 

572 name = getComparisonName( 

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

574 _joinNamePath(instance2._name, self.name) 

575 ) 

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

577 return False 

578 if d1._selection is None: 

579 return True 

580 if self.multi: 

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

582 else: 

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

584 equal = True 

585 for k, c1, c2 in nested: 

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

587 rtol=rtol, atol=atol, output=output) 

588 if not result and shortcut: 

589 return False 

590 equal = equal and result 

591 return equal