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 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 

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

66 """Generate nested configuration names.""" 

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 ↛ 158line 125 didn't jump to line 158, because the condition on line 125 was never false

126 

127 def _yaml_config_representer(dumper, data): 

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

129 

130 Stores the serialized stream as a scalar block string. 

131 """ 

132 stream = io.StringIO() 

133 data.saveToStream(stream) 

134 config_py = stream.getvalue() 

135 

136 # Strip multiple newlines from the end of the config 

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

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

139 

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

141 # Remove the trailing spaces so it has no choice 

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

143 

144 # Store the Python as a simple scalar 

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

146 

147 def _yaml_config_constructor(loader, node): 

148 """Construct a config from YAML""" 

149 config_py = loader.construct_scalar(node) 

150 return Config._fromPython(config_py) 

151 

152 # Register a generic constructor for Config and all subclasses 

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

154 for loader in YamlLoaders: 

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

156 

157 

158class ConfigMeta(type): 

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

160 

161 Notes 

162 ----- 

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

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

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

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

167 """ 

168 

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

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

171 cls._fields = {} 

172 cls._source = getStackFrame() 

173 

174 def getFields(classtype): 

175 fields = {} 

176 bases = list(classtype.__bases__) 

177 bases.reverse() 

178 for b in bases: 

179 fields.update(getFields(b)) 

180 

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

182 if isinstance(v, Field): 

183 fields[k] = v 

184 return fields 

185 

186 fields = getFields(cls) 

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

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

189 

190 def __setattr__(cls, name, value): 

191 if isinstance(value, Field): 

192 value.name = name 

193 cls._fields[name] = value 

194 type.__setattr__(cls, name, value) 

195 

196 

197class FieldValidationError(ValueError): 

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

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

200 

201 Parameters 

202 ---------- 

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

204 The field that was not valid. 

205 config : `lsst.pex.config.Config` 

206 The config containing the invalid field. 

207 msg : `str` 

208 Text describing why the field was not valid. 

209 """ 

210 

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

212 self.fieldType = type(field) 

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

214 """ 

215 

216 self.fieldName = field.name 

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

218 error (`str`). 

219 

220 See also 

221 -------- 

222 lsst.pex.config.Field.name 

223 """ 

224 

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

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

227 (`str`). 

228 """ 

229 

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

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

232 instance. 

233 """ 

234 

235 self.fieldSource = field.source 

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

237 """ 

238 

239 self.configSource = config._source 

240 error = ( 

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

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

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

244 % ( 

245 self.fieldType.__name__, 

246 self.fullname, 

247 msg, 

248 self.fieldSource.format(), 

249 self.configSource.format(), 

250 ) 

251 ) 

252 super().__init__(error) 

253 

254 

255class Field: 

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

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

258 

259 Parameters 

260 ---------- 

261 doc : `str` 

262 A description of the field for users. 

263 dtype : type 

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

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

266 `Field.supportedTypes`. 

267 default : object, optional 

268 The field's default value. 

269 check : callable, optional 

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

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

272 validation can be written as part of the 

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

274 optional : `bool`, optional 

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

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

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

278 deprecated : None or `str`, optional 

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

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

281 

282 Raises 

283 ------ 

284 ValueError 

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

286 (see `Field.supportedTypes`). 

287 

288 See also 

289 -------- 

290 ChoiceField 

291 ConfigChoiceField 

292 ConfigDictField 

293 ConfigField 

294 ConfigurableField 

295 DictField 

296 ListField 

297 RangeField 

298 RegistryField 

299 

300 Notes 

301 ----- 

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

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

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

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

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

307 `Field` attributes are `descriptors 

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

309 

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

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

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

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

314 type. See the example, below. 

315 

316 Examples 

317 -------- 

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

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

320 

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

322 >>> class Example(Config): 

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

324 ... 

325 >>> print(config.myInt) 

326 0 

327 >>> config.myInt = 5 

328 >>> print(config.myInt) 

329 5 

330 """ 

331 

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

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

334 """ 

335 

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

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

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

339 

340 source = getStackFrame() 

341 self._setup( 

342 doc=doc, 

343 dtype=dtype, 

344 default=default, 

345 check=check, 

346 optional=optional, 

347 source=source, 

348 deprecated=deprecated, 

349 ) 

350 

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

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

353 self.dtype = dtype 

354 """Data type for the field. 

355 """ 

356 

357 # append the deprecation message to the docstring. 

358 if deprecated is not None: 

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

360 self.doc = doc 

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

362 """ 

363 

364 self.deprecated = deprecated 

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

366 """ 

367 

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

369 if optional or default is not None: 

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

371 self.__doc__ += ")" 

372 

373 self.default = default 

374 """Default value for this field. 

375 """ 

376 

377 self.check = check 

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

379 """ 

380 

381 self.optional = optional 

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

383 

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

385 field's value is `None`. 

386 """ 

387 

388 self.source = source 

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

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

391 """ 

392 

393 def rename(self, instance): 

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

395 only). 

396 

397 Parameters 

398 ---------- 

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

400 The config instance that contains this field. 

401 

402 Notes 

403 ----- 

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

405 contains this field and should not be called directly. 

406 

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

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

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

410 `lsst.pex.config.config._joinNamePath`. 

411 """ 

412 pass 

413 

414 def validate(self, instance): 

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

416 

417 Parameters 

418 ---------- 

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

420 The config instance that contains this field. 

421 

422 Raises 

423 ------ 

424 lsst.pex.config.FieldValidationError 

425 Raised if verification fails. 

426 

427 Notes 

428 ----- 

429 This method provides basic validation: 

430 

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

432 - Ensures type correctness. 

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

434 

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

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

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

438 """ 

439 value = self.__get__(instance) 

440 if not self.optional and value is None: 

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

442 

443 def freeze(self, instance): 

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

445 

446 Parameters 

447 ---------- 

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

449 The config instance that contains this field. 

450 

451 Notes 

452 ----- 

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

454 hold subconfigs should freeze each subconfig. 

455 

456 **Subclasses should implement this method.** 

457 """ 

458 pass 

459 

460 def _validateValue(self, value): 

461 """Validate a value. 

462 

463 Parameters 

464 ---------- 

465 value : object 

466 The value being validated. 

467 

468 Raises 

469 ------ 

470 TypeError 

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

472 ``dtype``. 

473 ValueError 

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

475 """ 

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

477 return 

478 

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

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

481 value, 

482 _typeStr(value), 

483 _typeStr(self.dtype), 

484 ) 

485 raise TypeError(msg) 

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

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

488 raise ValueError(msg) 

489 

490 def _collectImports(self, instance, imports): 

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

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

493 set. 

494 

495 Parameters 

496 ---------- 

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

498 A config object that has this field defined on it 

499 imports : `set` 

500 Set of python modules that need imported after persistence 

501 """ 

502 pass 

503 

504 def save(self, outfile, instance): 

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

506 

507 Parameters 

508 ---------- 

509 outfile : file-like object 

510 A writeable field handle. 

511 instance : `Config` 

512 The `Config` instance that contains this field. 

513 

514 Notes 

515 ----- 

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

517 contains this field and should not be called directly. 

518 

519 The output consists of the documentation string 

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

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

522 

523 This output can be executed with Python. 

524 """ 

525 value = self.__get__(instance) 

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

527 

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

529 return 

530 

531 # write full documentation string as comment lines 

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

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

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

535 # non-finite numbers need special care 

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

537 else: 

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

539 

540 def toDict(self, instance): 

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

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

543 

544 Parameters 

545 ---------- 

546 instance : `Config` 

547 The `Config` that contains this field. 

548 

549 Returns 

550 ------- 

551 value : object 

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

553 

554 Notes 

555 ----- 

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

557 should not be called directly. 

558 

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

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

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

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

563 the field values in the subconfig. 

564 """ 

565 return self.__get__(instance) 

566 

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

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

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

570 directly 

571 

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

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

574 Config classes. 

575 

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

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

578 returned. 

579 """ 

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

581 return self 

582 else: 

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

584 try: 

585 return instance._storage[self.name] 

586 except AttributeError: 

587 if not isinstance(instance, Config): 

588 return self 

589 else: 

590 raise AttributeError( 

591 f"Config {instance} is missing " 

592 "_storage attribute, likely" 

593 " incorrectly initialized" 

594 ) 

595 

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

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

598 

599 Parameters 

600 ---------- 

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

602 The config instance that contains this field. 

603 value : obj 

604 Value to set on this field. 

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

606 The call stack (created by 

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

608 label : `str`, optional 

609 Event label for the history. 

610 

611 Notes 

612 ----- 

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

614 and should not be called directly. 

615 

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

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

618 should follow the following rules: 

619 

620 - Do not allow modification of frozen configs. 

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

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

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

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

625 invalid values. 

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

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

628 

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

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

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

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

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

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

635 reimplementation. 

636 """ 

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

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

639 

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

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

642 value = _autocast(value, self.dtype) 

643 try: 

644 self._validateValue(value) 

645 except BaseException as e: 

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

647 

648 instance._storage[self.name] = value 

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

650 at = getCallStack() 

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

652 

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

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

655 

656 Parameters 

657 ---------- 

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

659 The config instance that contains this field. 

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

661 The call stack (created by 

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

663 label : `str`, optional 

664 Event label for the history. 

665 

666 Notes 

667 ----- 

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

669 should not be called directly. 

670 """ 

671 if at is None: 

672 at = getCallStack() 

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

674 

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

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

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

678 

679 Parameters 

680 ---------- 

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

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

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

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

685 shortcut : `bool`, optional 

686 **Unused.** 

687 rtol : `float`, optional 

688 Relative tolerance for floating point comparisons. 

689 atol : `float`, optional 

690 Absolute tolerance for floating point comparisons. 

691 output : callable, optional 

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

693 report inequalities. 

694 

695 Notes 

696 ----- 

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

698 

699 See also 

700 -------- 

701 lsst.pex.config.compareScalars 

702 """ 

703 v1 = getattr(instance1, self.name) 

704 v2 = getattr(instance2, self.name) 

705 name = getComparisonName( 

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

707 ) 

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

709 

710 

711class RecordingImporter: 

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

713 imported. 

714 

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

716 

717 Examples 

718 -------- 

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

720 when done: 

721 

722 >>> with RecordingImporter() as importer: 

723 ... # import stuff 

724 ... import numpy as np 

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

726 """ 

727 

728 def __init__(self): 

729 self._modules = set() 

730 

731 def __enter__(self): 

732 self.origMetaPath = sys.meta_path 

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

734 return self 

735 

736 def __exit__(self, *args): 

737 self.uninstall() 

738 return False # Don't suppress exceptions 

739 

740 def uninstall(self): 

741 """Uninstall the importer.""" 

742 sys.meta_path = self.origMetaPath 

743 

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

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

746 self._modules.add(fullname) 

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

748 return None 

749 

750 def getModules(self): 

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

752 

753 Returns 

754 ------- 

755 modules : `set` of `str` 

756 Set of imported module names. 

757 """ 

758 return self._modules 

759 

760 

761class Config(metaclass=ConfigMeta): 

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

763 

764 Notes 

765 ----- 

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

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

768 class behavior. 

769 

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

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

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

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

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

775 configuration instance. 

776 

777 Examples 

778 -------- 

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

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

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

782 

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

784 >>> class DemoConfig(Config): 

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

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

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

788 ... 

789 >>> config = DemoConfig() 

790 

791 Configs support many `dict`-like APIs: 

792 

793 >>> config.keys() 

794 ['intField', 'listField'] 

795 >>> 'intField' in config 

796 True 

797 

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

799 

800 >>> config.intField 

801 42 

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

803 >>> print(config.listField) 

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

805 """ 

806 

807 def __iter__(self): 

808 """Iterate over fields.""" 

809 return self._fields.__iter__() 

810 

811 def keys(self): 

812 """Get field names. 

813 

814 Returns 

815 ------- 

816 names : `list` 

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

818 

819 See also 

820 -------- 

821 lsst.pex.config.Config.iterkeys 

822 """ 

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

824 

825 def values(self): 

826 """Get field values. 

827 

828 Returns 

829 ------- 

830 values : `list` 

831 List of field values. 

832 

833 See also 

834 -------- 

835 lsst.pex.config.Config.itervalues 

836 """ 

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

838 

839 def items(self): 

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

841 

842 Returns 

843 ------- 

844 items : `list` 

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

846 

847 0. Field name. 

848 1. Field value. 

849 

850 See also 

851 -------- 

852 lsst.pex.config.Config.iteritems 

853 """ 

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

855 

856 def iteritems(self): 

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

858 

859 Yields 

860 ------ 

861 item : `tuple` 

862 Tuple items are: 

863 

864 0. Field name. 

865 1. Field value. 

866 

867 See also 

868 -------- 

869 lsst.pex.config.Config.items 

870 """ 

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

872 

873 def itervalues(self): 

874 """Iterate over field values. 

875 

876 Yields 

877 ------ 

878 value : obj 

879 A field value. 

880 

881 See also 

882 -------- 

883 lsst.pex.config.Config.values 

884 """ 

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

886 

887 def iterkeys(self): 

888 """Iterate over field names 

889 

890 Yields 

891 ------ 

892 key : `str` 

893 A field's key (attribute name). 

894 

895 See also 

896 -------- 

897 lsst.pex.config.Config.values 

898 """ 

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

900 

901 def __contains__(self, name): 

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

903 

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

905 """ 

906 return self._storage.__contains__(name) 

907 

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

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

910 

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

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

913 some attributes are handled at allocation time rather than at 

914 initialization. 

915 

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

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

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

919 """ 

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

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

922 # remove __label and ignore it 

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

924 

925 instance = object.__new__(cls) 

926 instance._frozen = False 

927 instance._name = name 

928 instance._storage = {} 

929 instance._history = {} 

930 instance._imports = set() 

931 # load up defaults 

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

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

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

935 # set custom default-overides 

936 instance.setDefaults() 

937 # set constructor overides 

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

939 return instance 

940 

941 def __reduce__(self): 

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

943 

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

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

946 be pickled. 

947 """ 

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

949 # requires bytes 

950 stream = io.StringIO() 

951 self.saveToStream(stream) 

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

953 

954 def setDefaults(self): 

955 """Subclass hook for computing defaults. 

956 

957 Notes 

958 ----- 

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

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

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

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

963 ``setDefaults``. 

964 """ 

965 pass 

966 

967 def update(self, **kw): 

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

969 

970 Parameters 

971 ---------- 

972 kw 

973 Keywords are configuration field names. Values are configuration 

974 field values. 

975 

976 Notes 

977 ----- 

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

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

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

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

982 

983 Examples 

984 -------- 

985 This is a config with three fields: 

986 

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

988 >>> class DemoConfig(Config): 

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

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

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

992 ... 

993 >>> config = DemoConfig() 

994 

995 These are the default values of each field: 

996 

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

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

999 ... 

1000 fieldA: 42 

1001 fieldB: True 

1002 fieldC: 'Hello world' 

1003 

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

1005 

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

1007 

1008 Now the values of each field are: 

1009 

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

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

1012 ... 

1013 fieldA: 13 

1014 fieldB: True 

1015 fieldC: 'Updated!' 

1016 """ 

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

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

1019 

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

1021 try: 

1022 field = self._fields[name] 

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

1024 except KeyError: 

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

1026 

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

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

1029 configuration file. 

1030 

1031 Parameters 

1032 ---------- 

1033 filename : `str` 

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

1035 module. 

1036 root : `str`, optional 

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

1038 overridden. 

1039 

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

1041 contains:: 

1042 

1043 config.myField = 5 

1044 

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

1046 

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

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

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

1050 removed at some point. 

1051 

1052 See also 

1053 -------- 

1054 lsst.pex.config.Config.loadFromStream 

1055 lsst.pex.config.Config.loadFromString 

1056 lsst.pex.config.Config.save 

1057 lsst.pex.config.Config.saveToStream 

1058 lsst.pex.config.Config.saveToString 

1059 """ 

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

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

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

1063 

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

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

1066 provided stream. 

1067 

1068 Parameters 

1069 ---------- 

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

1071 Stream containing configuration override code. If this is a 

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

1073 root : `str`, optional 

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

1075 overridden. 

1076 

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

1078 contains:: 

1079 

1080 config.myField = 5 

1081 

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

1083 

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

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

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

1087 removed at some point. 

1088 filename : `str`, optional 

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

1090 in the stream. Used for error reporting. 

1091 

1092 Notes 

1093 ----- 

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

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

1096 `loadFromString` instead for most of these types. 

1097 

1098 See also 

1099 -------- 

1100 lsst.pex.config.Config.load 

1101 lsst.pex.config.Config.loadFromString 

1102 lsst.pex.config.Config.save 

1103 lsst.pex.config.Config.saveToStream 

1104 lsst.pex.config.Config.saveToString 

1105 """ 

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

1107 if filename is None: 

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

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

1110 else: 

1111 code = stream 

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

1113 

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

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

1116 provided string. 

1117 

1118 Parameters 

1119 ---------- 

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

1121 Stream containing configuration override code. 

1122 root : `str`, optional 

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

1124 overridden. 

1125 

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

1127 contains:: 

1128 

1129 config.myField = 5 

1130 

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

1132 

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

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

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

1136 removed at some point. 

1137 filename : `str`, optional 

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

1139 in the stream. Used for error reporting. 

1140 

1141 See also 

1142 -------- 

1143 lsst.pex.config.Config.load 

1144 lsst.pex.config.Config.loadFromStream 

1145 lsst.pex.config.Config.save 

1146 lsst.pex.config.Config.saveToStream 

1147 lsst.pex.config.Config.saveToString 

1148 """ 

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

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

1151 # has attribute "co_filename", 

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

1153 with RecordingImporter() as importer: 

1154 globals = {"__file__": filename} 

1155 try: 

1156 local = {root: self} 

1157 exec(code, globals, local) 

1158 except NameError as e: 

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

1160 print( 

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

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

1163 file=sys.stderr, 

1164 ) 

1165 local = {"root": self} 

1166 exec(code, globals, local) 

1167 else: 

1168 raise 

1169 

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

1171 

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

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

1174 reproduces this config. 

1175 

1176 Parameters 

1177 ---------- 

1178 filename : `str` 

1179 Desination filename of this configuration. 

1180 root : `str`, optional 

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

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

1183 

1184 See also 

1185 -------- 

1186 lsst.pex.config.Config.saveToStream 

1187 lsst.pex.config.Config.saveToString 

1188 lsst.pex.config.Config.load 

1189 lsst.pex.config.Config.loadFromStream 

1190 lsst.pex.config.Config.loadFromString 

1191 """ 

1192 d = os.path.dirname(filename) 

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

1194 self.saveToStream(outfile, root) 

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

1196 # for an explantion of these antics see: 

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

1198 umask = os.umask(0o077) 

1199 os.umask(umask) 

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

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

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

1203 # os.rename may not work across filesystems 

1204 shutil.move(outfile.name, filename) 

1205 

1206 def saveToString(self, skipImports=False): 

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

1208 string. 

1209 

1210 Parameters 

1211 ---------- 

1212 skipImports : `bool`, optional 

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

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

1215 additional clutter is not useful. 

1216 

1217 Returns 

1218 ------- 

1219 code : `str` 

1220 A code string readable by `loadFromString`. 

1221 

1222 See also 

1223 -------- 

1224 lsst.pex.config.Config.save 

1225 lsst.pex.config.Config.saveToStream 

1226 lsst.pex.config.Config.load 

1227 lsst.pex.config.Config.loadFromStream 

1228 lsst.pex.config.Config.loadFromString 

1229 """ 

1230 buffer = io.StringIO() 

1231 self.saveToStream(buffer, skipImports=skipImports) 

1232 return buffer.getvalue() 

1233 

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

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

1236 reproduces this config. 

1237 

1238 Parameters 

1239 ---------- 

1240 outfile : file-like object 

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

1242 bytes. 

1243 root 

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

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

1246 skipImports : `bool`, optional 

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

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

1249 additional clutter is not useful. 

1250 

1251 See also 

1252 -------- 

1253 lsst.pex.config.Config.save 

1254 lsst.pex.config.Config.saveToString 

1255 lsst.pex.config.Config.load 

1256 lsst.pex.config.Config.loadFromStream 

1257 lsst.pex.config.Config.loadFromString 

1258 """ 

1259 tmp = self._name 

1260 self._rename(root) 

1261 try: 

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

1263 self._collectImports() 

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

1265 self._imports.remove(self.__module__) 

1266 configType = type(self) 

1267 typeString = _typeStr(configType) 

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

1269 outfile.write( 

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

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

1272 ) 

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

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

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

1276 self._save(outfile) 

1277 finally: 

1278 self._rename(tmp) 

1279 

1280 def freeze(self): 

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

1282 self._frozen = True 

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

1284 field.freeze(self) 

1285 

1286 def _save(self, outfile): 

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

1288 

1289 Parameters 

1290 ---------- 

1291 outfile : file-like object 

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

1293 bytes. 

1294 """ 

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

1296 field.save(outfile, self) 

1297 

1298 def _collectImports(self): 

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

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

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

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

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

1304 class. 

1305 """ 

1306 self._imports.add(self.__module__) 

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

1308 field._collectImports(self, self._imports) 

1309 

1310 def toDict(self): 

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

1312 

1313 Returns 

1314 ------- 

1315 dict_ : `dict` 

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

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

1318 

1319 See also 

1320 -------- 

1321 lsst.pex.config.Field.toDict 

1322 

1323 Notes 

1324 ----- 

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

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

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

1328 """ 

1329 dict_ = {} 

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

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

1332 return dict_ 

1333 

1334 def names(self): 

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

1336 

1337 Returns 

1338 ------- 

1339 names : `list` of `str` 

1340 Field names. 

1341 """ 

1342 # 

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

1344 # pre-existing saveToStream() 

1345 # 

1346 with io.StringIO() as strFd: 

1347 self.saveToStream(strFd, "config") 

1348 contents = strFd.getvalue() 

1349 strFd.close() 

1350 # 

1351 # Pull the names out of the dumped config 

1352 # 

1353 keys = [] 

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

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

1356 continue 

1357 

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

1359 if mat: 

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

1361 

1362 return keys 

1363 

1364 def _rename(self, name): 

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

1366 

1367 Parameters 

1368 ---------- 

1369 name : `str` 

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

1371 

1372 Notes 

1373 ----- 

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

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

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

1377 method for *this* method to work. 

1378 

1379 See also 

1380 -------- 

1381 lsst.pex.config.Field.rename 

1382 """ 

1383 self._name = name 

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

1385 field.rename(self) 

1386 

1387 def validate(self): 

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

1389 

1390 Raises 

1391 ------ 

1392 lsst.pex.config.FieldValidationError 

1393 Raised if verification fails. 

1394 

1395 Notes 

1396 ----- 

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

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

1399 

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

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

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

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

1404 that handle recursing into subconfigs. 

1405 

1406 Inter-field relationships should only be checked in derived 

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

1408 validation is complete. 

1409 """ 

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

1411 field.validate(self) 

1412 

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

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

1415 

1416 Parameters 

1417 ---------- 

1418 name : `str` 

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

1420 kwargs 

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

1422 

1423 Returns 

1424 ------- 

1425 history : `str` 

1426 A string containing the formatted history. 

1427 

1428 See also 

1429 -------- 

1430 lsst.pex.config.history.format 

1431 """ 

1432 import lsst.pex.config.history as pexHist 

1433 

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

1435 

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

1437 """Read-only history. 

1438 """ 

1439 

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

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

1442 

1443 Notes 

1444 ----- 

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

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

1447 to them dynamically. 

1448 

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

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

1451 non-existent field. 

1452 """ 

1453 if attr in self._fields: 

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

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

1456 warnings.warn( 

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

1458 FutureWarning, 

1459 stacklevel=2, 

1460 ) 

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

1462 at = getCallStack() 

1463 # This allows Field descriptors to work. 

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

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

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

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

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

1469 # This allows specific private attributes to work. 

1470 self.__dict__[attr] = value 

1471 else: 

1472 # We throw everything else. 

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

1474 

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

1476 if attr in self._fields: 

1477 if at is None: 

1478 at = getCallStack() 

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

1480 else: 

1481 object.__delattr__(self, attr) 

1482 

1483 def __eq__(self, other): 

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

1485 for name in self._fields: 

1486 thisValue = getattr(self, name) 

1487 otherValue = getattr(other, name) 

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

1489 if not math.isnan(otherValue): 

1490 return False 

1491 elif thisValue != otherValue: 

1492 return False 

1493 return True 

1494 return False 

1495 

1496 def __ne__(self, other): 

1497 return not self.__eq__(other) 

1498 

1499 def __str__(self): 

1500 return str(self.toDict()) 

1501 

1502 def __repr__(self): 

1503 return "%s(%s)" % ( 

1504 _typeStr(self), 

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

1506 ) 

1507 

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

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

1510 equality. 

1511 

1512 Parameters 

1513 ---------- 

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

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

1516 config. 

1517 shortcut : `bool`, optional 

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

1519 `True`. 

1520 rtol : `float`, optional 

1521 Relative tolerance for floating point comparisons. 

1522 atol : `float`, optional 

1523 Absolute tolerance for floating point comparisons. 

1524 output : callable, optional 

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

1526 report inequalities. 

1527 

1528 Returns 

1529 ------- 

1530 isEqual : `bool` 

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

1532 `False` if there is an inequality. 

1533 

1534 See also 

1535 -------- 

1536 lsst.pex.config.compareConfigs 

1537 

1538 Notes 

1539 ----- 

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

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

1542 are not considered by this method. 

1543 

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

1545 """ 

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

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

1548 name = getComparisonName(name1, name2) 

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

1550 

1551 @classmethod 

1552 def __init_subclass__(cls, **kwargs): 

1553 """Run initialization for every subclass. 

1554 

1555 Specifically registers the subclass with a YAML representer 

1556 and YAML constructor (if pyyaml is available) 

1557 """ 

1558 super().__init_subclass__(**kwargs) 

1559 

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

1561 return 

1562 

1563 yaml.add_representer(cls, _yaml_config_representer) 

1564 

1565 @classmethod 

1566 def _fromPython(cls, config_py): 

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

1568 

1569 Parameters 

1570 ---------- 

1571 config_py : `str` 

1572 A serialized form of the Config as created by 

1573 `Config.saveToStream`. 

1574 

1575 Returns 

1576 ------- 

1577 config : `Config` 

1578 Reconstructed `Config` instant. 

1579 """ 

1580 cls = _classFromPython(config_py) 

1581 return unreduceConfig(cls, config_py) 

1582 

1583 

1584def _classFromPython(config_py): 

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

1586 

1587 Parameters 

1588 ---------- 

1589 config_py : `str` 

1590 A serialized form of the Config as created by 

1591 `Config.saveToStream`. 

1592 

1593 Returns 

1594 ------- 

1595 cls : `type` 

1596 The `Config` subclass associated with this config. 

1597 """ 

1598 # standard serialization has the form: 

1599 # import config.class 

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

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

1602 

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

1604 # large config into separate lines. 

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

1606 

1607 if not matches: 

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

1609 raise ValueError( 

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

1611 ) 

1612 

1613 module_name = matches.group(1) 

1614 module = importlib.import_module(module_name) 

1615 

1616 # Second line 

1617 full_name = matches.group(2) 

1618 

1619 # Remove the module name from the full name 

1620 if not full_name.startswith(module_name): 

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

1622 

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

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

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

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

1627 components = remainder.split(".") 

1628 pytype = module 

1629 for component in components: 

1630 pytype = getattr(pytype, component) 

1631 return pytype 

1632 

1633 

1634def unreduceConfig(cls, stream): 

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

1636 

1637 Parameters 

1638 ---------- 

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

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

1641 with configurations in the ``stream``. 

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

1643 Stream containing configuration override code. 

1644 

1645 Returns 

1646 ------- 

1647 config : `lsst.pex.config.Config` 

1648 Config instance. 

1649 

1650 See also 

1651 -------- 

1652 lsst.pex.config.Config.loadFromStream 

1653 """ 

1654 config = cls() 

1655 config.loadFromStream(stream) 

1656 return config