Coverage for python/lsst/pex/config/config.py: 60%

Shortcuts 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

408 statements  

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__ = ("Config", "ConfigMeta", "Field", "FieldValidationError") 

29 

30import io 

31import importlib 

32import os 

33import re 

34import sys 

35import math 

36import copy 

37import tempfile 

38import shutil 

39import warnings 

40 

41# if YAML is not available that's fine and we simply don't register 

42# the yaml representer since we know it won't be used. 

43try: 

44 import yaml 

45except ImportError: 

46 yaml = None 

47 YamlLoaders = () 

48 doImport = None 

49 

50from .comparison import getComparisonName, compareScalars, compareConfigs 

51from .callStack import getStackFrame, getCallStack 

52 

53if yaml: 53 ↛ 64line 53 didn't jump to line 64, because the condition on line 53 was never false

54 YamlLoaders = (yaml.Loader, yaml.FullLoader, yaml.SafeLoader, yaml.UnsafeLoader) 

55 

56 try: 

57 # CLoader is not always available 

58 from yaml import CLoader 

59 YamlLoaders += (CLoader,) 

60 except ImportError: 

61 pass 

62 

63 

64def _joinNamePath(prefix=None, name=None, index=None): 

65 """Generate nested configuration names. 

66 """ 

67 if not prefix and not name: 67 ↛ 68line 67 didn't jump to line 68, because the condition on line 67 was never true

68 raise ValueError("Invalid name: cannot be None") 

69 elif not name: 69 ↛ 70line 69 didn't jump to line 70, because the condition on line 69 was never true

70 name = prefix 

71 elif prefix and name: 71 ↛ 74line 71 didn't jump to line 74, because the condition on line 71 was never false

72 name = prefix + "." + name 

73 

74 if index is not None: 74 ↛ 75line 74 didn't jump to line 75, because the condition on line 74 was never true

75 return "%s[%r]" % (name, index) 

76 else: 

77 return name 

78 

79 

80def _autocast(x, dtype): 

81 """Cast a value to a type, if appropriate. 

82 

83 Parameters 

84 ---------- 

85 x : object 

86 A value. 

87 dtype : tpye 

88 Data type, such as `float`, `int`, or `str`. 

89 

90 Returns 

91 ------- 

92 values : object 

93 If appropriate, the returned value is ``x`` cast to the given type 

94 ``dtype``. If the cast cannot be performed the original value of 

95 ``x`` is returned. 

96 """ 

97 if dtype == float and isinstance(x, int): 

98 return float(x) 

99 return x 

100 

101 

102def _typeStr(x): 

103 """Generate a fully-qualified type name. 

104 

105 Returns 

106 ------- 

107 `str` 

108 Fully-qualified type name. 

109 

110 Notes 

111 ----- 

112 This function is used primarily for writing config files to be executed 

113 later upon with the 'load' function. 

114 """ 

115 if hasattr(x, '__module__') and hasattr(x, '__name__'): 

116 xtype = x 

117 else: 

118 xtype = type(x) 

119 if (sys.version_info.major <= 2 and xtype.__module__ == '__builtin__') or xtype.__module__ == 'builtins': 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true

120 return xtype.__name__ 

121 else: 

122 return "%s.%s" % (xtype.__module__, xtype.__name__) 

123 

124 

125if yaml: 125 ↛ 157line 125 didn't jump to line 157, because the condition on line 125 was never false

126 def _yaml_config_representer(dumper, data): 

127 """Represent a Config object in a form suitable for YAML. 

128 

129 Stores the serialized stream as a scalar block string. 

130 """ 

131 stream = io.StringIO() 

132 data.saveToStream(stream) 

133 config_py = stream.getvalue() 

134 

135 # Strip multiple newlines from the end of the config 

136 # This simplifies the YAML to use | and not |+ 

137 config_py = config_py.rstrip() + "\n" 

138 

139 # Trailing spaces force pyyaml to use non-block form. 

140 # Remove the trailing spaces so it has no choice 

141 config_py = re.sub(r"\s+$", "\n", config_py, flags=re.MULTILINE) 

142 

143 # Store the Python as a simple scalar 

144 return dumper.represent_scalar("lsst.pex.config.Config", config_py, style="|") 

145 

146 def _yaml_config_constructor(loader, node): 

147 """Construct a config from YAML""" 

148 config_py = loader.construct_scalar(node) 

149 return Config._fromPython(config_py) 

150 

151 # Register a generic constructor for Config and all subclasses 

152 # Need to register for all the loaders we would like to use 

153 for loader in YamlLoaders: 

154 yaml.add_constructor("lsst.pex.config.Config", _yaml_config_constructor, Loader=loader) 

155 

156 

157class ConfigMeta(type): 

158 """A metaclass for `lsst.pex.config.Config`. 

159 

160 Notes 

161 ----- 

162 ``ConfigMeta`` adds a dictionary containing all `~lsst.pex.config.Field` 

163 class attributes as a class attribute called ``_fields``, and adds 

164 the name of each field as an instance variable of the field itself (so you 

165 don't have to pass the name of the field to the field constructor). 

166 """ 

167 

168 def __init__(cls, name, bases, dict_): 

169 type.__init__(cls, name, bases, dict_) 

170 cls._fields = {} 

171 cls._source = getStackFrame() 

172 

173 def getFields(classtype): 

174 fields = {} 

175 bases = list(classtype.__bases__) 

176 bases.reverse() 

177 for b in bases: 

178 fields.update(getFields(b)) 

179 

180 for k, v in classtype.__dict__.items(): 

181 if isinstance(v, Field): 

182 fields[k] = v 

183 return fields 

184 

185 fields = getFields(cls) 

186 for k, v in fields.items(): 

187 setattr(cls, k, copy.deepcopy(v)) 

188 

189 def __setattr__(cls, name, value): 

190 if isinstance(value, Field): 

191 value.name = name 

192 cls._fields[name] = value 

193 type.__setattr__(cls, name, value) 

194 

195 

196class FieldValidationError(ValueError): 

197 """Raised when a ``~lsst.pex.config.Field`` is not valid in a 

198 particular ``~lsst.pex.config.Config``. 

199 

200 Parameters 

201 ---------- 

202 field : `lsst.pex.config.Field` 

203 The field that was not valid. 

204 config : `lsst.pex.config.Config` 

205 The config containing the invalid field. 

206 msg : `str` 

207 Text describing why the field was not valid. 

208 """ 

209 

210 def __init__(self, field, config, msg): 

211 self.fieldType = type(field) 

212 """Type of the `~lsst.pex.config.Field` that incurred the error. 

213 """ 

214 

215 self.fieldName = field.name 

216 """Name of the `~lsst.pex.config.Field` instance that incurred the 

217 error (`str`). 

218 

219 See also 

220 -------- 

221 lsst.pex.config.Field.name 

222 """ 

223 

224 self.fullname = _joinNamePath(config._name, field.name) 

225 """Fully-qualified name of the `~lsst.pex.config.Field` instance 

226 (`str`). 

227 """ 

228 

229 self.history = config.history.setdefault(field.name, []) 

230 """Full history of all changes to the `~lsst.pex.config.Field` 

231 instance. 

232 """ 

233 

234 self.fieldSource = field.source 

235 """File and line number of the `~lsst.pex.config.Field` definition. 

236 """ 

237 

238 self.configSource = config._source 

239 error = "%s '%s' failed validation: %s\n"\ 

240 "For more information see the Field definition at:\n%s"\ 

241 " and the Config definition at:\n%s" % \ 

242 (self.fieldType.__name__, self.fullname, msg, 

243 self.fieldSource.format(), self.configSource.format()) 

244 super().__init__(error) 

245 

246 

247class Field: 

248 """A field in a `~lsst.pex.config.Config` that supports `int`, `float`, 

249 `complex`, `bool`, and `str` data types. 

250 

251 Parameters 

252 ---------- 

253 doc : `str` 

254 A description of the field for users. 

255 dtype : type 

256 The field's data type. ``Field`` only supports basic data types: 

257 `int`, `float`, `complex`, `bool`, and `str`. See 

258 `Field.supportedTypes`. 

259 default : object, optional 

260 The field's default value. 

261 check : callable, optional 

262 A callable that is called with the field's value. This callable should 

263 return `False` if the value is invalid. More complex inter-field 

264 validation can be written as part of the 

265 `lsst.pex.config.Config.validate` method. 

266 optional : `bool`, optional 

267 This sets whether the field is considered optional, and therefore 

268 doesn't need to be set by the user. When `False`, 

269 `lsst.pex.config.Config.validate` fails if the field's value is `None`. 

270 deprecated : None or `str`, optional 

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

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

273 

274 Raises 

275 ------ 

276 ValueError 

277 Raised when the ``dtype`` parameter is not one of the supported types 

278 (see `Field.supportedTypes`). 

279 

280 See also 

281 -------- 

282 ChoiceField 

283 ConfigChoiceField 

284 ConfigDictField 

285 ConfigField 

286 ConfigurableField 

287 DictField 

288 ListField 

289 RangeField 

290 RegistryField 

291 

292 Notes 

293 ----- 

294 ``Field`` instances (including those of any subclass of ``Field``) are used 

295 as class attributes of `~lsst.pex.config.Config` subclasses (see the 

296 example, below). ``Field`` attributes work like the `property` attributes 

297 of classes that implement custom setters and getters. `Field` attributes 

298 belong to the class, but operate on the instance. Formally speaking, 

299 `Field` attributes are `descriptors 

300 <https://docs.python.org/3/howto/descriptor.html>`_. 

301 

302 When you access a `Field` attribute on a `Config` instance, you don't 

303 get the `Field` instance itself. Instead, you get the value of that field, 

304 which might be a simple type (`int`, `float`, `str`, `bool`) or a custom 

305 container type (like a `lsst.pex.config.List`) depending on the field's 

306 type. See the example, below. 

307 

308 Examples 

309 -------- 

310 Instances of ``Field`` should be used as class attributes of 

311 `lsst.pex.config.Config` subclasses: 

312 

313 >>> from lsst.pex.config import Config, Field 

314 >>> class Example(Config): 

315 ... myInt = Field("An integer field.", int, default=0) 

316 ... 

317 >>> print(config.myInt) 

318 0 

319 >>> config.myInt = 5 

320 >>> print(config.myInt) 

321 5 

322 """ 

323 

324 supportedTypes = set((str, bool, float, int, complex)) 

325 """Supported data types for field values (`set` of types). 

326 """ 

327 

328 def __init__(self, doc, dtype, default=None, check=None, optional=False, deprecated=None): 

329 if dtype not in self.supportedTypes: 329 ↛ 330line 329 didn't jump to line 330, because the condition on line 329 was never true

330 raise ValueError("Unsupported Field dtype %s" % _typeStr(dtype)) 

331 

332 source = getStackFrame() 

333 self._setup(doc=doc, dtype=dtype, default=default, check=check, optional=optional, source=source, 

334 deprecated=deprecated) 

335 

336 def _setup(self, doc, dtype, default, check, optional, source, deprecated): 

337 """Set attributes, usually during initialization. 

338 """ 

339 self.dtype = dtype 

340 """Data type for the field. 

341 """ 

342 

343 # append the deprecation message to the docstring. 

344 if deprecated is not None: 

345 doc = f"{doc} Deprecated: {deprecated}" 

346 self.doc = doc 

347 """A description of the field (`str`). 

348 """ 

349 

350 self.deprecated = deprecated 

351 """If not None, a description of why this field is deprecated (`str`). 

352 """ 

353 

354 self.__doc__ = f"{doc} (`{dtype.__name__}`" 

355 if optional or default is not None: 

356 self.__doc__ += f", default ``{default!r}``" 

357 self.__doc__ += ")" 

358 

359 self.default = default 

360 """Default value for this field. 

361 """ 

362 

363 self.check = check 

364 """A user-defined function that validates the value of the field. 

365 """ 

366 

367 self.optional = optional 

368 """Flag that determines if the field is required to be set (`bool`). 

369 

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

371 field's value is `None`. 

372 """ 

373 

374 self.source = source 

375 """The stack frame where this field is defined (`list` of 

376 `lsst.pex.config.callStack.StackFrame`). 

377 """ 

378 

379 def rename(self, instance): 

380 """Rename the field in a `~lsst.pex.config.Config` (for internal use 

381 only). 

382 

383 Parameters 

384 ---------- 

385 instance : `lsst.pex.config.Config` 

386 The config instance that contains this field. 

387 

388 Notes 

389 ----- 

390 This method is invoked by the `lsst.pex.config.Config` object that 

391 contains this field and should not be called directly. 

392 

393 Renaming is only relevant for `~lsst.pex.config.Field` instances that 

394 hold subconfigs. `~lsst.pex.config.Fields` that hold subconfigs should 

395 rename each subconfig with the full field name as generated by 

396 `lsst.pex.config.config._joinNamePath`. 

397 """ 

398 pass 

399 

400 def validate(self, instance): 

401 """Validate the field (for internal use only). 

402 

403 Parameters 

404 ---------- 

405 instance : `lsst.pex.config.Config` 

406 The config instance that contains this field. 

407 

408 Raises 

409 ------ 

410 lsst.pex.config.FieldValidationError 

411 Raised if verification fails. 

412 

413 Notes 

414 ----- 

415 This method provides basic validation: 

416 

417 - Ensures that the value is not `None` if the field is not optional. 

418 - Ensures type correctness. 

419 - Ensures that the user-provided ``check`` function is valid. 

420 

421 Most `~lsst.pex.config.Field` subclasses should call 

422 `lsst.pex.config.field.Field.validate` if they re-implement 

423 `~lsst.pex.config.field.Field.validate`. 

424 """ 

425 value = self.__get__(instance) 

426 if not self.optional and value is None: 

427 raise FieldValidationError(self, instance, "Required value cannot be None") 

428 

429 def freeze(self, instance): 

430 """Make this field read-only (for internal use only). 

431 

432 Parameters 

433 ---------- 

434 instance : `lsst.pex.config.Config` 

435 The config instance that contains this field. 

436 

437 Notes 

438 ----- 

439 Freezing is only relevant for fields that hold subconfigs. Fields which 

440 hold subconfigs should freeze each subconfig. 

441 

442 **Subclasses should implement this method.** 

443 """ 

444 pass 

445 

446 def _validateValue(self, value): 

447 """Validate a value. 

448 

449 Parameters 

450 ---------- 

451 value : object 

452 The value being validated. 

453 

454 Raises 

455 ------ 

456 TypeError 

457 Raised if the value's type is incompatible with the field's 

458 ``dtype``. 

459 ValueError 

460 Raised if the value is rejected by the ``check`` method. 

461 """ 

462 if value is None: 462 ↛ 463line 462 didn't jump to line 463, because the condition on line 462 was never true

463 return 

464 

465 if not isinstance(value, self.dtype): 465 ↛ 466line 465 didn't jump to line 466, because the condition on line 465 was never true

466 msg = "Value %s is of incorrect type %s. Expected type %s" % \ 

467 (value, _typeStr(value), _typeStr(self.dtype)) 

468 raise TypeError(msg) 

469 if self.check is not None and not self.check(value): 469 ↛ 470line 469 didn't jump to line 470, because the condition on line 469 was never true

470 msg = "Value %s is not a valid value" % str(value) 

471 raise ValueError(msg) 

472 

473 def _collectImports(self, instance, imports): 

474 """This function should call the _collectImports method on all config 

475 objects the field may own, and union them with the supplied imports 

476 set. 

477 

478 Parameters 

479 ---------- 

480 instance : instance or subclass of `lsst.pex.config.Config` 

481 A config object that has this field defined on it 

482 imports : `set` 

483 Set of python modules that need imported after persistence 

484 """ 

485 pass 

486 

487 def save(self, outfile, instance): 

488 """Save this field to a file (for internal use only). 

489 

490 Parameters 

491 ---------- 

492 outfile : file-like object 

493 A writeable field handle. 

494 instance : `Config` 

495 The `Config` instance that contains this field. 

496 

497 Notes 

498 ----- 

499 This method is invoked by the `~lsst.pex.config.Config` object that 

500 contains this field and should not be called directly. 

501 

502 The output consists of the documentation string 

503 (`lsst.pex.config.Field.doc`) formatted as a Python comment. The second 

504 line is formatted as an assignment: ``{fullname}={value}``. 

505 

506 This output can be executed with Python. 

507 """ 

508 value = self.__get__(instance) 

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

510 

511 if self.deprecated and value == self.default: 511 ↛ 512line 511 didn't jump to line 512, because the condition on line 511 was never true

512 return 

513 

514 # write full documentation string as comment lines 

515 # (i.e. first character is #) 

516 doc = "# " + str(self.doc).replace("\n", "\n# ") 

517 if isinstance(value, float) and not math.isfinite(value): 517 ↛ 519line 517 didn't jump to line 519, because the condition on line 517 was never true

518 # non-finite numbers need special care 

519 outfile.write(u"{}\n{}=float('{!r}')\n\n".format(doc, fullname, value)) 

520 else: 

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

522 

523 def toDict(self, instance): 

524 """Convert the field value so that it can be set as the value of an 

525 item in a `dict` (for internal use only). 

526 

527 Parameters 

528 ---------- 

529 instance : `Config` 

530 The `Config` that contains this field. 

531 

532 Returns 

533 ------- 

534 value : object 

535 The field's value. See *Notes*. 

536 

537 Notes 

538 ----- 

539 This method invoked by the owning `~lsst.pex.config.Config` object and 

540 should not be called directly. 

541 

542 Simple values are passed through. Complex data structures must be 

543 manipulated. For example, a `~lsst.pex.config.Field` holding a 

544 subconfig should, instead of the subconfig object, return a `dict` 

545 where the keys are the field names in the subconfig, and the values are 

546 the field values in the subconfig. 

547 """ 

548 return self.__get__(instance) 

549 

550 def __get__(self, instance, owner=None, at=None, label="default"): 

551 """Define how attribute access should occur on the Config instance 

552 This is invoked by the owning config object and should not be called 

553 directly 

554 

555 When the field attribute is accessed on a Config class object, it 

556 returns the field object itself in order to allow inspection of 

557 Config classes. 

558 

559 When the field attribute is access on a config instance, the actual 

560 value described by the field (and held by the Config instance) is 

561 returned. 

562 """ 

563 if instance is None: 563 ↛ 564line 563 didn't jump to line 564, because the condition on line 563 was never true

564 return self 

565 else: 

566 # try statements are almost free in python if they succeed 

567 try: 

568 return instance._storage[self.name] 

569 except AttributeError: 

570 if not isinstance(instance, Config): 

571 return self 

572 else: 

573 raise AttributeError(f"Config {instance} is missing " 

574 "_storage attribute, likely" 

575 " incorrectly initialized") 

576 

577 def __set__(self, instance, value, at=None, label='assignment'): 

578 """Set an attribute on the config instance. 

579 

580 Parameters 

581 ---------- 

582 instance : `lsst.pex.config.Config` 

583 The config instance that contains this field. 

584 value : obj 

585 Value to set on this field. 

586 at : `list` of `lsst.pex.config.callStack.StackFrame` 

587 The call stack (created by 

588 `lsst.pex.config.callStack.getCallStack`). 

589 label : `str`, optional 

590 Event label for the history. 

591 

592 Notes 

593 ----- 

594 This method is invoked by the owning `lsst.pex.config.Config` object 

595 and should not be called directly. 

596 

597 Derived `~lsst.pex.config.Field` classes may need to override the 

598 behavior. When overriding ``__set__``, `~lsst.pex.config.Field` authors 

599 should follow the following rules: 

600 

601 - Do not allow modification of frozen configs. 

602 - Validate the new value **before** modifying the field. Except if the 

603 new value is `None`. `None` is special and no attempt should be made 

604 to validate it until `lsst.pex.config.Config.validate` is called. 

605 - Do not modify the `~lsst.pex.config.Config` instance to contain 

606 invalid values. 

607 - If the field is modified, update the history of the 

608 `lsst.pex.config.field.Field` to reflect the changes. 

609 

610 In order to decrease the need to implement this method in derived 

611 `~lsst.pex.config.Field` types, value validation is performed in the 

612 `lsst.pex.config.Field._validateValue`. If only the validation step 

613 differs in the derived `~lsst.pex.config.Field`, it is simpler to 

614 implement `lsst.pex.config.Field._validateValue` than to reimplement 

615 ``__set__``. More complicated behavior, however, may require 

616 reimplementation. 

617 """ 

618 if instance._frozen: 618 ↛ 619line 618 didn't jump to line 619, because the condition on line 618 was never true

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

620 

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

622 if value is not None: 622 ↛ 629line 622 didn't jump to line 629, because the condition on line 622 was never false

623 value = _autocast(value, self.dtype) 

624 try: 

625 self._validateValue(value) 

626 except BaseException as e: 

627 raise FieldValidationError(self, instance, str(e)) 

628 

629 instance._storage[self.name] = value 

630 if at is None: 630 ↛ 631line 630 didn't jump to line 631, because the condition on line 630 was never true

631 at = getCallStack() 

632 history.append((value, at, label)) 

633 

634 def __delete__(self, instance, at=None, label='deletion'): 

635 """Delete an attribute from a `lsst.pex.config.Config` instance. 

636 

637 Parameters 

638 ---------- 

639 instance : `lsst.pex.config.Config` 

640 The config instance that contains this field. 

641 at : `list` of `lsst.pex.config.callStack.StackFrame` 

642 The call stack (created by 

643 `lsst.pex.config.callStack.getCallStack`). 

644 label : `str`, optional 

645 Event label for the history. 

646 

647 Notes 

648 ----- 

649 This is invoked by the owning `~lsst.pex.config.Config` object and 

650 should not be called directly. 

651 """ 

652 if at is None: 

653 at = getCallStack() 

654 self.__set__(instance, None, at=at, label=label) 

655 

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

657 """Compare a field (named `Field.name`) in two 

658 `~lsst.pex.config.Config` instances for equality. 

659 

660 Parameters 

661 ---------- 

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

663 Left-hand side `Config` instance to compare. 

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

665 Right-hand side `Config` instance to compare. 

666 shortcut : `bool`, optional 

667 **Unused.** 

668 rtol : `float`, optional 

669 Relative tolerance for floating point comparisons. 

670 atol : `float`, optional 

671 Absolute tolerance for floating point comparisons. 

672 output : callable, optional 

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

674 report inequalities. 

675 

676 Notes 

677 ----- 

678 This method must be overridden by more complex `Field` subclasses. 

679 

680 See also 

681 -------- 

682 lsst.pex.config.compareScalars 

683 """ 

684 v1 = getattr(instance1, self.name) 

685 v2 = getattr(instance2, self.name) 

686 name = getComparisonName( 

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

688 _joinNamePath(instance2._name, self.name) 

689 ) 

690 return compareScalars(name, v1, v2, dtype=self.dtype, rtol=rtol, atol=atol, output=output) 

691 

692 

693class RecordingImporter: 

694 """Importer (for `sys.meta_path`) that records which modules are being 

695 imported. 

696 

697 *This class does not do any importing itself.* 

698 

699 Examples 

700 -------- 

701 Use this class as a context manager to ensure it is properly uninstalled 

702 when done: 

703 

704 >>> with RecordingImporter() as importer: 

705 ... # import stuff 

706 ... import numpy as np 

707 ... print("Imported: " + importer.getModules()) 

708 """ 

709 

710 def __init__(self): 

711 self._modules = set() 

712 

713 def __enter__(self): 

714 self.origMetaPath = sys.meta_path 

715 sys.meta_path = [self] + sys.meta_path 

716 return self 

717 

718 def __exit__(self, *args): 

719 self.uninstall() 

720 return False # Don't suppress exceptions 

721 

722 def uninstall(self): 

723 """Uninstall the importer. 

724 """ 

725 sys.meta_path = self.origMetaPath 

726 

727 def find_module(self, fullname, path=None): 

728 """Called as part of the ``import`` chain of events. 

729 """ 

730 self._modules.add(fullname) 

731 # Return None because we don't do any importing. 

732 return None 

733 

734 def getModules(self): 

735 """Get the set of modules that were imported. 

736 

737 Returns 

738 ------- 

739 modules : `set` of `str` 

740 Set of imported module names. 

741 """ 

742 return self._modules 

743 

744 

745class Config(metaclass=ConfigMeta): 

746 """Base class for configuration (*config*) objects. 

747 

748 Notes 

749 ----- 

750 A ``Config`` object will usually have several `~lsst.pex.config.Field` 

751 instances as class attributes. These are used to define most of the base 

752 class behavior. 

753 

754 ``Config`` implements a mapping API that provides many `dict`-like methods, 

755 such as `keys`, `values`, `items`, `iteritems`, `iterkeys`, and 

756 `itervalues`. ``Config`` instances also support the ``in`` operator to 

757 test if a field is in the config. Unlike a `dict`, ``Config`` classes are 

758 not subscriptable. Instead, access individual fields as attributes of the 

759 configuration instance. 

760 

761 Examples 

762 -------- 

763 Config classes are subclasses of ``Config`` that have 

764 `~lsst.pex.config.Field` instances (or instances of 

765 `~lsst.pex.config.Field` subclasses) as class attributes: 

766 

767 >>> from lsst.pex.config import Config, Field, ListField 

768 >>> class DemoConfig(Config): 

769 ... intField = Field(doc="An integer field", dtype=int, default=42) 

770 ... listField = ListField(doc="List of favorite beverages.", dtype=str, 

771 ... default=['coffee', 'green tea', 'water']) 

772 ... 

773 >>> config = DemoConfig() 

774 

775 Configs support many `dict`-like APIs: 

776 

777 >>> config.keys() 

778 ['intField', 'listField'] 

779 >>> 'intField' in config 

780 True 

781 

782 Individual fields can be accessed as attributes of the configuration: 

783 

784 >>> config.intField 

785 42 

786 >>> config.listField.append('earl grey tea') 

787 >>> print(config.listField) 

788 ['coffee', 'green tea', 'water', 'earl grey tea'] 

789 """ 

790 

791 def __iter__(self): 

792 """Iterate over fields. 

793 """ 

794 return self._fields.__iter__() 

795 

796 def keys(self): 

797 """Get field names. 

798 

799 Returns 

800 ------- 

801 names : `list` 

802 List of `lsst.pex.config.Field` names. 

803 

804 See also 

805 -------- 

806 lsst.pex.config.Config.iterkeys 

807 """ 

808 return list(self._storage.keys()) 

809 

810 def values(self): 

811 """Get field values. 

812 

813 Returns 

814 ------- 

815 values : `list` 

816 List of field values. 

817 

818 See also 

819 -------- 

820 lsst.pex.config.Config.itervalues 

821 """ 

822 return list(self._storage.values()) 

823 

824 def items(self): 

825 """Get configurations as ``(field name, field value)`` pairs. 

826 

827 Returns 

828 ------- 

829 items : `list` 

830 List of tuples for each configuration. Tuple items are: 

831 

832 0. Field name. 

833 1. Field value. 

834 

835 See also 

836 -------- 

837 lsst.pex.config.Config.iteritems 

838 """ 

839 return list(self._storage.items()) 

840 

841 def iteritems(self): 

842 """Iterate over (field name, field value) pairs. 

843 

844 Yields 

845 ------ 

846 item : `tuple` 

847 Tuple items are: 

848 

849 0. Field name. 

850 1. Field value. 

851 

852 See also 

853 -------- 

854 lsst.pex.config.Config.items 

855 """ 

856 return iter(self._storage.items()) 

857 

858 def itervalues(self): 

859 """Iterate over field values. 

860 

861 Yields 

862 ------ 

863 value : obj 

864 A field value. 

865 

866 See also 

867 -------- 

868 lsst.pex.config.Config.values 

869 """ 

870 return iter(self.storage.values()) 

871 

872 def iterkeys(self): 

873 """Iterate over field names 

874 

875 Yields 

876 ------ 

877 key : `str` 

878 A field's key (attribute name). 

879 

880 See also 

881 -------- 

882 lsst.pex.config.Config.values 

883 """ 

884 return iter(self.storage.keys()) 

885 

886 def __contains__(self, name): 

887 """!Return True if the specified field exists in this config 

888 

889 @param[in] name field name to test for 

890 """ 

891 return self._storage.__contains__(name) 

892 

893 def __new__(cls, *args, **kw): 

894 """Allocate a new `lsst.pex.config.Config` object. 

895 

896 In order to ensure that all Config object are always in a proper state 

897 when handed to users or to derived `~lsst.pex.config.Config` classes, 

898 some attributes are handled at allocation time rather than at 

899 initialization. 

900 

901 This ensures that even if a derived `~lsst.pex.config.Config` class 

902 implements ``__init__``, its author does not need to be concerned about 

903 when or even the base ``Config.__init__`` should be called. 

904 """ 

905 name = kw.pop("__name", None) 

906 at = kw.pop("__at", getCallStack()) 

907 # remove __label and ignore it 

908 kw.pop("__label", "default") 

909 

910 instance = object.__new__(cls) 

911 instance._frozen = False 

912 instance._name = name 

913 instance._storage = {} 

914 instance._history = {} 

915 instance._imports = set() 

916 # load up defaults 

917 for field in instance._fields.values(): 

918 instance._history[field.name] = [] 

919 field.__set__(instance, field.default, at=at + [field.source], label="default") 

920 # set custom default-overides 

921 instance.setDefaults() 

922 # set constructor overides 

923 instance.update(__at=at, **kw) 

924 return instance 

925 

926 def __reduce__(self): 

927 """Reduction for pickling (function with arguments to reproduce). 

928 

929 We need to condense and reconstitute the `~lsst.pex.config.Config`, 

930 since it may contain lambdas (as the ``check`` elements) that cannot 

931 be pickled. 

932 """ 

933 # The stream must be in characters to match the API but pickle 

934 # requires bytes 

935 stream = io.StringIO() 

936 self.saveToStream(stream) 

937 return (unreduceConfig, (self.__class__, stream.getvalue().encode())) 

938 

939 def setDefaults(self): 

940 """Subclass hook for computing defaults. 

941 

942 Notes 

943 ----- 

944 Derived `~lsst.pex.config.Config` classes that must compute defaults 

945 rather than using the `~lsst.pex.config.Field` instances's defaults 

946 should do so here. To correctly use inherited defaults, 

947 implementations of ``setDefaults`` must call their base class's 

948 ``setDefaults``. 

949 """ 

950 pass 

951 

952 def update(self, **kw): 

953 """Update values of fields specified by the keyword arguments. 

954 

955 Parameters 

956 ---------- 

957 kw 

958 Keywords are configuration field names. Values are configuration 

959 field values. 

960 

961 Notes 

962 ----- 

963 The ``__at`` and ``__label`` keyword arguments are special internal 

964 keywords. They are used to strip out any internal steps from the 

965 history tracebacks of the config. Do not modify these keywords to 

966 subvert a `~lsst.pex.config.Config` instance's history. 

967 

968 Examples 

969 -------- 

970 This is a config with three fields: 

971 

972 >>> from lsst.pex.config import Config, Field 

973 >>> class DemoConfig(Config): 

974 ... fieldA = Field(doc='Field A', dtype=int, default=42) 

975 ... fieldB = Field(doc='Field B', dtype=bool, default=True) 

976 ... fieldC = Field(doc='Field C', dtype=str, default='Hello world') 

977 ... 

978 >>> config = DemoConfig() 

979 

980 These are the default values of each field: 

981 

982 >>> for name, value in config.iteritems(): 

983 ... print(f"{name}: {value}") 

984 ... 

985 fieldA: 42 

986 fieldB: True 

987 fieldC: 'Hello world' 

988 

989 Using this method to update ``fieldA`` and ``fieldC``: 

990 

991 >>> config.update(fieldA=13, fieldC='Updated!') 

992 

993 Now the values of each field are: 

994 

995 >>> for name, value in config.iteritems(): 

996 ... print(f"{name}: {value}") 

997 ... 

998 fieldA: 13 

999 fieldB: True 

1000 fieldC: 'Updated!' 

1001 """ 

1002 at = kw.pop("__at", getCallStack()) 

1003 label = kw.pop("__label", "update") 

1004 

1005 for name, value in kw.items(): 

1006 try: 

1007 field = self._fields[name] 

1008 field.__set__(self, value, at=at, label=label) 

1009 except KeyError: 

1010 raise KeyError("No field of name %s exists in config type %s" % (name, _typeStr(self))) 

1011 

1012 def load(self, filename, root="config"): 

1013 """Modify this config in place by executing the Python code in a 

1014 configuration file. 

1015 

1016 Parameters 

1017 ---------- 

1018 filename : `str` 

1019 Name of the configuration file. A configuration file is Python 

1020 module. 

1021 root : `str`, optional 

1022 Name of the variable in file that refers to the config being 

1023 overridden. 

1024 

1025 For example, the value of root is ``"config"`` and the file 

1026 contains:: 

1027 

1028 config.myField = 5 

1029 

1030 Then this config's field ``myField`` is set to ``5``. 

1031 

1032 **Deprecated:** For backwards compatibility, older config files 

1033 that use ``root="root"`` instead of ``root="config"`` will be 

1034 loaded with a warning printed to `sys.stderr`. This feature will be 

1035 removed at some point. 

1036 

1037 See also 

1038 -------- 

1039 lsst.pex.config.Config.loadFromStream 

1040 lsst.pex.config.Config.loadFromString 

1041 lsst.pex.config.Config.save 

1042 lsst.pex.config.Config.saveToStream 

1043 lsst.pex.config.Config.saveToString 

1044 """ 

1045 with open(filename, "r") as f: 

1046 code = compile(f.read(), filename=filename, mode="exec") 

1047 self.loadFromString(code, root=root, filename=filename) 

1048 

1049 def loadFromStream(self, stream, root="config", filename=None): 

1050 """Modify this Config in place by executing the Python code in the 

1051 provided stream. 

1052 

1053 Parameters 

1054 ---------- 

1055 stream : file-like object, `str`, `bytes`, or compiled string 

1056 Stream containing configuration override code. If this is a 

1057 code object, it should be compiled with ``mode="exec"``. 

1058 root : `str`, optional 

1059 Name of the variable in file that refers to the config being 

1060 overridden. 

1061 

1062 For example, the value of root is ``"config"`` and the file 

1063 contains:: 

1064 

1065 config.myField = 5 

1066 

1067 Then this config's field ``myField`` is set to ``5``. 

1068 

1069 **Deprecated:** For backwards compatibility, older config files 

1070 that use ``root="root"`` instead of ``root="config"`` will be 

1071 loaded with a warning printed to `sys.stderr`. This feature will be 

1072 removed at some point. 

1073 filename : `str`, optional 

1074 Name of the configuration file, or `None` if unknown or contained 

1075 in the stream. Used for error reporting. 

1076 

1077 Notes 

1078 ----- 

1079 For backwards compatibility reasons, this method accepts strings, bytes 

1080 and code objects as well as file-like objects. New code should use 

1081 `loadFromString` instead for most of these types. 

1082 

1083 See also 

1084 -------- 

1085 lsst.pex.config.Config.load 

1086 lsst.pex.config.Config.loadFromString 

1087 lsst.pex.config.Config.save 

1088 lsst.pex.config.Config.saveToStream 

1089 lsst.pex.config.Config.saveToString 

1090 """ 

1091 if hasattr(stream, "read"): 1091 ↛ 1092line 1091 didn't jump to line 1092, because the condition on line 1091 was never true

1092 if filename is None: 

1093 filename = getattr(stream, "name", "?") 

1094 code = compile(stream.read(), filename=filename, mode="exec") 

1095 else: 

1096 code = stream 

1097 self.loadFromString(code, root=root, filename=filename) 

1098 

1099 def loadFromString(self, code, root="config", filename=None): 

1100 """Modify this Config in place by executing the Python code in the 

1101 provided string. 

1102 

1103 Parameters 

1104 ---------- 

1105 code : `str`, `bytes`, or compiled string 

1106 Stream containing configuration override code. 

1107 root : `str`, optional 

1108 Name of the variable in file that refers to the config being 

1109 overridden. 

1110 

1111 For example, the value of root is ``"config"`` and the file 

1112 contains:: 

1113 

1114 config.myField = 5 

1115 

1116 Then this config's field ``myField`` is set to ``5``. 

1117 

1118 **Deprecated:** For backwards compatibility, older config files 

1119 that use ``root="root"`` instead of ``root="config"`` will be 

1120 loaded with a warning printed to `sys.stderr`. This feature will be 

1121 removed at some point. 

1122 filename : `str`, optional 

1123 Name of the configuration file, or `None` if unknown or contained 

1124 in the stream. Used for error reporting. 

1125 

1126 See also 

1127 -------- 

1128 lsst.pex.config.Config.load 

1129 lsst.pex.config.Config.loadFromStream 

1130 lsst.pex.config.Config.save 

1131 lsst.pex.config.Config.saveToStream 

1132 lsst.pex.config.Config.saveToString 

1133 """ 

1134 if filename is None: 1134 ↛ 1138line 1134 didn't jump to line 1138, because the condition on line 1134 was never false

1135 # try to determine the file name; a compiled string 

1136 # has attribute "co_filename", 

1137 filename = getattr(code, "co_filename", "?") 

1138 with RecordingImporter() as importer: 

1139 globals = {"__file__": filename} 

1140 try: 

1141 local = {root: self} 

1142 exec(code, globals, local) 

1143 except NameError as e: 

1144 if root == "config" and "root" in e.args[0]: 

1145 print(f"Config override file {filename!r}" 

1146 " appears to use 'root' instead of 'config'; trying with 'root'", file=sys.stderr) 

1147 local = {"root": self} 

1148 exec(code, globals, local) 

1149 else: 

1150 raise 

1151 

1152 self._imports.update(importer.getModules()) 

1153 

1154 def save(self, filename, root="config"): 

1155 """Save a Python script to the named file, which, when loaded, 

1156 reproduces this config. 

1157 

1158 Parameters 

1159 ---------- 

1160 filename : `str` 

1161 Desination filename of this configuration. 

1162 root : `str`, optional 

1163 Name to use for the root config variable. The same value must be 

1164 used when loading (see `lsst.pex.config.Config.load`). 

1165 

1166 See also 

1167 -------- 

1168 lsst.pex.config.Config.saveToStream 

1169 lsst.pex.config.Config.saveToString 

1170 lsst.pex.config.Config.load 

1171 lsst.pex.config.Config.loadFromStream 

1172 lsst.pex.config.Config.loadFromString 

1173 """ 

1174 d = os.path.dirname(filename) 

1175 with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=d) as outfile: 

1176 self.saveToStream(outfile, root) 

1177 # tempfile is hardcoded to create files with mode '0600' 

1178 # for an explantion of these antics see: 

1179 # https://stackoverflow.com/questions/10291131/how-to-use-os-umask-in-python 

1180 umask = os.umask(0o077) 

1181 os.umask(umask) 

1182 os.chmod(outfile.name, (~umask & 0o666)) 

1183 # chmod before the move so we get quasi-atomic behavior if the 

1184 # source and dest. are on the same filesystem. 

1185 # os.rename may not work across filesystems 

1186 shutil.move(outfile.name, filename) 

1187 

1188 def saveToString(self, skipImports=False): 

1189 """Return the Python script form of this configuration as an executable 

1190 string. 

1191 

1192 Parameters 

1193 ---------- 

1194 skipImports : `bool`, optional 

1195 If `True` then do not include ``import`` statements in output, 

1196 this is to support human-oriented output from ``pipetask`` where 

1197 additional clutter is not useful. 

1198 

1199 Returns 

1200 ------- 

1201 code : `str` 

1202 A code string readable by `loadFromString`. 

1203 

1204 See also 

1205 -------- 

1206 lsst.pex.config.Config.save 

1207 lsst.pex.config.Config.saveToStream 

1208 lsst.pex.config.Config.load 

1209 lsst.pex.config.Config.loadFromStream 

1210 lsst.pex.config.Config.loadFromString 

1211 """ 

1212 buffer = io.StringIO() 

1213 self.saveToStream(buffer, skipImports=skipImports) 

1214 return buffer.getvalue() 

1215 

1216 def saveToStream(self, outfile, root="config", skipImports=False): 

1217 """Save a configuration file to a stream, which, when loaded, 

1218 reproduces this config. 

1219 

1220 Parameters 

1221 ---------- 

1222 outfile : file-like object 

1223 Destination file object write the config into. Accepts strings not 

1224 bytes. 

1225 root 

1226 Name to use for the root config variable. The same value must be 

1227 used when loading (see `lsst.pex.config.Config.load`). 

1228 skipImports : `bool`, optional 

1229 If `True` then do not include ``import`` statements in output, 

1230 this is to support human-oriented output from ``pipetask`` where 

1231 additional clutter is not useful. 

1232 

1233 See also 

1234 -------- 

1235 lsst.pex.config.Config.save 

1236 lsst.pex.config.Config.saveToString 

1237 lsst.pex.config.Config.load 

1238 lsst.pex.config.Config.loadFromStream 

1239 lsst.pex.config.Config.loadFromString 

1240 """ 

1241 tmp = self._name 

1242 self._rename(root) 

1243 try: 

1244 if not skipImports: 1244 ↛ 1256line 1244 didn't jump to line 1256, because the condition on line 1244 was never false

1245 self._collectImports() 

1246 # Remove self from the set, as it is handled explicitly below 

1247 self._imports.remove(self.__module__) 

1248 configType = type(self) 

1249 typeString = _typeStr(configType) 

1250 outfile.write(f"import {configType.__module__}\n") 

1251 outfile.write(f"assert type({root})=={typeString}, 'config is of type %s.%s instead of " 

1252 f"{typeString}' % (type({root}).__module__, type({root}).__name__)\n") 

1253 for imp in self._imports: 1253 ↛ 1254line 1253 didn't jump to line 1254, because the loop on line 1253 never started

1254 if imp in sys.modules and sys.modules[imp] is not None: 

1255 outfile.write(u"import {}\n".format(imp)) 

1256 self._save(outfile) 

1257 finally: 

1258 self._rename(tmp) 

1259 

1260 def freeze(self): 

1261 """Make this config, and all subconfigs, read-only. 

1262 """ 

1263 self._frozen = True 

1264 for field in self._fields.values(): 

1265 field.freeze(self) 

1266 

1267 def _save(self, outfile): 

1268 """Save this config to an open stream object. 

1269 

1270 Parameters 

1271 ---------- 

1272 outfile : file-like object 

1273 Destination file object write the config into. Accepts strings not 

1274 bytes. 

1275 """ 

1276 for field in self._fields.values(): 

1277 field.save(outfile, self) 

1278 

1279 def _collectImports(self): 

1280 """Adds module containing self to the list of things to import and 

1281 then loops over all the fields in the config calling a corresponding 

1282 collect method. The field method will call _collectImports on any 

1283 configs it may own and return the set of things to import. This 

1284 returned set will be merged with the set of imports for this config 

1285 class. 

1286 """ 

1287 self._imports.add(self.__module__) 

1288 for name, field in self._fields.items(): 

1289 field._collectImports(self, self._imports) 

1290 

1291 def toDict(self): 

1292 """Make a dictionary of field names and their values. 

1293 

1294 Returns 

1295 ------- 

1296 dict_ : `dict` 

1297 Dictionary with keys that are `~lsst.pex.config.Field` names. 

1298 Values are `~lsst.pex.config.Field` values. 

1299 

1300 See also 

1301 -------- 

1302 lsst.pex.config.Field.toDict 

1303 

1304 Notes 

1305 ----- 

1306 This method uses the `~lsst.pex.config.Field.toDict` method of 

1307 individual fields. Subclasses of `~lsst.pex.config.Field` may need to 

1308 implement a ``toDict`` method for *this* method to work. 

1309 """ 

1310 dict_ = {} 

1311 for name, field in self._fields.items(): 

1312 dict_[name] = field.toDict(self) 

1313 return dict_ 

1314 

1315 def names(self): 

1316 """Get all the field names in the config, recursively. 

1317 

1318 Returns 

1319 ------- 

1320 names : `list` of `str` 

1321 Field names. 

1322 """ 

1323 # 

1324 # Rather than sort out the recursion all over again use the 

1325 # pre-existing saveToStream() 

1326 # 

1327 with io.StringIO() as strFd: 

1328 self.saveToStream(strFd, "config") 

1329 contents = strFd.getvalue() 

1330 strFd.close() 

1331 # 

1332 # Pull the names out of the dumped config 

1333 # 

1334 keys = [] 

1335 for line in contents.split("\n"): 

1336 if re.search(r"^((assert|import)\s+|\s*$|#)", line): 

1337 continue 

1338 

1339 mat = re.search(r"^(?:config\.)?([^=]+)\s*=\s*.*", line) 

1340 if mat: 

1341 keys.append(mat.group(1)) 

1342 

1343 return keys 

1344 

1345 def _rename(self, name): 

1346 """Rename this config object in its parent `~lsst.pex.config.Config`. 

1347 

1348 Parameters 

1349 ---------- 

1350 name : `str` 

1351 New name for this config in its parent `~lsst.pex.config.Config`. 

1352 

1353 Notes 

1354 ----- 

1355 This method uses the `~lsst.pex.config.Field.rename` method of 

1356 individual `lsst.pex.config.Field` instances. 

1357 `lsst.pex.config.Field` subclasses may need to implement a ``rename`` 

1358 method for *this* method to work. 

1359 

1360 See also 

1361 -------- 

1362 lsst.pex.config.Field.rename 

1363 """ 

1364 self._name = name 

1365 for field in self._fields.values(): 

1366 field.rename(self) 

1367 

1368 def validate(self): 

1369 """Validate the Config, raising an exception if invalid. 

1370 

1371 Raises 

1372 ------ 

1373 lsst.pex.config.FieldValidationError 

1374 Raised if verification fails. 

1375 

1376 Notes 

1377 ----- 

1378 The base class implementation performs type checks on all fields by 

1379 calling their `~lsst.pex.config.Field.validate` methods. 

1380 

1381 Complex single-field validation can be defined by deriving new Field 

1382 types. For convenience, some derived `lsst.pex.config.Field`-types 

1383 (`~lsst.pex.config.ConfigField` and 

1384 `~lsst.pex.config.ConfigChoiceField`) are defined in `lsst.pex.config` 

1385 that handle recursing into subconfigs. 

1386 

1387 Inter-field relationships should only be checked in derived 

1388 `~lsst.pex.config.Config` classes after calling this method, and base 

1389 validation is complete. 

1390 """ 

1391 for field in self._fields.values(): 

1392 field.validate(self) 

1393 

1394 def formatHistory(self, name, **kwargs): 

1395 """Format a configuration field's history to a human-readable format. 

1396 

1397 Parameters 

1398 ---------- 

1399 name : `str` 

1400 Name of a `~lsst.pex.config.Field` in this config. 

1401 kwargs 

1402 Keyword arguments passed to `lsst.pex.config.history.format`. 

1403 

1404 Returns 

1405 ------- 

1406 history : `str` 

1407 A string containing the formatted history. 

1408 

1409 See also 

1410 -------- 

1411 lsst.pex.config.history.format 

1412 """ 

1413 import lsst.pex.config.history as pexHist 

1414 return pexHist.format(self, name, **kwargs) 

1415 

1416 history = property(lambda x: x._history) 1416 ↛ exitline 1416 didn't run the lambda on line 1416

1417 """Read-only history. 

1418 """ 

1419 

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

1421 """Set an attribute (such as a field's value). 

1422 

1423 Notes 

1424 ----- 

1425 Unlike normal Python objects, `~lsst.pex.config.Config` objects are 

1426 locked such that no additional attributes nor properties may be added 

1427 to them dynamically. 

1428 

1429 Although this is not the standard Python behavior, it helps to protect 

1430 users from accidentally mispelling a field name, or trying to set a 

1431 non-existent field. 

1432 """ 

1433 if attr in self._fields: 

1434 if self._fields[attr].deprecated is not None: 1434 ↛ 1435line 1434 didn't jump to line 1435, because the condition on line 1434 was never true

1435 fullname = _joinNamePath(self._name, self._fields[attr].name) 

1436 warnings.warn(f"Config field {fullname} is deprecated: {self._fields[attr].deprecated}", 

1437 FutureWarning, stacklevel=2) 

1438 if at is None: 1438 ↛ 1441line 1438 didn't jump to line 1441, because the condition on line 1438 was never false

1439 at = getCallStack() 

1440 # This allows Field descriptors to work. 

1441 self._fields[attr].__set__(self, value, at=at, label=label) 

1442 elif hasattr(getattr(self.__class__, attr, None), '__set__'): 1442 ↛ 1444line 1442 didn't jump to line 1444, because the condition on line 1442 was never true

1443 # This allows properties and other non-Field descriptors to work. 

1444 return object.__setattr__(self, attr, value) 

1445 elif attr in self.__dict__ or attr in ("_name", "_history", "_storage", "_frozen", "_imports"): 1445 ↛ 1450line 1445 didn't jump to line 1450, because the condition on line 1445 was never false

1446 # This allows specific private attributes to work. 

1447 self.__dict__[attr] = value 

1448 else: 

1449 # We throw everything else. 

1450 raise AttributeError("%s has no attribute %s" % (_typeStr(self), attr)) 

1451 

1452 def __delattr__(self, attr, at=None, label="deletion"): 

1453 if attr in self._fields: 

1454 if at is None: 

1455 at = getCallStack() 

1456 self._fields[attr].__delete__(self, at=at, label=label) 

1457 else: 

1458 object.__delattr__(self, attr) 

1459 

1460 def __eq__(self, other): 

1461 if type(other) == type(self): 1461 ↛ 1462line 1461 didn't jump to line 1462, because the condition on line 1461 was never true

1462 for name in self._fields: 

1463 thisValue = getattr(self, name) 

1464 otherValue = getattr(other, name) 

1465 if isinstance(thisValue, float) and math.isnan(thisValue): 

1466 if not math.isnan(otherValue): 

1467 return False 

1468 elif thisValue != otherValue: 

1469 return False 

1470 return True 

1471 return False 

1472 

1473 def __ne__(self, other): 

1474 return not self.__eq__(other) 

1475 

1476 def __str__(self): 

1477 return str(self.toDict()) 

1478 

1479 def __repr__(self): 

1480 return "%s(%s)" % ( 

1481 _typeStr(self), 

1482 ", ".join("%s=%r" % (k, v) for k, v in self.toDict().items() if v is not None) 

1483 ) 

1484 

1485 def compare(self, other, shortcut=True, rtol=1E-8, atol=1E-8, output=None): 

1486 """Compare this configuration to another `~lsst.pex.config.Config` for 

1487 equality. 

1488 

1489 Parameters 

1490 ---------- 

1491 other : `lsst.pex.config.Config` 

1492 Other `~lsst.pex.config.Config` object to compare against this 

1493 config. 

1494 shortcut : `bool`, optional 

1495 If `True`, return as soon as an inequality is found. Default is 

1496 `True`. 

1497 rtol : `float`, optional 

1498 Relative tolerance for floating point comparisons. 

1499 atol : `float`, optional 

1500 Absolute tolerance for floating point comparisons. 

1501 output : callable, optional 

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

1503 report inequalities. 

1504 

1505 Returns 

1506 ------- 

1507 isEqual : `bool` 

1508 `True` when the two `lsst.pex.config.Config` instances are equal. 

1509 `False` if there is an inequality. 

1510 

1511 See also 

1512 -------- 

1513 lsst.pex.config.compareConfigs 

1514 

1515 Notes 

1516 ----- 

1517 Unselected targets of `~lsst.pex.config.RegistryField` fields and 

1518 unselected choices of `~lsst.pex.config.ConfigChoiceField` fields 

1519 are not considered by this method. 

1520 

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

1522 """ 

1523 name1 = self._name if self._name is not None else "config" 

1524 name2 = other._name if other._name is not None else "config" 

1525 name = getComparisonName(name1, name2) 

1526 return compareConfigs(name, self, other, shortcut=shortcut, 

1527 rtol=rtol, atol=atol, output=output) 

1528 

1529 @classmethod 

1530 def __init_subclass__(cls, **kwargs): 

1531 """Run initialization for every subclass. 

1532 

1533 Specifically registers the subclass with a YAML representer 

1534 and YAML constructor (if pyyaml is available) 

1535 """ 

1536 super().__init_subclass__(**kwargs) 

1537 

1538 if not yaml: 1538 ↛ 1539line 1538 didn't jump to line 1539, because the condition on line 1538 was never true

1539 return 

1540 

1541 yaml.add_representer(cls, _yaml_config_representer) 

1542 

1543 @classmethod 

1544 def _fromPython(cls, config_py): 

1545 """Instantiate a `Config`-subclass from serialized Python form. 

1546 

1547 Parameters 

1548 ---------- 

1549 config_py : `str` 

1550 A serialized form of the Config as created by 

1551 `Config.saveToStream`. 

1552 

1553 Returns 

1554 ------- 

1555 config : `Config` 

1556 Reconstructed `Config` instant. 

1557 """ 

1558 cls = _classFromPython(config_py) 

1559 return unreduceConfig(cls, config_py) 

1560 

1561 

1562def _classFromPython(config_py): 

1563 """Return the Config subclass required by this Config serialization. 

1564 

1565 Parameters 

1566 ---------- 

1567 config_py : `str` 

1568 A serialized form of the Config as created by 

1569 `Config.saveToStream`. 

1570 

1571 Returns 

1572 ------- 

1573 cls : `type` 

1574 The `Config` subclass associated with this config. 

1575 """ 

1576 # standard serialization has the form: 

1577 # import config.class 

1578 # assert type(config)==config.class.Config, ... 

1579 # We want to parse these two lines so we can get the class itself 

1580 

1581 # Do a single regex to avoid large string copies when splitting a 

1582 # large config into separate lines. 

1583 matches = re.search(r"^import ([\w.]+)\nassert .*==(.*?),", config_py) 

1584 

1585 if not matches: 

1586 first_line, second_line, _ = config_py.split("\n", 2) 

1587 raise ValueError("First two lines did not match expected form. Got:\n" 

1588 f" - {first_line}\n" 

1589 f" - {second_line}") 

1590 

1591 module_name = matches.group(1) 

1592 module = importlib.import_module(module_name) 

1593 

1594 # Second line 

1595 full_name = matches.group(2) 

1596 

1597 # Remove the module name from the full name 

1598 if not full_name.startswith(module_name): 

1599 raise ValueError(f"Module name ({module_name}) inconsistent with full name ({full_name})") 

1600 

1601 # if module name is a.b.c and full name is a.b.c.d.E then 

1602 # we need to remove a.b.c. and iterate over the remainder 

1603 # The +1 is for the extra dot after a.b.c 

1604 remainder = full_name[len(module_name)+1:] 

1605 components = remainder.split(".") 

1606 pytype = module 

1607 for component in components: 

1608 pytype = getattr(pytype, component) 

1609 return pytype 

1610 

1611 

1612def unreduceConfig(cls, stream): 

1613 """Create a `~lsst.pex.config.Config` from a stream. 

1614 

1615 Parameters 

1616 ---------- 

1617 cls : `lsst.pex.config.Config`-type 

1618 A `lsst.pex.config.Config` type (not an instance) that is instantiated 

1619 with configurations in the ``stream``. 

1620 stream : file-like object, `str`, or compiled string 

1621 Stream containing configuration override code. 

1622 

1623 Returns 

1624 ------- 

1625 config : `lsst.pex.config.Config` 

1626 Config instance. 

1627 

1628 See also 

1629 -------- 

1630 lsst.pex.config.Config.loadFromStream 

1631 """ 

1632 config = cls() 

1633 config.loadFromStream(stream) 

1634 return config