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

422 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-06-02 03:31 -0700

1# This file is part of pex_config. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28__all__ = ("Config", "ConfigMeta", "Field", "FieldValidationError", "UnexpectedProxyUsageError") 

29 

30import copy 

31import importlib 

32import io 

33import math 

34import os 

35import re 

36import shutil 

37import sys 

38import tempfile 

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 .callStack import getCallStack, getStackFrame 

51from .comparison import compareConfigs, compareScalars, getComparisonName 

52 

53if yaml: 53 ↛ 65line 53 didn't jump to line 65, 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 

60 YamlLoaders += (CLoader,) 

61 except ImportError: 

62 pass 

63 

64 

65class UnexpectedProxyUsageError(TypeError): 

66 """Exception raised when a proxy class is used in a context that suggests 

67 it should have already been converted to the thing it proxies. 

68 """ 

69 

70 

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

72 """Generate nested configuration names.""" 

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

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

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

76 name = prefix 

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

78 name = prefix + "." + name 

79 

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

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

82 else: 

83 return name 

84 

85 

86def _autocast(x, dtype): 

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

88 

89 Parameters 

90 ---------- 

91 x : object 

92 A value. 

93 dtype : tpye 

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

95 

96 Returns 

97 ------- 

98 values : object 

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

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

101 ``x`` is returned. 

102 """ 

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

104 return float(x) 

105 return x 

106 

107 

108def _typeStr(x): 

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

110 

111 Returns 

112 ------- 

113 `str` 

114 Fully-qualified type name. 

115 

116 Notes 

117 ----- 

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

119 later upon with the 'load' function. 

120 """ 

121 if hasattr(x, "__module__") and hasattr(x, "__name__"): 

122 xtype = x 

123 else: 

124 xtype = type(x) 

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

126 return xtype.__name__ 

127 else: 

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

129 

130 

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

132 

133 def _yaml_config_representer(dumper, data): 

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

135 

136 Stores the serialized stream as a scalar block string. 

137 """ 

138 stream = io.StringIO() 

139 data.saveToStream(stream) 

140 config_py = stream.getvalue() 

141 

142 # Strip multiple newlines from the end of the config 

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

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

145 

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

147 # Remove the trailing spaces so it has no choice 

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

149 

150 # Store the Python as a simple scalar 

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

152 

153 def _yaml_config_constructor(loader, node): 

154 """Construct a config from YAML""" 

155 config_py = loader.construct_scalar(node) 

156 return Config._fromPython(config_py) 

157 

158 # Register a generic constructor for Config and all subclasses 

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

160 for loader in YamlLoaders: 

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

162 

163 

164class ConfigMeta(type): 

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

166 

167 Notes 

168 ----- 

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

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

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

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

173 """ 

174 

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

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

177 cls._fields = {} 

178 cls._source = getStackFrame() 

179 

180 def getFields(classtype): 

181 fields = {} 

182 bases = list(classtype.__bases__) 

183 bases.reverse() 

184 for b in bases: 

185 fields.update(getFields(b)) 

186 

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

188 if isinstance(v, Field): 

189 fields[k] = v 

190 return fields 

191 

192 fields = getFields(cls) 

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

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

195 

196 def __setattr__(cls, name, value): 

197 if isinstance(value, Field): 

198 value.name = name 

199 cls._fields[name] = value 

200 type.__setattr__(cls, name, value) 

201 

202 

203class FieldValidationError(ValueError): 

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

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

206 

207 Parameters 

208 ---------- 

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

210 The field that was not valid. 

211 config : `lsst.pex.config.Config` 

212 The config containing the invalid field. 

213 msg : `str` 

214 Text describing why the field was not valid. 

215 """ 

216 

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

218 self.fieldType = type(field) 

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

220 """ 

221 

222 self.fieldName = field.name 

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

224 error (`str`). 

225 

226 See also 

227 -------- 

228 lsst.pex.config.Field.name 

229 """ 

230 

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

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

233 (`str`). 

234 """ 

235 

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

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

238 instance. 

239 """ 

240 

241 self.fieldSource = field.source 

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

243 """ 

244 

245 self.configSource = config._source 

246 error = ( 

247 "%s '%s' failed validation: %s\n" 

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

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

250 % ( 

251 self.fieldType.__name__, 

252 self.fullname, 

253 msg, 

254 self.fieldSource.format(), 

255 self.configSource.format(), 

256 ) 

257 ) 

258 super().__init__(error) 

259 

260 

261class Field: 

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

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

264 

265 Parameters 

266 ---------- 

267 doc : `str` 

268 A description of the field for users. 

269 dtype : type 

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

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

272 `Field.supportedTypes`. 

273 default : object, optional 

274 The field's default value. 

275 check : callable, optional 

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

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

278 validation can be written as part of the 

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

280 optional : `bool`, optional 

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

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

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

284 deprecated : None or `str`, optional 

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

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

287 

288 Raises 

289 ------ 

290 ValueError 

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

292 (see `Field.supportedTypes`). 

293 

294 See also 

295 -------- 

296 ChoiceField 

297 ConfigChoiceField 

298 ConfigDictField 

299 ConfigField 

300 ConfigurableField 

301 DictField 

302 ListField 

303 RangeField 

304 RegistryField 

305 

306 Notes 

307 ----- 

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

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

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

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

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

313 `Field` attributes are `descriptors 

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

315 

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

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

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

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

320 type. See the example, below. 

321 

322 Examples 

323 -------- 

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

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

326 

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

328 >>> class Example(Config): 

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

330 ... 

331 >>> print(config.myInt) 

332 0 

333 >>> config.myInt = 5 

334 >>> print(config.myInt) 

335 5 

336 """ 

337 

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

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

340 """ 

341 

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

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

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

345 

346 source = getStackFrame() 

347 self._setup( 

348 doc=doc, 

349 dtype=dtype, 

350 default=default, 

351 check=check, 

352 optional=optional, 

353 source=source, 

354 deprecated=deprecated, 

355 ) 

356 

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

358 """Set attributes, usually during initialization.""" 

359 self.dtype = dtype 

360 """Data type for the field. 

361 """ 

362 

363 # append the deprecation message to the docstring. 

364 if deprecated is not None: 

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

366 self.doc = doc 

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

368 """ 

369 

370 self.deprecated = deprecated 

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

372 """ 

373 

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

375 if optional or default is not None: 

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

377 self.__doc__ += ")" 

378 

379 self.default = default 

380 """Default value for this field. 

381 """ 

382 

383 self.check = check 

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

385 """ 

386 

387 self.optional = optional 

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

389 

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

391 field's value is `None`. 

392 """ 

393 

394 self.source = source 

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

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

397 """ 

398 

399 def rename(self, instance): 

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

401 only). 

402 

403 Parameters 

404 ---------- 

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

406 The config instance that contains this field. 

407 

408 Notes 

409 ----- 

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

411 contains this field and should not be called directly. 

412 

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

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

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

416 `lsst.pex.config.config._joinNamePath`. 

417 """ 

418 pass 

419 

420 def validate(self, instance): 

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

422 

423 Parameters 

424 ---------- 

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

426 The config instance that contains this field. 

427 

428 Raises 

429 ------ 

430 lsst.pex.config.FieldValidationError 

431 Raised if verification fails. 

432 

433 Notes 

434 ----- 

435 This method provides basic validation: 

436 

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

438 - Ensures type correctness. 

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

440 

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

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

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

444 """ 

445 value = self.__get__(instance) 

446 if not self.optional and value is None: 

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

448 

449 def freeze(self, instance): 

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

451 

452 Parameters 

453 ---------- 

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

455 The config instance that contains this field. 

456 

457 Notes 

458 ----- 

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

460 hold subconfigs should freeze each subconfig. 

461 

462 **Subclasses should implement this method.** 

463 """ 

464 pass 

465 

466 def _validateValue(self, value): 

467 """Validate a value. 

468 

469 Parameters 

470 ---------- 

471 value : object 

472 The value being validated. 

473 

474 Raises 

475 ------ 

476 TypeError 

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

478 ``dtype``. 

479 ValueError 

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

481 """ 

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

483 return 

484 

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

486 msg = "Value %s is of incorrect type %s. Expected type %s" % ( 

487 value, 

488 _typeStr(value), 

489 _typeStr(self.dtype), 

490 ) 

491 raise TypeError(msg) 

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

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

494 raise ValueError(msg) 

495 

496 def _collectImports(self, instance, imports): 

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

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

499 set. 

500 

501 Parameters 

502 ---------- 

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

504 A config object that has this field defined on it 

505 imports : `set` 

506 Set of python modules that need imported after persistence 

507 """ 

508 pass 

509 

510 def save(self, outfile, instance): 

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

512 

513 Parameters 

514 ---------- 

515 outfile : file-like object 

516 A writeable field handle. 

517 instance : `Config` 

518 The `Config` instance that contains this field. 

519 

520 Notes 

521 ----- 

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

523 contains this field and should not be called directly. 

524 

525 The output consists of the documentation string 

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

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

528 

529 This output can be executed with Python. 

530 """ 

531 value = self.__get__(instance) 

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

533 

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

535 return 

536 

537 # write full documentation string as comment lines 

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

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

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

541 # non-finite numbers need special care 

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

543 else: 

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

545 

546 def toDict(self, instance): 

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

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

549 

550 Parameters 

551 ---------- 

552 instance : `Config` 

553 The `Config` that contains this field. 

554 

555 Returns 

556 ------- 

557 value : object 

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

559 

560 Notes 

561 ----- 

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

563 should not be called directly. 

564 

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

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

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

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

569 the field values in the subconfig. 

570 """ 

571 return self.__get__(instance) 

572 

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

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

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

576 directly 

577 

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

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

580 Config classes. 

581 

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

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

584 returned. 

585 """ 

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

587 return self 

588 else: 

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

590 try: 

591 return instance._storage[self.name] 

592 except AttributeError: 

593 if not isinstance(instance, Config): 

594 return self 

595 else: 

596 raise AttributeError( 

597 f"Config {instance} is missing " 

598 "_storage attribute, likely" 

599 " incorrectly initialized" 

600 ) 

601 

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

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

604 

605 Parameters 

606 ---------- 

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

608 The config instance that contains this field. 

609 value : obj 

610 Value to set on this field. 

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

612 The call stack (created by 

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

614 label : `str`, optional 

615 Event label for the history. 

616 

617 Notes 

618 ----- 

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

620 and should not be called directly. 

621 

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

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

624 should follow the following rules: 

625 

626 - Do not allow modification of frozen configs. 

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

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

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

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

631 invalid values. 

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

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

634 

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

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

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

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

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

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

641 reimplementation. 

642 """ 

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

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

645 

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

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

648 value = _autocast(value, self.dtype) 

649 try: 

650 self._validateValue(value) 

651 except BaseException as e: 

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

653 

654 instance._storage[self.name] = value 

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

656 at = getCallStack() 

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

658 

659 def __delete__(self, instance, at=None, label="deletion"): 

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

661 

662 Parameters 

663 ---------- 

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

665 The config instance that contains this field. 

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

667 The call stack (created by 

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

669 label : `str`, optional 

670 Event label for the history. 

671 

672 Notes 

673 ----- 

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

675 should not be called directly. 

676 """ 

677 if at is None: 

678 at = getCallStack() 

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

680 

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

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

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

684 

685 Parameters 

686 ---------- 

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

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

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

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

691 shortcut : `bool`, optional 

692 **Unused.** 

693 rtol : `float`, optional 

694 Relative tolerance for floating point comparisons. 

695 atol : `float`, optional 

696 Absolute tolerance for floating point comparisons. 

697 output : callable, optional 

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

699 report inequalities. 

700 

701 Notes 

702 ----- 

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

704 

705 See also 

706 -------- 

707 lsst.pex.config.compareScalars 

708 """ 

709 v1 = getattr(instance1, self.name) 

710 v2 = getattr(instance2, self.name) 

711 name = getComparisonName( 

712 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name) 

713 ) 

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

715 

716 

717class RecordingImporter: 

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

719 imported. 

720 

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

722 

723 Examples 

724 -------- 

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

726 when done: 

727 

728 >>> with RecordingImporter() as importer: 

729 ... # import stuff 

730 ... import numpy as np 

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

732 """ 

733 

734 def __init__(self): 

735 self._modules = set() 

736 

737 def __enter__(self): 

738 self.origMetaPath = sys.meta_path 

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

740 return self 

741 

742 def __exit__(self, *args): 

743 self.uninstall() 

744 return False # Don't suppress exceptions 

745 

746 def uninstall(self): 

747 """Uninstall the importer.""" 

748 sys.meta_path = self.origMetaPath 

749 

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

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

752 self._modules.add(fullname) 

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

754 return None 

755 

756 def getModules(self): 

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

758 

759 Returns 

760 ------- 

761 modules : `set` of `str` 

762 Set of imported module names. 

763 """ 

764 return self._modules 

765 

766 

767class Config(metaclass=ConfigMeta): 

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

769 

770 Notes 

771 ----- 

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

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

774 class behavior. 

775 

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

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

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

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

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

781 configuration instance. 

782 

783 Examples 

784 -------- 

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

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

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

788 

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

790 >>> class DemoConfig(Config): 

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

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

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

794 ... 

795 >>> config = DemoConfig() 

796 

797 Configs support many `dict`-like APIs: 

798 

799 >>> config.keys() 

800 ['intField', 'listField'] 

801 >>> 'intField' in config 

802 True 

803 

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

805 

806 >>> config.intField 

807 42 

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

809 >>> print(config.listField) 

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

811 """ 

812 

813 def __iter__(self): 

814 """Iterate over fields.""" 

815 return self._fields.__iter__() 

816 

817 def keys(self): 

818 """Get field names. 

819 

820 Returns 

821 ------- 

822 names : `list` 

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

824 

825 See also 

826 -------- 

827 lsst.pex.config.Config.iterkeys 

828 """ 

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

830 

831 def values(self): 

832 """Get field values. 

833 

834 Returns 

835 ------- 

836 values : `list` 

837 List of field values. 

838 

839 See also 

840 -------- 

841 lsst.pex.config.Config.itervalues 

842 """ 

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

844 

845 def items(self): 

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

847 

848 Returns 

849 ------- 

850 items : `list` 

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

852 

853 0. Field name. 

854 1. Field value. 

855 

856 See also 

857 -------- 

858 lsst.pex.config.Config.iteritems 

859 """ 

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

861 

862 def iteritems(self): 

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

864 

865 Yields 

866 ------ 

867 item : `tuple` 

868 Tuple items are: 

869 

870 0. Field name. 

871 1. Field value. 

872 

873 See also 

874 -------- 

875 lsst.pex.config.Config.items 

876 """ 

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

878 

879 def itervalues(self): 

880 """Iterate over field values. 

881 

882 Yields 

883 ------ 

884 value : obj 

885 A field value. 

886 

887 See also 

888 -------- 

889 lsst.pex.config.Config.values 

890 """ 

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

892 

893 def iterkeys(self): 

894 """Iterate over field names 

895 

896 Yields 

897 ------ 

898 key : `str` 

899 A field's key (attribute name). 

900 

901 See also 

902 -------- 

903 lsst.pex.config.Config.values 

904 """ 

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

906 

907 def __contains__(self, name): 

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

909 

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

911 """ 

912 return self._storage.__contains__(name) 

913 

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

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

916 

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

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

919 some attributes are handled at allocation time rather than at 

920 initialization. 

921 

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

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

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

925 """ 

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

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

928 # remove __label and ignore it 

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

930 

931 instance = object.__new__(cls) 

932 instance._frozen = False 

933 instance._name = name 

934 instance._storage = {} 

935 instance._history = {} 

936 instance._imports = set() 

937 # load up defaults 

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

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

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

941 # set custom default-overides 

942 instance.setDefaults() 

943 # set constructor overides 

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

945 return instance 

946 

947 def __reduce__(self): 

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

949 

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

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

952 be pickled. 

953 """ 

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

955 # requires bytes 

956 stream = io.StringIO() 

957 self.saveToStream(stream) 

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

959 

960 def setDefaults(self): 

961 """Subclass hook for computing defaults. 

962 

963 Notes 

964 ----- 

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

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

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

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

969 ``setDefaults``. 

970 """ 

971 pass 

972 

973 def update(self, **kw): 

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

975 

976 Parameters 

977 ---------- 

978 kw 

979 Keywords are configuration field names. Values are configuration 

980 field values. 

981 

982 Notes 

983 ----- 

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

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

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

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

988 

989 Examples 

990 -------- 

991 This is a config with three fields: 

992 

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

994 >>> class DemoConfig(Config): 

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

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

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

998 ... 

999 >>> config = DemoConfig() 

1000 

1001 These are the default values of each field: 

1002 

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

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

1005 ... 

1006 fieldA: 42 

1007 fieldB: True 

1008 fieldC: 'Hello world' 

1009 

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

1011 

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

1013 

1014 Now the values of each field are: 

1015 

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

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

1018 ... 

1019 fieldA: 13 

1020 fieldB: True 

1021 fieldC: 'Updated!' 

1022 """ 

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

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

1025 

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

1027 try: 

1028 field = self._fields[name] 

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

1030 except KeyError: 

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

1032 

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

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

1035 configuration file. 

1036 

1037 Parameters 

1038 ---------- 

1039 filename : `str` 

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

1041 module. 

1042 root : `str`, optional 

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

1044 overridden. 

1045 

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

1047 contains:: 

1048 

1049 config.myField = 5 

1050 

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

1052 

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

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

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

1056 removed at some point. 

1057 

1058 See also 

1059 -------- 

1060 lsst.pex.config.Config.loadFromStream 

1061 lsst.pex.config.Config.loadFromString 

1062 lsst.pex.config.Config.save 

1063 lsst.pex.config.Config.saveToStream 

1064 lsst.pex.config.Config.saveToString 

1065 """ 

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

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

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

1069 

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

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

1072 provided stream. 

1073 

1074 Parameters 

1075 ---------- 

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

1077 Stream containing configuration override code. If this is a 

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

1079 root : `str`, optional 

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

1081 overridden. 

1082 

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

1084 contains:: 

1085 

1086 config.myField = 5 

1087 

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

1089 

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

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

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

1093 removed at some point. 

1094 filename : `str`, optional 

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

1096 in the stream. Used for error reporting. 

1097 

1098 Notes 

1099 ----- 

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

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

1102 `loadFromString` instead for most of these types. 

1103 

1104 See also 

1105 -------- 

1106 lsst.pex.config.Config.load 

1107 lsst.pex.config.Config.loadFromString 

1108 lsst.pex.config.Config.save 

1109 lsst.pex.config.Config.saveToStream 

1110 lsst.pex.config.Config.saveToString 

1111 """ 

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

1113 if filename is None: 

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

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

1116 else: 

1117 code = stream 

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

1119 

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

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

1122 provided string. 

1123 

1124 Parameters 

1125 ---------- 

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

1127 Stream containing configuration override code. 

1128 root : `str`, optional 

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

1130 overridden. 

1131 

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

1133 contains:: 

1134 

1135 config.myField = 5 

1136 

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

1138 

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

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

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

1142 removed at some point. 

1143 filename : `str`, optional 

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

1145 in the stream. Used for error reporting. 

1146 

1147 See also 

1148 -------- 

1149 lsst.pex.config.Config.load 

1150 lsst.pex.config.Config.loadFromStream 

1151 lsst.pex.config.Config.save 

1152 lsst.pex.config.Config.saveToStream 

1153 lsst.pex.config.Config.saveToString 

1154 """ 

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

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

1157 # has attribute "co_filename", 

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

1159 with RecordingImporter() as importer: 

1160 globals = {"__file__": filename} 

1161 try: 

1162 local = {root: self} 

1163 exec(code, globals, local) 

1164 except NameError as e: 

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

1166 print( 

1167 f"Config override file {filename!r}" 

1168 " appears to use 'root' instead of 'config'; trying with 'root'", 

1169 file=sys.stderr, 

1170 ) 

1171 local = {"root": self} 

1172 exec(code, globals, local) 

1173 else: 

1174 raise 

1175 

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

1177 

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

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

1180 reproduces this config. 

1181 

1182 Parameters 

1183 ---------- 

1184 filename : `str` 

1185 Desination filename of this configuration. 

1186 root : `str`, optional 

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

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

1189 

1190 See also 

1191 -------- 

1192 lsst.pex.config.Config.saveToStream 

1193 lsst.pex.config.Config.saveToString 

1194 lsst.pex.config.Config.load 

1195 lsst.pex.config.Config.loadFromStream 

1196 lsst.pex.config.Config.loadFromString 

1197 """ 

1198 d = os.path.dirname(filename) 

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

1200 self.saveToStream(outfile, root) 

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

1202 # for an explantion of these antics see: 

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

1204 umask = os.umask(0o077) 

1205 os.umask(umask) 

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

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

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

1209 # os.rename may not work across filesystems 

1210 shutil.move(outfile.name, filename) 

1211 

1212 def saveToString(self, skipImports=False): 

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

1214 string. 

1215 

1216 Parameters 

1217 ---------- 

1218 skipImports : `bool`, optional 

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

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

1221 additional clutter is not useful. 

1222 

1223 Returns 

1224 ------- 

1225 code : `str` 

1226 A code string readable by `loadFromString`. 

1227 

1228 See also 

1229 -------- 

1230 lsst.pex.config.Config.save 

1231 lsst.pex.config.Config.saveToStream 

1232 lsst.pex.config.Config.load 

1233 lsst.pex.config.Config.loadFromStream 

1234 lsst.pex.config.Config.loadFromString 

1235 """ 

1236 buffer = io.StringIO() 

1237 self.saveToStream(buffer, skipImports=skipImports) 

1238 return buffer.getvalue() 

1239 

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

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

1242 reproduces this config. 

1243 

1244 Parameters 

1245 ---------- 

1246 outfile : file-like object 

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

1248 bytes. 

1249 root 

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

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

1252 skipImports : `bool`, optional 

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

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

1255 additional clutter is not useful. 

1256 

1257 See also 

1258 -------- 

1259 lsst.pex.config.Config.save 

1260 lsst.pex.config.Config.saveToString 

1261 lsst.pex.config.Config.load 

1262 lsst.pex.config.Config.loadFromStream 

1263 lsst.pex.config.Config.loadFromString 

1264 """ 

1265 tmp = self._name 

1266 self._rename(root) 

1267 try: 

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

1269 self._collectImports() 

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

1271 self._imports.remove(self.__module__) 

1272 configType = type(self) 

1273 typeString = _typeStr(configType) 

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

1275 outfile.write( 

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

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

1278 ) 

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

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

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

1282 self._save(outfile) 

1283 finally: 

1284 self._rename(tmp) 

1285 

1286 def freeze(self): 

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

1288 self._frozen = True 

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

1290 field.freeze(self) 

1291 

1292 def _save(self, outfile): 

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

1294 

1295 Parameters 

1296 ---------- 

1297 outfile : file-like object 

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

1299 bytes. 

1300 """ 

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

1302 field.save(outfile, self) 

1303 

1304 def _collectImports(self): 

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

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

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

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

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

1310 class. 

1311 """ 

1312 self._imports.add(self.__module__) 

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

1314 field._collectImports(self, self._imports) 

1315 

1316 def toDict(self): 

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

1318 

1319 Returns 

1320 ------- 

1321 dict_ : `dict` 

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

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

1324 

1325 See also 

1326 -------- 

1327 lsst.pex.config.Field.toDict 

1328 

1329 Notes 

1330 ----- 

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

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

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

1334 """ 

1335 dict_ = {} 

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

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

1338 return dict_ 

1339 

1340 def names(self): 

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

1342 

1343 Returns 

1344 ------- 

1345 names : `list` of `str` 

1346 Field names. 

1347 """ 

1348 # 

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

1350 # pre-existing saveToStream() 

1351 # 

1352 with io.StringIO() as strFd: 

1353 self.saveToStream(strFd, "config") 

1354 contents = strFd.getvalue() 

1355 strFd.close() 

1356 # 

1357 # Pull the names out of the dumped config 

1358 # 

1359 keys = [] 

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

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

1362 continue 

1363 

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

1365 if mat: 

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

1367 

1368 return keys 

1369 

1370 def _rename(self, name): 

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

1372 

1373 Parameters 

1374 ---------- 

1375 name : `str` 

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

1377 

1378 Notes 

1379 ----- 

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

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

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

1383 method for *this* method to work. 

1384 

1385 See also 

1386 -------- 

1387 lsst.pex.config.Field.rename 

1388 """ 

1389 self._name = name 

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

1391 field.rename(self) 

1392 

1393 def validate(self): 

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

1395 

1396 Raises 

1397 ------ 

1398 lsst.pex.config.FieldValidationError 

1399 Raised if verification fails. 

1400 

1401 Notes 

1402 ----- 

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

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

1405 

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

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

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

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

1410 that handle recursing into subconfigs. 

1411 

1412 Inter-field relationships should only be checked in derived 

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

1414 validation is complete. 

1415 """ 

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

1417 field.validate(self) 

1418 

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

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

1421 

1422 Parameters 

1423 ---------- 

1424 name : `str` 

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

1426 kwargs 

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

1428 

1429 Returns 

1430 ------- 

1431 history : `str` 

1432 A string containing the formatted history. 

1433 

1434 See also 

1435 -------- 

1436 lsst.pex.config.history.format 

1437 """ 

1438 import lsst.pex.config.history as pexHist 

1439 

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

1441 

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

1443 """Read-only history. 

1444 """ 

1445 

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

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

1448 

1449 Notes 

1450 ----- 

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

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

1453 to them dynamically. 

1454 

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

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

1457 non-existent field. 

1458 """ 

1459 if attr in self._fields: 

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

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

1462 warnings.warn( 

1463 f"Config field {fullname} is deprecated: {self._fields[attr].deprecated}", 

1464 FutureWarning, 

1465 stacklevel=2, 

1466 ) 

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

1468 at = getCallStack() 

1469 # This allows Field descriptors to work. 

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

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

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

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

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

1475 # This allows specific private attributes to work. 

1476 self.__dict__[attr] = value 

1477 else: 

1478 # We throw everything else. 

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

1480 

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

1482 if attr in self._fields: 

1483 if at is None: 

1484 at = getCallStack() 

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

1486 else: 

1487 object.__delattr__(self, attr) 

1488 

1489 def __eq__(self, other): 

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

1491 for name in self._fields: 

1492 thisValue = getattr(self, name) 

1493 otherValue = getattr(other, name) 

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

1495 if not math.isnan(otherValue): 

1496 return False 

1497 elif thisValue != otherValue: 

1498 return False 

1499 return True 

1500 return False 

1501 

1502 def __ne__(self, other): 

1503 return not self.__eq__(other) 

1504 

1505 def __str__(self): 

1506 return str(self.toDict()) 

1507 

1508 def __repr__(self): 

1509 return "%s(%s)" % ( 

1510 _typeStr(self), 

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

1512 ) 

1513 

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

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

1516 equality. 

1517 

1518 Parameters 

1519 ---------- 

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

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

1522 config. 

1523 shortcut : `bool`, optional 

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

1525 `True`. 

1526 rtol : `float`, optional 

1527 Relative tolerance for floating point comparisons. 

1528 atol : `float`, optional 

1529 Absolute tolerance for floating point comparisons. 

1530 output : callable, optional 

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

1532 report inequalities. 

1533 

1534 Returns 

1535 ------- 

1536 isEqual : `bool` 

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

1538 `False` if there is an inequality. 

1539 

1540 See also 

1541 -------- 

1542 lsst.pex.config.compareConfigs 

1543 

1544 Notes 

1545 ----- 

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

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

1548 are not considered by this method. 

1549 

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

1551 """ 

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

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

1554 name = getComparisonName(name1, name2) 

1555 return compareConfigs(name, self, other, shortcut=shortcut, rtol=rtol, atol=atol, output=output) 

1556 

1557 @classmethod 

1558 def __init_subclass__(cls, **kwargs): 

1559 """Run initialization for every subclass. 

1560 

1561 Specifically registers the subclass with a YAML representer 

1562 and YAML constructor (if pyyaml is available) 

1563 """ 

1564 super().__init_subclass__(**kwargs) 

1565 

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

1567 return 

1568 

1569 yaml.add_representer(cls, _yaml_config_representer) 

1570 

1571 @classmethod 

1572 def _fromPython(cls, config_py): 

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

1574 

1575 Parameters 

1576 ---------- 

1577 config_py : `str` 

1578 A serialized form of the Config as created by 

1579 `Config.saveToStream`. 

1580 

1581 Returns 

1582 ------- 

1583 config : `Config` 

1584 Reconstructed `Config` instant. 

1585 """ 

1586 cls = _classFromPython(config_py) 

1587 return unreduceConfig(cls, config_py) 

1588 

1589 

1590def _classFromPython(config_py): 

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

1592 

1593 Parameters 

1594 ---------- 

1595 config_py : `str` 

1596 A serialized form of the Config as created by 

1597 `Config.saveToStream`. 

1598 

1599 Returns 

1600 ------- 

1601 cls : `type` 

1602 The `Config` subclass associated with this config. 

1603 """ 

1604 # standard serialization has the form: 

1605 # import config.class 

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

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

1608 

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

1610 # large config into separate lines. 

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

1612 

1613 if not matches: 

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

1615 raise ValueError( 

1616 "First two lines did not match expected form. Got:\n" f" - {first_line}\n" f" - {second_line}" 

1617 ) 

1618 

1619 module_name = matches.group(1) 

1620 module = importlib.import_module(module_name) 

1621 

1622 # Second line 

1623 full_name = matches.group(2) 

1624 

1625 # Remove the module name from the full name 

1626 if not full_name.startswith(module_name): 

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

1628 

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

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

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

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

1633 components = remainder.split(".") 

1634 pytype = module 

1635 for component in components: 

1636 pytype = getattr(pytype, component) 

1637 return pytype 

1638 

1639 

1640def unreduceConfig(cls, stream): 

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

1642 

1643 Parameters 

1644 ---------- 

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

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

1647 with configurations in the ``stream``. 

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

1649 Stream containing configuration override code. 

1650 

1651 Returns 

1652 ------- 

1653 config : `lsst.pex.config.Config` 

1654 Config instance. 

1655 

1656 See also 

1657 -------- 

1658 lsst.pex.config.Config.loadFromStream 

1659 """ 

1660 config = cls() 

1661 config.loadFromStream(stream) 

1662 return config