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

454 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-09 03:27 -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/>. 

27from __future__ import annotations 

28 

29__all__ = ( 

30 "Config", 

31 "ConfigMeta", 

32 "Field", 

33 "FieldTypeVar", 

34 "FieldValidationError", 

35 "UnexpectedProxyUsageError", 

36) 

37 

38import copy 

39import importlib 

40import io 

41import math 

42import numbers 

43import os 

44import re 

45import shutil 

46import sys 

47import tempfile 

48import warnings 

49from collections.abc import Mapping 

50from types import GenericAlias 

51from typing import Any, ForwardRef, Generic, TypeVar, cast, overload 

52 

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

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

55try: 

56 import yaml 

57except ImportError: 

58 yaml = None 

59 

60from .callStack import getCallStack, getStackFrame 

61from .comparison import compareConfigs, compareScalars, getComparisonName 

62 

63if yaml: 63 ↛ 74line 63 didn't jump to line 74 because the condition on line 63 was always true

64 YamlLoaders: tuple[Any, ...] = (yaml.Loader, yaml.FullLoader, yaml.SafeLoader, yaml.UnsafeLoader) 

65 

66 try: 

67 # CLoader is not always available 

68 from yaml import CLoader 

69 

70 YamlLoaders += (CLoader,) 

71 except ImportError: 

72 pass 

73else: 

74 YamlLoaders = () 

75 doImport = None 

76 

77 

78class _PexConfigGenericAlias(GenericAlias): 

79 """A Subclass of python's GenericAlias used in defining and instantiating 

80 Generics. 

81 

82 This class differs from `types.GenericAlias` in that it calls a method 

83 named _parseTypingArgs defined on Fields. This method gives Field and its 

84 subclasses an opportunity to transform type parameters into class key word 

85 arguments. Code authors do not need to implement any returns of this object 

86 directly, and instead only need implement _parseTypingArgs, if a Field 

87 subclass differs from the base class implementation. 

88 

89 This class is intended to be an implementation detail, returned from a 

90 Field's `__class_getitem__` method. 

91 """ 

92 

93 def __call__(self, *args: Any, **kwds: Any) -> Any: 

94 origin_kwargs = self._parseTypingArgs(self.__args__, kwds) 

95 return super().__call__(*args, **{**kwds, **origin_kwargs}) 

96 

97 

98FieldTypeVar = TypeVar("FieldTypeVar") 

99 

100 

101class UnexpectedProxyUsageError(TypeError): 

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

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

104 """ 

105 

106 

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

108 """Generate nested configuration names.""" 

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

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

111 elif not name: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true

112 name = prefix 

113 elif prefix and name: 113 ↛ 116line 113 didn't jump to line 116 because the condition on line 113 was always true

114 name = prefix + "." + name 

115 

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

117 return f"{name}[{index!r}]" 

118 else: 

119 return name 

120 

121 

122def _autocast(x, dtype): 

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

124 

125 Parameters 

126 ---------- 

127 x : object 

128 A value. 

129 dtype : type 

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

131 

132 Returns 

133 ------- 

134 values : object 

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

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

137 ``x`` is returned. 

138 

139 Notes 

140 ----- 

141 Will convert numpy scalar types to the standard Python equivalents. 

142 """ 

143 if dtype is float and isinstance(x, numbers.Real): 143 ↛ 145line 143 didn't jump to line 145 because the condition on line 143 was always true

144 return float(x) 

145 if dtype is int and isinstance(x, numbers.Integral): 

146 return int(x) 

147 return x 

148 

149 

150def _typeStr(x): 

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

152 

153 Returns 

154 ------- 

155 `str` 

156 Fully-qualified type name. 

157 

158 Notes 

159 ----- 

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

161 later upon with the 'load' function. 

162 """ 

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

164 xtype = x 

165 else: 

166 xtype = type(x) 

167 if xtype.__module__ == "builtins": 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true

168 return xtype.__name__ 

169 else: 

170 return f"{xtype.__module__}.{xtype.__name__}" 

171 

172 

173if yaml: 173 ↛ 206line 173 didn't jump to line 206 because the condition on line 173 was always true

174 

175 def _yaml_config_representer(dumper, data): 

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

177 

178 Stores the serialized stream as a scalar block string. 

179 """ 

180 stream = io.StringIO() 

181 data.saveToStream(stream) 

182 config_py = stream.getvalue() 

183 

184 # Strip multiple newlines from the end of the config 

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

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

187 

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

189 # Remove the trailing spaces so it has no choice 

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

191 

192 # Store the Python as a simple scalar 

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

194 

195 def _yaml_config_constructor(loader, node): 

196 """Construct a config from YAML.""" 

197 config_py = loader.construct_scalar(node) 

198 return Config._fromPython(config_py) 

199 

200 # Register a generic constructor for Config and all subclasses 

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

202 for loader in YamlLoaders: 

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

204 

205 

206class ConfigMeta(type): 

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

208 

209 Parameters 

210 ---------- 

211 name : `str` 

212 Name to use for class. 

213 bases : `~collections.abc.Iterable` 

214 Base classes. 

215 dict_ : `dict` 

216 Additional parameters. 

217 

218 Notes 

219 ----- 

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

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

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

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

224 """ 

225 

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

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

228 cls._fields = {} 

229 cls._source = getStackFrame() 

230 

231 def getFields(classtype): 

232 fields = {} 

233 bases = list(classtype.__bases__) 

234 bases.reverse() 

235 for b in bases: 

236 fields.update(getFields(b)) 

237 

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

239 if isinstance(v, Field): 

240 fields[k] = v 

241 return fields 

242 

243 fields = getFields(cls) 

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

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

246 

247 def __setattr__(cls, name, value): 

248 if isinstance(value, Field): 

249 value.name = name 

250 cls._fields[name] = value 

251 type.__setattr__(cls, name, value) 

252 

253 

254class FieldValidationError(ValueError): 

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

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

257 

258 Parameters 

259 ---------- 

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

261 The field that was not valid. 

262 config : `lsst.pex.config.Config` 

263 The config containing the invalid field. 

264 msg : `str` 

265 Text describing why the field was not valid. 

266 """ 

267 

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

269 self.fieldType = type(field) 

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

271 """ 

272 

273 self.fieldName = field.name 

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

275 error (`str`). 

276 

277 See also 

278 -------- 

279 lsst.pex.config.Field.name 

280 """ 

281 

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

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

284 (`str`). 

285 """ 

286 

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

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

289 instance. 

290 """ 

291 

292 self.fieldSource = field.source 

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

294 """ 

295 

296 self.configSource = config._source 

297 error = ( 

298 f"{self.fieldType.__name__} '{self.fullname}' failed validation: {msg}\n" 

299 f"For more information see the Field definition at:\n{self.fieldSource.format()}" 

300 f" and the Config definition at:\n{self.configSource.format()}" 

301 ) 

302 super().__init__(error) 

303 

304 

305class Field(Generic[FieldTypeVar]): 

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

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

308 

309 Parameters 

310 ---------- 

311 doc : `str` 

312 A description of the field for users. 

313 dtype : type, optional 

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

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

316 `Field.supportedTypes`. Optional if supplied as a typing argument to 

317 the class. 

318 default : object, optional 

319 The field's default value. 

320 check : callable, optional 

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

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

323 validation can be written as part of the 

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

325 optional : `bool`, optional 

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

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

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

329 deprecated : None or `str`, optional 

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

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

332 

333 Raises 

334 ------ 

335 ValueError 

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

337 (see `Field.supportedTypes`). 

338 

339 See Also 

340 -------- 

341 ChoiceField 

342 ConfigChoiceField 

343 ConfigDictField 

344 ConfigField 

345 ConfigurableField 

346 DictField 

347 ListField 

348 RangeField 

349 RegistryField 

350 

351 Notes 

352 ----- 

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

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

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

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

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

358 `Field` attributes are `descriptors 

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

360 

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

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

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

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

365 type. See the example, below. 

366 

367 Fields can be annotated with a type similar to other python classes (python 

368 specification `here <https://peps.python.org/pep-0484/#generics>`_ ). 

369 See the name field in the Config example below for an example of this. 

370 Unlike most other uses in python, this has an effect at type checking *and* 

371 runtime. If the type is specified with a class annotation, it will be used 

372 as the value of the ``dtype`` in the ``Field`` and there is no need to 

373 specify it as an argument during instantiation. 

374 

375 There are Some notes on dtype through type annotation syntax. Type 

376 annotation syntax supports supplying the argument as a string of a type 

377 name. i.e. "float", but this cannot be used to resolve circular references. 

378 Type annotation syntax can be used on an identifier in addition to Class 

379 assignment i.e. ``variable: Field[str] = Config.someField`` vs 

380 ``someField = Field[str](doc="some doc"). However, this syntax is only 

381 useful for annotating the type of the identifier (i.e. variable in previous 

382 example) and does nothing for assigning the dtype of the ``Field``. 

383 

384 Examples 

385 -------- 

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

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

388 

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

390 >>> class Example(Config): 

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

392 ... name = Field[str](doc="A string Field") 

393 >>> print(config.myInt) 

394 0 

395 >>> config.myInt = 5 

396 >>> print(config.myInt) 

397 5 

398 """ 

399 

400 name: str 

401 """Identifier (variable name) used to refer to a Field within a Config 

402 Class. 

403 """ 

404 

405 supportedTypes = {str, bool, float, int, complex} 

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

407 """ 

408 

409 @staticmethod 

410 def _parseTypingArgs( 

411 params: tuple[type, ...] | tuple[str, ...], kwds: Mapping[str, Any] 

412 ) -> Mapping[str, Any]: 

413 """Parse type annotations into keyword constructor arguments. 

414 

415 This is a special private method that interprets type arguments (i.e. 

416 Field[str]) into keyword arguments to be passed on to the constructor. 

417 

418 Subclasses of Field can implement this method to customize how they 

419 handle turning type parameters into keyword arguments (see DictField 

420 for an example) 

421 

422 Parameters 

423 ---------- 

424 params : `tuple` of `type` or `tuple` of str 

425 Parameters passed to the type annotation. These will either be 

426 types or strings. Strings are to interpreted as forward references 

427 and will be treated as such. 

428 kwds : `MutableMapping` with keys of `str` and values of `Any` 

429 These are the user supplied keywords that are to be passed to the 

430 Field constructor. 

431 

432 Returns 

433 ------- 

434 kwds : `MutableMapping` with keys of `str` and values of `Any` 

435 The mapping of keywords that will be passed onto the constructor 

436 of the Field. Should be filled in with any information gleaned 

437 from the input parameters. 

438 

439 Raises 

440 ------ 

441 ValueError 

442 Raised if params is of incorrect length. 

443 Raised if a forward reference could not be resolved 

444 Raised if there is a conflict between params and values in kwds 

445 """ 

446 if len(params) > 1: 

447 raise ValueError("Only single type parameters are supported") 

448 unpackedParams = params[0] 

449 if isinstance(unpackedParams, str): 

450 _typ = ForwardRef(unpackedParams) 

451 # type ignore below because typeshed seems to be wrong. It 

452 # indicates there are only 2 args, as it was in python 3.8, but 

453 # 3.9+ takes 3 args. 

454 result = _typ._evaluate(globals(), locals(), recursive_guard=set()) # type: ignore 

455 if result is None: 

456 raise ValueError("Could not deduce type from input") 

457 unpackedParams = cast(type, result) 

458 if "dtype" in kwds and kwds["dtype"] != unpackedParams: 

459 raise ValueError("Conflicting definition for dtype") 

460 elif "dtype" not in kwds: 

461 kwds = {**kwds, **{"dtype": unpackedParams}} 

462 return kwds 

463 

464 def __class_getitem__(cls, params: tuple[type, ...] | type | ForwardRef): 

465 return _PexConfigGenericAlias(cls, params) 

466 

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

468 if dtype is None: 468 ↛ 469line 468 didn't jump to line 469 because the condition on line 468 was never true

469 raise ValueError( 

470 "dtype must either be supplied as an argument or as a type argument to the class" 

471 ) 

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

473 raise ValueError(f"Unsupported Field dtype {_typeStr(dtype)}") 

474 

475 source = getStackFrame() 

476 self._setup( 

477 doc=doc, 

478 dtype=dtype, 

479 default=default, 

480 check=check, 

481 optional=optional, 

482 source=source, 

483 deprecated=deprecated, 

484 ) 

485 

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

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

488 self.dtype = dtype 

489 """Data type for the field. 

490 """ 

491 

492 if not doc: 492 ↛ 493line 492 didn't jump to line 493 because the condition on line 492 was never true

493 raise ValueError("Docstring is empty.") 

494 

495 # append the deprecation message to the docstring. 

496 if deprecated is not None: 

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

498 self.doc = doc 

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

500 """ 

501 

502 self.deprecated = deprecated 

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

504 """ 

505 

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

507 if optional or default is not None: 

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

509 self.__doc__ += ")" 

510 

511 self.default = default 

512 """Default value for this field. 

513 """ 

514 

515 self.check = check 

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

517 """ 

518 

519 self.optional = optional 

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

521 

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

523 field's value is `None`. 

524 """ 

525 

526 self.source = source 

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

528 `~lsst.pex.config.callStack.StackFrame`). 

529 """ 

530 

531 def rename(self, instance): 

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

533 only). 

534 

535 Parameters 

536 ---------- 

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

538 The config instance that contains this field. 

539 

540 Notes 

541 ----- 

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

543 contains this field and should not be called directly. 

544 

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

546 hold subconfigs. `~lsst.pex.config.Field`\s that hold subconfigs should 

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

548 `lsst.pex.config.config._joinNamePath`. 

549 """ 

550 pass 

551 

552 def validate(self, instance): 

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

554 

555 Parameters 

556 ---------- 

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

558 The config instance that contains this field. 

559 

560 Raises 

561 ------ 

562 lsst.pex.config.FieldValidationError 

563 Raised if verification fails. 

564 

565 Notes 

566 ----- 

567 This method provides basic validation: 

568 

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

570 - Ensures type correctness. 

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

572 

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

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

575 `~lsst.pex.config.Field.validate`. 

576 """ 

577 value = self.__get__(instance) 

578 if not self.optional and value is None: 

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

580 

581 def freeze(self, instance): 

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

583 

584 Parameters 

585 ---------- 

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

587 The config instance that contains this field. 

588 

589 Notes 

590 ----- 

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

592 hold subconfigs should freeze each subconfig. 

593 

594 **Subclasses should implement this method.** 

595 """ 

596 pass 

597 

598 def _validateValue(self, value): 

599 """Validate a value. 

600 

601 Parameters 

602 ---------- 

603 value : object 

604 The value being validated. 

605 

606 Raises 

607 ------ 

608 TypeError 

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

610 ``dtype``. 

611 ValueError 

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

613 """ 

614 if value is None: 614 ↛ 615line 614 didn't jump to line 615 because the condition on line 614 was never true

615 return 

616 

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

618 msg = ( 

619 f"Value {value} is of incorrect type {_typeStr(value)}. Expected type {_typeStr(self.dtype)}" 

620 ) 

621 raise TypeError(msg) 

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

623 msg = f"Value {value} is not a valid value" 

624 raise ValueError(msg) 

625 

626 def _collectImports(self, instance, imports): 

627 """Call the _collectImports method on all config 

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

629 set. 

630 

631 Parameters 

632 ---------- 

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

634 A config object that has this field defined on it 

635 imports : `set` 

636 Set of python modules that need imported after persistence 

637 """ 

638 pass 

639 

640 def save(self, outfile, instance): 

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

642 

643 Parameters 

644 ---------- 

645 outfile : file-like object 

646 A writeable field handle. 

647 instance : `~lsst.pex.config.Config` 

648 The `~lsst.pex.config.Config` instance that contains this field. 

649 

650 Notes 

651 ----- 

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

653 contains this field and should not be called directly. 

654 

655 The output consists of the documentation string 

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

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

658 

659 This output can be executed with Python. 

660 """ 

661 value = self.__get__(instance) 

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

663 

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

665 return 

666 

667 # write full documentation string as comment lines 

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

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

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

671 # non-finite numbers need special care 

672 outfile.write(f"{doc}\n{fullname}=float('{value!r}')\n\n") 

673 else: 

674 outfile.write(f"{doc}\n{fullname}={value!r}\n\n") 

675 

676 def toDict(self, instance): 

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

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

679 

680 Parameters 

681 ---------- 

682 instance : `~lsst.pex.config.Config` 

683 The `~lsst.pex.config.Config` that contains this field. 

684 

685 Returns 

686 ------- 

687 value : object 

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

689 

690 Notes 

691 ----- 

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

693 should not be called directly. 

694 

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

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

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

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

699 the field values in the subconfig. 

700 """ 

701 return self.__get__(instance) 

702 

703 @overload 

704 def __get__( 

705 self, instance: None, owner: Any = None, at: Any = None, label: str = "default" 

706 ) -> Field[FieldTypeVar]: ... 

707 

708 @overload 

709 def __get__( 

710 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default" 

711 ) -> FieldTypeVar: ... 

712 

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

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

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

716 directly. 

717 

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

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

720 Config classes. 

721 

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

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

724 returned. 

725 """ 

726 if instance is None: 726 ↛ 727line 726 didn't jump to line 727 because the condition on line 726 was never true

727 return self 

728 else: 

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

730 try: 

731 return instance._storage[self.name] 

732 except AttributeError: 

733 if not isinstance(instance, Config): 

734 return self 

735 else: 

736 raise AttributeError( 

737 f"Config {instance} is missing _storage attribute, likely incorrectly initialized" 

738 ) from None 

739 

740 def __set__( 

741 self, instance: Config, value: FieldTypeVar | None, at: Any = None, label: str = "assignment" 

742 ) -> None: 

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

744 

745 Parameters 

746 ---------- 

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

748 The config instance that contains this field. 

749 value : obj 

750 Value to set on this field. 

751 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`,\ 

752 optional 

753 The call stack (created by 

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

755 label : `str`, optional 

756 Event label for the history. 

757 

758 Notes 

759 ----- 

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

761 and should not be called directly. 

762 

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

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

765 should follow the following rules: 

766 

767 - Do not allow modification of frozen configs. 

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

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

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

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

772 invalid values. 

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

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

775 

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

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

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

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

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

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

782 reimplementation. 

783 """ 

784 if instance._frozen: 784 ↛ 785line 784 didn't jump to line 785 because the condition on line 784 was never true

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

786 

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

788 if value is not None: 788 ↛ 795line 788 didn't jump to line 795 because the condition on line 788 was always true

789 value = _autocast(value, self.dtype) 

790 try: 

791 self._validateValue(value) 

792 except BaseException as e: 

793 raise FieldValidationError(self, instance, str(e)) from e 

794 

795 instance._storage[self.name] = value 

796 if at is None: 796 ↛ 797line 796 didn't jump to line 797 because the condition on line 796 was never true

797 at = getCallStack() 

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

799 

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

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

802 

803 Parameters 

804 ---------- 

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

806 The config instance that contains this field. 

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

808 The call stack (created by 

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

810 label : `str`, optional 

811 Event label for the history. 

812 

813 Notes 

814 ----- 

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

816 should not be called directly. 

817 """ 

818 if at is None: 

819 at = getCallStack() 

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

821 

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

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

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

825 

826 Parameters 

827 ---------- 

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

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

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

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

832 shortcut : `bool`, optional 

833 **Unused.** 

834 rtol : `float`, optional 

835 Relative tolerance for floating point comparisons. 

836 atol : `float`, optional 

837 Absolute tolerance for floating point comparisons. 

838 output : callable, optional 

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

840 report inequalities. 

841 

842 Notes 

843 ----- 

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

845 

846 See Also 

847 -------- 

848 lsst.pex.config.compareScalars 

849 """ 

850 v1 = getattr(instance1, self.name) 

851 v2 = getattr(instance2, self.name) 

852 name = getComparisonName( 

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

854 ) 

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

856 

857 

858class RecordingImporter: 

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

860 imported. 

861 

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

863 

864 Examples 

865 -------- 

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

867 when done: 

868 

869 >>> with RecordingImporter() as importer: 

870 ... # import stuff 

871 ... import numpy as np 

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

873 """ 

874 

875 def __init__(self): 

876 self._modules = set() 

877 

878 def __enter__(self): 

879 self.origMetaPath = sys.meta_path 

880 sys.meta_path = [self] + sys.meta_path # type: ignore 

881 return self 

882 

883 def __exit__(self, *args): 

884 self.uninstall() 

885 return False # Don't suppress exceptions 

886 

887 def uninstall(self): 

888 """Uninstall the importer.""" 

889 sys.meta_path = self.origMetaPath 

890 

891 def find_spec(self, fullname, path, target=None): 

892 """Find a module. 

893 

894 Called as part of the ``import`` chain of events. 

895 

896 Parameters 

897 ---------- 

898 fullname : `str` 

899 Name of module. 

900 path : `list` [`str`] 

901 Search path. Unused. 

902 target : `~typing.Any`, optional 

903 Unused. 

904 """ 

905 self._modules.add(fullname) 

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

907 return None 

908 

909 def getModules(self): 

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

911 

912 Returns 

913 ------- 

914 modules : `set` of `str` 

915 Set of imported module names. 

916 """ 

917 return self._modules 

918 

919 

920# type ignore because type checker thinks ConfigMeta is Generic when it is not 

921class Config(metaclass=ConfigMeta): # type: ignore 

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

923 

924 Notes 

925 ----- 

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

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

928 class behavior. 

929 

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

931 such as `keys`, `values`, and `items`. ``Config`` instances also support 

932 the ``in`` operator to test if a field is in the config. Unlike a `dict`, 

933 ``Config`` classes are not subscriptable. Instead, access individual 

934 fields as attributes of the configuration instance. 

935 

936 Examples 

937 -------- 

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

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

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

941 

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

943 >>> class DemoConfig(Config): 

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

945 ... listField = ListField( 

946 ... doc="List of favorite beverages.", 

947 ... dtype=str, 

948 ... default=["coffee", "green tea", "water"], 

949 ... ) 

950 >>> config = DemoConfig() 

951 

952 Configs support many `dict`-like APIs: 

953 

954 >>> config.keys() 

955 ['intField', 'listField'] 

956 >>> "intField" in config 

957 True 

958 

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

960 

961 >>> config.intField 

962 42 

963 >>> config.listField.append("earl grey tea") 

964 >>> print(config.listField) 

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

966 """ 

967 

968 _storage: dict[str, Any] 

969 _fields: dict[str, Field] 

970 _history: dict[str, list[Any]] 

971 _imports: set[Any] 

972 

973 def __iter__(self): 

974 """Iterate over fields.""" 

975 return self._fields.__iter__() 

976 

977 def keys(self): 

978 """Get field names. 

979 

980 Returns 

981 ------- 

982 names : `~collections.abc.KeysView` 

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

984 """ 

985 return self._storage.keys() 

986 

987 def values(self): 

988 """Get field values. 

989 

990 Returns 

991 ------- 

992 values : `~collections.abc.ValuesView` 

993 Iterator of field values. 

994 """ 

995 return self._storage.values() 

996 

997 def items(self): 

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

999 

1000 Returns 

1001 ------- 

1002 items : `~collections.abc.ItemsView` 

1003 Iterator of tuples for each configuration. Tuple items are: 

1004 

1005 0. Field name. 

1006 1. Field value. 

1007 """ 

1008 return self._storage.items() 

1009 

1010 def __contains__(self, name): 

1011 """Return `True` if the specified field exists in this config. 

1012 

1013 Parameters 

1014 ---------- 

1015 name : `str` 

1016 Field name to test for. 

1017 

1018 Returns 

1019 ------- 

1020 in : `bool` 

1021 `True` if the specified field exists in the config. 

1022 """ 

1023 return self._storage.__contains__(name) 

1024 

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

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

1027 

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

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

1030 some attributes are handled at allocation time rather than at 

1031 initialization. 

1032 

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

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

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

1036 """ 

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

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

1039 # remove __label and ignore it 

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

1041 

1042 instance = object.__new__(cls) 

1043 instance._frozen = False 

1044 instance._name = name 

1045 instance._storage = {} 

1046 instance._history = {} 

1047 instance._imports = set() 

1048 # load up defaults 

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

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

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

1052 # set custom default-overrides 

1053 instance.setDefaults() 

1054 # set constructor overrides 

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

1056 return instance 

1057 

1058 def __reduce__(self): 

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

1060 

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

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

1063 be pickled. 

1064 """ 

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

1066 # requires bytes 

1067 stream = io.StringIO() 

1068 self.saveToStream(stream) 

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

1070 

1071 def setDefaults(self): 

1072 """Subclass hook for computing defaults. 

1073 

1074 Notes 

1075 ----- 

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

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

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

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

1080 ``setDefaults``. 

1081 """ 

1082 pass 

1083 

1084 def update(self, **kw): 

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

1086 

1087 Parameters 

1088 ---------- 

1089 **kw 

1090 Keywords are configuration field names. Values are configuration 

1091 field values. 

1092 

1093 Notes 

1094 ----- 

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

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

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

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

1099 

1100 Examples 

1101 -------- 

1102 This is a config with three fields: 

1103 

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

1105 >>> class DemoConfig(Config): 

1106 ... fieldA = Field(doc="Field A", dtype=int, default=42) 

1107 ... fieldB = Field(doc="Field B", dtype=bool, default=True) 

1108 ... fieldC = Field(doc="Field C", dtype=str, default="Hello world") 

1109 >>> config = DemoConfig() 

1110 

1111 These are the default values of each field: 

1112 

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

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

1115 fieldA: 42 

1116 fieldB: True 

1117 fieldC: 'Hello world' 

1118 

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

1120 

1121 >>> config.update(fieldA=13, fieldC="Updated!") 

1122 

1123 Now the values of each field are: 

1124 

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

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

1127 fieldA: 13 

1128 fieldB: True 

1129 fieldC: 'Updated!' 

1130 """ 

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

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

1133 

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

1135 try: 

1136 field = self._fields[name] 

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

1138 except KeyError as e: 

1139 e.add_note(f"No field of name {name} exists in config type {_typeStr(self)}") 

1140 raise 

1141 

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

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

1144 configuration file. 

1145 

1146 Parameters 

1147 ---------- 

1148 filename : `str` 

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

1150 module. 

1151 root : `str`, optional 

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

1153 overridden. 

1154 

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

1156 contains:: 

1157 

1158 config.myField = 5 

1159 

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

1161 

1162 See Also 

1163 -------- 

1164 lsst.pex.config.Config.loadFromStream 

1165 lsst.pex.config.Config.loadFromString 

1166 lsst.pex.config.Config.save 

1167 lsst.pex.config.Config.saveToStream 

1168 lsst.pex.config.Config.saveToString 

1169 """ 

1170 with open(filename) as f: 

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

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

1173 

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

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

1176 provided stream. 

1177 

1178 Parameters 

1179 ---------- 

1180 stream : file-like object, `str`, `bytes`, or `~types.CodeType` 

1181 Stream containing configuration override code. If this is a 

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

1183 root : `str`, optional 

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

1185 overridden. 

1186 

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

1188 contains:: 

1189 

1190 config.myField = 5 

1191 

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

1193 filename : `str`, optional 

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

1195 in the stream. Used for error reporting. 

1196 extraLocals : `dict` of `str` to `object`, optional 

1197 Any extra variables to include in local scope when loading. 

1198 

1199 Notes 

1200 ----- 

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

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

1203 `loadFromString` instead for most of these types. 

1204 

1205 See Also 

1206 -------- 

1207 lsst.pex.config.Config.load 

1208 lsst.pex.config.Config.loadFromString 

1209 lsst.pex.config.Config.save 

1210 lsst.pex.config.Config.saveToStream 

1211 lsst.pex.config.Config.saveToString 

1212 """ 

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

1214 if filename is None: 

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

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

1217 else: 

1218 code = stream 

1219 self.loadFromString(code, root=root, filename=filename, extraLocals=extraLocals) 

1220 

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

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

1223 provided string. 

1224 

1225 Parameters 

1226 ---------- 

1227 code : `str`, `bytes`, or `~types.CodeType` 

1228 Stream containing configuration override code. 

1229 root : `str`, optional 

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

1231 overridden. 

1232 

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

1234 contains:: 

1235 

1236 config.myField = 5 

1237 

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

1239 filename : `str`, optional 

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

1241 in the stream. Used for error reporting. 

1242 extraLocals : `dict` of `str` to `object`, optional 

1243 Any extra variables to include in local scope when loading. 

1244 

1245 Raises 

1246 ------ 

1247 ValueError 

1248 Raised if a key in extraLocals is the same value as the value of 

1249 the root argument. 

1250 

1251 See Also 

1252 -------- 

1253 lsst.pex.config.Config.load 

1254 lsst.pex.config.Config.loadFromStream 

1255 lsst.pex.config.Config.save 

1256 lsst.pex.config.Config.saveToStream 

1257 lsst.pex.config.Config.saveToString 

1258 """ 

1259 if filename is None: 1259 ↛ 1263line 1259 didn't jump to line 1263 because the condition on line 1259 was always true

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

1261 # has attribute "co_filename", 

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

1263 with RecordingImporter() as importer: 

1264 globals = {"__file__": filename} 

1265 local = {root: self} 

1266 if extraLocals is not None: 1266 ↛ 1268line 1266 didn't jump to line 1268 because the condition on line 1266 was never true

1267 # verify the value of root was not passed as extra local args 

1268 if root in extraLocals: 

1269 raise ValueError( 

1270 f"{root} is reserved and cannot be used as a variable name in extraLocals" 

1271 ) 

1272 local.update(extraLocals) 

1273 exec(code, globals, local) 

1274 

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

1276 

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

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

1279 reproduces this config. 

1280 

1281 Parameters 

1282 ---------- 

1283 filename : `str` 

1284 Desination filename of this configuration. 

1285 root : `str`, optional 

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

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

1288 

1289 See Also 

1290 -------- 

1291 lsst.pex.config.Config.saveToStream 

1292 lsst.pex.config.Config.saveToString 

1293 lsst.pex.config.Config.load 

1294 lsst.pex.config.Config.loadFromStream 

1295 lsst.pex.config.Config.loadFromString 

1296 """ 

1297 d = os.path.dirname(filename) 

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

1299 self.saveToStream(outfile, root) 

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

1301 # for an explantion of these antics see: 

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

1303 umask = os.umask(0o077) 

1304 os.umask(umask) 

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

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

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

1308 # os.rename may not work across filesystems 

1309 shutil.move(outfile.name, filename) 

1310 

1311 def saveToString(self, skipImports=False): 

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

1313 string. 

1314 

1315 Parameters 

1316 ---------- 

1317 skipImports : `bool`, optional 

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

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

1320 additional clutter is not useful. 

1321 

1322 Returns 

1323 ------- 

1324 code : `str` 

1325 A code string readable by `loadFromString`. 

1326 

1327 See Also 

1328 -------- 

1329 lsst.pex.config.Config.save 

1330 lsst.pex.config.Config.saveToStream 

1331 lsst.pex.config.Config.load 

1332 lsst.pex.config.Config.loadFromStream 

1333 lsst.pex.config.Config.loadFromString 

1334 """ 

1335 buffer = io.StringIO() 

1336 self.saveToStream(buffer, skipImports=skipImports) 

1337 return buffer.getvalue() 

1338 

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

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

1341 reproduces this config. 

1342 

1343 Parameters 

1344 ---------- 

1345 outfile : file-like object 

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

1347 bytes. 

1348 root : `str`, optional 

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

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

1351 skipImports : `bool`, optional 

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

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

1354 additional clutter is not useful. 

1355 

1356 See Also 

1357 -------- 

1358 lsst.pex.config.Config.save 

1359 lsst.pex.config.Config.saveToString 

1360 lsst.pex.config.Config.load 

1361 lsst.pex.config.Config.loadFromStream 

1362 lsst.pex.config.Config.loadFromString 

1363 """ 

1364 tmp = self._name 

1365 self._rename(root) 

1366 try: 

1367 if not skipImports: 1367 ↛ 1384line 1367 didn't jump to line 1384 because the condition on line 1367 was always true

1368 self._collectImports() 

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

1370 self._imports.remove(self.__module__) 

1371 configType = type(self) 

1372 typeString = _typeStr(configType) 

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

1374 # We are required to write this on a single line because 

1375 # of later regex matching, rather than adopting black style 

1376 # formatting. 

1377 outfile.write( 

1378 f'assert type({root}) is {typeString}, f"config is of type ' 

1379 f'{{type({root}).__module__}}.{{type({root}).__name__}} instead of {typeString}"\n\n' 

1380 ) 

1381 for imp in sorted(self._imports): 1381 ↛ 1382line 1381 didn't jump to line 1382 because the loop on line 1381 never started

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

1383 outfile.write(f"import {imp}\n") 

1384 self._save(outfile) 

1385 finally: 

1386 self._rename(tmp) 

1387 

1388 def freeze(self): 

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

1390 self._frozen = True 

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

1392 field.freeze(self) 

1393 

1394 def _save(self, outfile): 

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

1396 

1397 Parameters 

1398 ---------- 

1399 outfile : file-like object 

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

1401 bytes. 

1402 """ 

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

1404 field.save(outfile, self) 

1405 

1406 def _collectImports(self): 

1407 """Add module containing self to the list of things to import and 

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

1409 collect method. 

1410 

1411 The field method will call _collectImports on any 

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

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

1414 class. 

1415 """ 

1416 self._imports.add(self.__module__) 

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

1418 field._collectImports(self, self._imports) 

1419 

1420 def toDict(self): 

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

1422 

1423 Returns 

1424 ------- 

1425 dict_ : `dict` 

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

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

1428 

1429 See Also 

1430 -------- 

1431 lsst.pex.config.Field.toDict 

1432 

1433 Notes 

1434 ----- 

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

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

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

1438 """ 

1439 dict_ = {} 

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

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

1442 return dict_ 

1443 

1444 def names(self): 

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

1446 

1447 Returns 

1448 ------- 

1449 names : `list` of `str` 

1450 Field names. 

1451 """ 

1452 # 

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

1454 # pre-existing saveToStream() 

1455 # 

1456 with io.StringIO() as strFd: 

1457 self.saveToStream(strFd, "config") 

1458 contents = strFd.getvalue() 

1459 strFd.close() 

1460 # 

1461 # Pull the names out of the dumped config 

1462 # 

1463 keys = [] 

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

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

1466 continue 

1467 

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

1469 if mat: 

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

1471 

1472 return keys 

1473 

1474 def _rename(self, name): 

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

1476 

1477 Parameters 

1478 ---------- 

1479 name : `str` 

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

1481 

1482 Notes 

1483 ----- 

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

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

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

1487 method for *this* method to work. 

1488 

1489 See Also 

1490 -------- 

1491 lsst.pex.config.Field.rename 

1492 """ 

1493 self._name = name 

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

1495 field.rename(self) 

1496 

1497 def validate(self): 

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

1499 

1500 Raises 

1501 ------ 

1502 lsst.pex.config.FieldValidationError 

1503 Raised if verification fails. 

1504 

1505 Notes 

1506 ----- 

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

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

1509 

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

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

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

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

1514 ``lsst.pex.config`` that handle recursing into subconfigs. 

1515 

1516 Inter-field relationships should only be checked in derived 

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

1518 validation is complete. 

1519 """ 

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

1521 field.validate(self) 

1522 

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

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

1525 

1526 Parameters 

1527 ---------- 

1528 name : `str` 

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

1530 **kwargs 

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

1532 

1533 Returns 

1534 ------- 

1535 history : `str` 

1536 A string containing the formatted history. 

1537 

1538 See Also 

1539 -------- 

1540 lsst.pex.config.history.format 

1541 """ 

1542 import lsst.pex.config.history as pexHist 

1543 

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

1545 

1546 history = property(lambda x: x._history) 

1547 """Read-only history. 

1548 """ 

1549 

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

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

1552 

1553 Notes 

1554 ----- 

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

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

1557 to them dynamically. 

1558 

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

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

1561 non-existent field. 

1562 """ 

1563 if attr in self._fields: 

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

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

1566 warnings.warn( 

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

1568 FutureWarning, 

1569 stacklevel=2, 

1570 ) 

1571 if at is None: 1571 ↛ 1574line 1571 didn't jump to line 1574 because the condition on line 1571 was always true

1572 at = getCallStack() 

1573 # This allows Field descriptors to work. 

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

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

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

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

1578 elif attr in self.__dict__ or attr in ("_name", "_history", "_storage", "_frozen", "_imports"): 1578 ↛ 1583line 1578 didn't jump to line 1583 because the condition on line 1578 was always true

1579 # This allows specific private attributes to work. 

1580 self.__dict__[attr] = value 

1581 else: 

1582 # We throw everything else. 

1583 raise AttributeError(f"{_typeStr(self)} has no attribute {attr}") 

1584 

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

1586 if attr in self._fields: 

1587 if at is None: 

1588 at = getCallStack() 

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

1590 else: 

1591 object.__delattr__(self, attr) 

1592 

1593 def __eq__(self, other): 

1594 if type(other) is type(self): 1594 ↛ 1595line 1594 didn't jump to line 1595 because the condition on line 1594 was never true

1595 for name in self._fields: 

1596 thisValue = getattr(self, name) 

1597 otherValue = getattr(other, name) 

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

1599 if not math.isnan(otherValue): 

1600 return False 

1601 elif thisValue != otherValue: 

1602 return False 

1603 return True 

1604 return False 

1605 

1606 def __ne__(self, other): 

1607 return not self.__eq__(other) 

1608 

1609 def __str__(self): 

1610 return str(self.toDict()) 

1611 

1612 def __repr__(self): 

1613 return "{}({})".format( 

1614 _typeStr(self), 

1615 ", ".join(f"{k}={v!r}" for k, v in self.toDict().items() if v is not None), 

1616 ) 

1617 

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

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

1620 equality. 

1621 

1622 Parameters 

1623 ---------- 

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

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

1626 config. 

1627 shortcut : `bool`, optional 

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

1629 `True`. 

1630 rtol : `float`, optional 

1631 Relative tolerance for floating point comparisons. 

1632 atol : `float`, optional 

1633 Absolute tolerance for floating point comparisons. 

1634 output : callable, optional 

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

1636 report inequalities. 

1637 

1638 Returns 

1639 ------- 

1640 isEqual : `bool` 

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

1642 `False` if there is an inequality. 

1643 

1644 See Also 

1645 -------- 

1646 lsst.pex.config.compareConfigs 

1647 

1648 Notes 

1649 ----- 

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

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

1652 are not considered by this method. 

1653 

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

1655 """ 

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

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

1658 name = getComparisonName(name1, name2) 

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

1660 

1661 @classmethod 

1662 def __init_subclass__(cls, **kwargs): 

1663 """Run initialization for every subclass. 

1664 

1665 Specifically registers the subclass with a YAML representer 

1666 and YAML constructor (if pyyaml is available) 

1667 """ 

1668 super().__init_subclass__(**kwargs) 

1669 

1670 if not yaml: 1670 ↛ 1671line 1670 didn't jump to line 1671 because the condition on line 1670 was never true

1671 return 

1672 

1673 yaml.add_representer(cls, _yaml_config_representer) 

1674 

1675 @classmethod 

1676 def _fromPython(cls, config_py): 

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

1678 

1679 Parameters 

1680 ---------- 

1681 config_py : `str` 

1682 A serialized form of the Config as created by 

1683 `Config.saveToStream`. 

1684 

1685 Returns 

1686 ------- 

1687 config : `Config` 

1688 Reconstructed `Config` instant. 

1689 """ 

1690 cls = _classFromPython(config_py) 

1691 return unreduceConfig(cls, config_py) 

1692 

1693 

1694def _classFromPython(config_py): 

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

1696 

1697 Parameters 

1698 ---------- 

1699 config_py : `str` 

1700 A serialized form of the Config as created by 

1701 `Config.saveToStream`. 

1702 

1703 Returns 

1704 ------- 

1705 cls : `type` 

1706 The `Config` subclass associated with this config. 

1707 """ 

1708 # standard serialization has the form: 

1709 # import config.class 

1710 # assert type(config) is config.class.Config, ... 

1711 # Older files use "type(config)==" instead. 

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

1713 

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

1715 # large config into separate lines. 

1716 # The assert regex cannot be greedy because the assert error string 

1717 # can include both "," and " is ". 

1718 matches = re.search(r"^import ([\w.]+)\nassert type\(\S+\)(?:\s*==\s*| is )(.*?),", config_py) 

1719 

1720 if not matches: 

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

1722 raise ValueError( 

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

1724 ) 

1725 

1726 module_name = matches.group(1) 

1727 module = importlib.import_module(module_name) 

1728 

1729 # Second line 

1730 full_name = matches.group(2) 

1731 

1732 # Remove the module name from the full name 

1733 if not full_name.startswith(module_name): 

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

1735 

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

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

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

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

1740 components = remainder.split(".") 

1741 pytype = module 

1742 for component in components: 

1743 pytype = getattr(pytype, component) 

1744 return pytype 

1745 

1746 

1747def unreduceConfig(cls_, stream): 

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

1749 

1750 Parameters 

1751 ---------- 

1752 cls_ : `lsst.pex.config.Config`-type 

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

1754 with configurations in the ``stream``. 

1755 stream : file-like object, `str`, or `~types.CodeType` 

1756 Stream containing configuration override code. 

1757 

1758 Returns 

1759 ------- 

1760 config : `lsst.pex.config.Config` 

1761 Config instance. 

1762 

1763 See Also 

1764 -------- 

1765 lsst.pex.config.Config.loadFromStream 

1766 """ 

1767 config = cls_() 

1768 config.loadFromStream(stream) 

1769 return config