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

500 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-15 06:24 -0300

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 logging 

42import math 

43import numbers 

44import os 

45import re 

46import shutil 

47import sys 

48import tempfile 

49import warnings 

50from collections.abc import Mapping 

51from contextlib import contextmanager 

52from contextvars import ContextVar 

53from types import GenericAlias 

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

55 

56from lsst.resources import ResourcePath, ResourcePathExpression 

57 

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

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

60try: 

61 import yaml 

62except ImportError: 

63 yaml = None 

64 

65from .callStack import getCallStack, getStackFrame 

66from .comparison import compareConfigs, compareScalars, getComparisonName 

67 

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

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

70 

71 try: 

72 # CLoader is not always available 

73 from yaml import CLoader 

74 

75 YamlLoaders += (CLoader,) 

76 except ImportError: 

77 pass 

78else: 

79 YamlLoaders = () 

80 doImport = None 

81 

82_LOG = logging.getLogger(__name__) 

83 

84 

85# Tracks the current config directory for the current context. 

86_config_dir_stack: ContextVar[ResourcePath | None] = ContextVar("_config_dir_stack", default=None) 

87 

88 

89def _get_config_root() -> ResourcePath | None: 

90 return _config_dir_stack.get() 

91 

92 

93@contextmanager 

94def _push_config_root(dirname: ResourcePath): 

95 token = _config_dir_stack.set(dirname) 

96 try: 

97 yield 

98 finally: 

99 _config_dir_stack.reset(token) 

100 

101 

102class _PexConfigGenericAlias(GenericAlias): 

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

104 Generics. 

105 

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

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

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

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

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

111 subclass differs from the base class implementation. 

112 

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

114 Field's `__class_getitem__` method. 

115 """ 

116 

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

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

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

120 

121 

122FieldTypeVar = TypeVar("FieldTypeVar") 

123 

124 

125class UnexpectedProxyUsageError(TypeError): 

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

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

128 """ 

129 

130 

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

132 """Generate nested configuration names.""" 

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

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

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

136 name = prefix 

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

138 name = prefix + "." + name 

139 

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

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

142 else: 

143 return name 

144 

145 

146def _autocast(x, dtype): 

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

148 

149 Parameters 

150 ---------- 

151 x : object 

152 A value. 

153 dtype : type 

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

155 

156 Returns 

157 ------- 

158 values : object 

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

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

161 ``x`` is returned. 

162 

163 Notes 

164 ----- 

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

166 """ 

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

168 return float(x) 

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

170 return int(x) 

171 return x 

172 

173 

174def _typeStr(x): 

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

176 

177 Returns 

178 ------- 

179 `str` 

180 Fully-qualified type name. 

181 

182 Notes 

183 ----- 

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

185 later upon with the 'load' function. 

186 """ 

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

188 xtype = x 

189 else: 

190 xtype = type(x) 

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

192 return xtype.__name__ 

193 else: 

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

195 

196 

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

198 

199 def _yaml_config_representer(dumper, data): 

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

201 

202 Stores the serialized stream as a scalar block string. 

203 """ 

204 stream = io.StringIO() 

205 data.saveToStream(stream) 

206 config_py = stream.getvalue() 

207 

208 # Strip multiple newlines from the end of the config 

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

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

211 

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

213 # Remove the trailing spaces so it has no choice 

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

215 

216 # Store the Python as a simple scalar 

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

218 

219 def _yaml_config_constructor(loader, node): 

220 """Construct a config from YAML.""" 

221 config_py = loader.construct_scalar(node) 

222 return Config._fromPython(config_py) 

223 

224 # Register a generic constructor for Config and all subclasses 

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

226 for loader in YamlLoaders: 

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

228 

229 

230class ConfigMeta(type): 

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

232 

233 Parameters 

234 ---------- 

235 name : `str` 

236 Name to use for class. 

237 bases : `~collections.abc.Iterable` 

238 Base classes. 

239 dict_ : `dict` 

240 Additional parameters. 

241 

242 Notes 

243 ----- 

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

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

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

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

248 """ 

249 

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

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

252 cls._fields = {} 

253 cls._source = getStackFrame() 

254 

255 def getFields(classtype): 

256 fields = {} 

257 bases = list(classtype.__bases__) 

258 bases.reverse() 

259 for b in bases: 

260 fields.update(getFields(b)) 

261 

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

263 if isinstance(v, Field): 

264 fields[k] = v 

265 return fields 

266 

267 fields = getFields(cls) 

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

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

270 

271 def __setattr__(cls, name, value): 

272 if isinstance(value, Field): 

273 value.name = name 

274 cls._fields[name] = value 

275 type.__setattr__(cls, name, value) 

276 

277 

278class FieldValidationError(ValueError): 

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

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

281 

282 Parameters 

283 ---------- 

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

285 The field that was not valid. 

286 config : `lsst.pex.config.Config` 

287 The config containing the invalid field. 

288 msg : `str` 

289 Text describing why the field was not valid. 

290 """ 

291 

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

293 self.fieldType = type(field) 

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

295 """ 

296 

297 self.fieldName = field.name 

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

299 error (`str`). 

300 

301 See also 

302 -------- 

303 lsst.pex.config.Field.name 

304 """ 

305 

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

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

308 (`str`). 

309 """ 

310 

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

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

313 instance. 

314 """ 

315 

316 self.fieldSource = field.source 

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

318 """ 

319 

320 self.configSource = config._source 

321 error = ( 

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

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

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

325 ) 

326 super().__init__(error) 

327 

328 

329class Field(Generic[FieldTypeVar]): 

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

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

332 

333 Parameters 

334 ---------- 

335 doc : `str` 

336 A description of the field for users. 

337 dtype : type, optional 

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

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

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

341 the class. 

342 default : object, optional 

343 The field's default value. 

344 check : callable, optional 

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

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

347 validation can be written as part of the 

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

349 optional : `bool`, optional 

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

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

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

353 deprecated : None or `str`, optional 

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

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

356 

357 Raises 

358 ------ 

359 ValueError 

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

361 (see `Field.supportedTypes`). 

362 

363 See Also 

364 -------- 

365 ChoiceField 

366 ConfigChoiceField 

367 ConfigDictField 

368 ConfigField 

369 ConfigurableField 

370 DictField 

371 ListField 

372 RangeField 

373 RegistryField 

374 

375 Notes 

376 ----- 

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

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

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

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

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

382 `Field` attributes are `descriptors 

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

384 

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

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

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

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

389 type. See the example, below. 

390 

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

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

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

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

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

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

397 specify it as an argument during instantiation. 

398 

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

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

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

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

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

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

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

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

407 

408 Examples 

409 -------- 

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

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

412 

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

414 >>> class Example(Config): 

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

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

417 >>> print(config.myInt) 

418 0 

419 >>> config.myInt = 5 

420 >>> print(config.myInt) 

421 5 

422 """ 

423 

424 name: str 

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

426 Class. 

427 """ 

428 

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

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

431 """ 

432 

433 @staticmethod 

434 def _parseTypingArgs( 

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

436 ) -> Mapping[str, Any]: 

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

438 

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

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

441 

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

443 handle turning type parameters into keyword arguments (see DictField 

444 for an example) 

445 

446 Parameters 

447 ---------- 

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

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

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

451 and will be treated as such. 

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

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

454 Field constructor. 

455 

456 Returns 

457 ------- 

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

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

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

461 from the input parameters. 

462 

463 Raises 

464 ------ 

465 ValueError 

466 Raised if params is of incorrect length. 

467 Raised if a forward reference could not be resolved 

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

469 """ 

470 if len(params) > 1: 

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

472 unpackedParams = params[0] 

473 if isinstance(unpackedParams, str): 

474 _typ = ForwardRef(unpackedParams) 

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

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

477 # 3.9+ takes 3 args. 

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

479 if result is None: 

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

481 unpackedParams = cast(type, result) 

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

483 raise ValueError("Conflicting definition for dtype") 

484 elif "dtype" not in kwds: 

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

486 return kwds 

487 

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

489 return _PexConfigGenericAlias(cls, params) 

490 

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

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

493 raise ValueError( 

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

495 ) 

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

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

498 

499 source = getStackFrame() 

500 self._setup( 

501 doc=doc, 

502 dtype=dtype, 

503 default=default, 

504 check=check, 

505 optional=optional, 

506 source=source, 

507 deprecated=deprecated, 

508 ) 

509 

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

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

512 self.dtype = dtype 

513 """Data type for the field. 

514 """ 

515 

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

517 raise ValueError("Docstring is empty.") 

518 

519 # append the deprecation message to the docstring. 

520 if deprecated is not None: 

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

522 self.doc = doc 

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

524 """ 

525 

526 self.deprecated = deprecated 

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

528 """ 

529 

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

531 if optional or default is not None: 

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

533 self.__doc__ += ")" 

534 

535 self.default = default 

536 """Default value for this field. 

537 """ 

538 

539 self.check = check 

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

541 """ 

542 

543 self.optional = optional 

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

545 

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

547 field's value is `None`. 

548 """ 

549 

550 self.source = source 

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

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

553 """ 

554 

555 def rename(self, instance): 

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

557 only). 

558 

559 Parameters 

560 ---------- 

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

562 The config instance that contains this field. 

563 

564 Notes 

565 ----- 

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

567 contains this field and should not be called directly. 

568 

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

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

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

572 `lsst.pex.config.config._joinNamePath`. 

573 """ 

574 pass 

575 

576 def validate(self, instance): 

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

578 

579 Parameters 

580 ---------- 

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

582 The config instance that contains this field. 

583 

584 Raises 

585 ------ 

586 lsst.pex.config.FieldValidationError 

587 Raised if verification fails. 

588 

589 Notes 

590 ----- 

591 This method provides basic validation: 

592 

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

594 - Ensures type correctness. 

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

596 

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

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

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

600 """ 

601 value = self.__get__(instance) 

602 if not self.optional and value is None: 

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

604 

605 def freeze(self, instance): 

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

607 

608 Parameters 

609 ---------- 

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

611 The config instance that contains this field. 

612 

613 Notes 

614 ----- 

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

616 hold subconfigs should freeze each subconfig. 

617 

618 **Subclasses should implement this method.** 

619 """ 

620 pass 

621 

622 def _validateValue(self, value): 

623 """Validate a value. 

624 

625 Parameters 

626 ---------- 

627 value : object 

628 The value being validated. 

629 

630 Raises 

631 ------ 

632 TypeError 

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

634 ``dtype``. 

635 ValueError 

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

637 """ 

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

639 return 

640 

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

642 msg = ( 

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

644 ) 

645 raise TypeError(msg) 

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

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

648 raise ValueError(msg) 

649 

650 def _collectImports(self, instance, imports): 

651 """Call the _collectImports method on all config 

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

653 set. 

654 

655 Parameters 

656 ---------- 

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

658 A config object that has this field defined on it 

659 imports : `set` 

660 Set of python modules that need imported after persistence 

661 """ 

662 pass 

663 

664 def save(self, outfile, instance): 

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

666 

667 Parameters 

668 ---------- 

669 outfile : file-like object 

670 A writeable field handle. 

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

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

673 

674 Notes 

675 ----- 

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

677 contains this field and should not be called directly. 

678 

679 The output consists of the documentation string 

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

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

682 

683 This output can be executed with Python. 

684 """ 

685 value = self.__get__(instance) 

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

687 

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

689 return 

690 

691 # write full documentation string as comment lines 

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

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

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

695 # non-finite numbers need special care 

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

697 else: 

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

699 

700 def toDict(self, instance): 

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

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

703 

704 Parameters 

705 ---------- 

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

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

708 

709 Returns 

710 ------- 

711 value : object 

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

713 

714 Notes 

715 ----- 

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

717 should not be called directly. 

718 

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

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

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

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

723 the field values in the subconfig. 

724 """ 

725 return self.__get__(instance) 

726 

727 def _copy_storage(self, old: Config, new: Config) -> Any: 

728 """Copy the storage for this field in the given field into an object 

729 suitable for storage in a new copy of that config. 

730 

731 Any frozen storage should be unfrozen. 

732 """ 

733 return copy.deepcopy(old._storage[self.name]) 

734 

735 @overload 

736 def __get__( 

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

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

739 

740 @overload 

741 def __get__( 

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

743 ) -> FieldTypeVar: ... 

744 

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

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

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

748 directly. 

749 

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

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

752 Config classes. 

753 

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

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

756 returned. 

757 """ 

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

759 return self 

760 else: 

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

762 try: 

763 return instance._storage[self.name] 

764 except AttributeError: 

765 if not isinstance(instance, Config): 

766 return self 

767 else: 

768 raise AttributeError( 

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

770 ) from None 

771 

772 def __set__( 

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

774 ) -> None: 

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

776 

777 Parameters 

778 ---------- 

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

780 The config instance that contains this field. 

781 value : obj 

782 Value to set on this field. 

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

784 optional 

785 The call stack (created by 

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

787 label : `str`, optional 

788 Event label for the history. 

789 

790 Notes 

791 ----- 

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

793 and should not be called directly. 

794 

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

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

797 should follow the following rules: 

798 

799 - Do not allow modification of frozen configs. 

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

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

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

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

804 invalid values. 

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

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

807 

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

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

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

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

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

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

814 reimplementation. 

815 """ 

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

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

818 

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

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

821 value = _autocast(value, self.dtype) 

822 try: 

823 self._validateValue(value) 

824 except BaseException as e: 

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

826 

827 instance._storage[self.name] = value 

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

829 at = getCallStack() 

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

831 

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

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

834 

835 Parameters 

836 ---------- 

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

838 The config instance that contains this field. 

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

840 The call stack (created by 

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

842 label : `str`, optional 

843 Event label for the history. 

844 

845 Notes 

846 ----- 

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

848 should not be called directly. 

849 """ 

850 if at is None: 

851 at = getCallStack() 

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

853 

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

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

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

857 

858 Parameters 

859 ---------- 

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

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

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

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

864 shortcut : `bool`, optional 

865 **Unused.** 

866 rtol : `float`, optional 

867 Relative tolerance for floating point comparisons. 

868 atol : `float`, optional 

869 Absolute tolerance for floating point comparisons. 

870 output : callable, optional 

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

872 report inequalities. 

873 

874 Notes 

875 ----- 

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

877 

878 See Also 

879 -------- 

880 lsst.pex.config.compareScalars 

881 """ 

882 v1 = getattr(instance1, self.name) 

883 v2 = getattr(instance2, self.name) 

884 name = getComparisonName( 

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

886 ) 

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

888 

889 

890class RecordingImporter: 

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

892 imported. 

893 

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

895 

896 Examples 

897 -------- 

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

899 when done: 

900 

901 >>> with RecordingImporter() as importer: 

902 ... # import stuff 

903 ... import numpy as np 

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

905 """ 

906 

907 def __init__(self): 

908 self._modules = set() 

909 

910 def __enter__(self): 

911 self.origMetaPath = sys.meta_path 

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

913 return self 

914 

915 def __exit__(self, *args): 

916 self.uninstall() 

917 return False # Don't suppress exceptions 

918 

919 def uninstall(self): 

920 """Uninstall the importer.""" 

921 sys.meta_path = self.origMetaPath 

922 

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

924 """Find a module. 

925 

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

927 

928 Parameters 

929 ---------- 

930 fullname : `str` 

931 Name of module. 

932 path : `list` [`str`] 

933 Search path. Unused. 

934 target : `~typing.Any`, optional 

935 Unused. 

936 """ 

937 self._modules.add(fullname) 

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

939 return None 

940 

941 def getModules(self): 

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

943 

944 Returns 

945 ------- 

946 modules : `set` of `str` 

947 Set of imported module names. 

948 """ 

949 return self._modules 

950 

951 

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

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

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

955 

956 Notes 

957 ----- 

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

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

960 class behavior. 

961 

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

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

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

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

966 fields as attributes of the configuration instance. 

967 

968 Examples 

969 -------- 

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

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

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

973 

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

975 >>> class DemoConfig(Config): 

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

977 ... listField = ListField( 

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

979 ... dtype=str, 

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

981 ... ) 

982 >>> config = DemoConfig() 

983 

984 Configs support many `dict`-like APIs: 

985 

986 >>> config.keys() 

987 ['intField', 'listField'] 

988 >>> "intField" in config 

989 True 

990 

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

992 

993 >>> config.intField 

994 42 

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

996 >>> print(config.listField) 

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

998 """ 

999 

1000 _storage: dict[str, Any] 

1001 _fields: dict[str, Field] 

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

1003 _imports: set[Any] 

1004 

1005 def __iter__(self): 

1006 """Iterate over fields.""" 

1007 return self._fields.__iter__() 

1008 

1009 def keys(self): 

1010 """Get field names. 

1011 

1012 Returns 

1013 ------- 

1014 names : `~collections.abc.KeysView` 

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

1016 """ 

1017 return self._storage.keys() 

1018 

1019 def values(self): 

1020 """Get field values. 

1021 

1022 Returns 

1023 ------- 

1024 values : `~collections.abc.ValuesView` 

1025 Iterator of field values. 

1026 """ 

1027 return self._storage.values() 

1028 

1029 def items(self): 

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

1031 

1032 Returns 

1033 ------- 

1034 items : `~collections.abc.ItemsView` 

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

1036 

1037 0. Field name. 

1038 1. Field value. 

1039 """ 

1040 return self._storage.items() 

1041 

1042 def __contains__(self, name): 

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

1044 

1045 Parameters 

1046 ---------- 

1047 name : `str` 

1048 Field name to test for. 

1049 

1050 Returns 

1051 ------- 

1052 in : `bool` 

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

1054 """ 

1055 return self._storage.__contains__(name) 

1056 

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

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

1059 

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

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

1062 some attributes are handled at allocation time rather than at 

1063 initialization. 

1064 

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

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

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

1068 """ 

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

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

1071 # remove __label and ignore it 

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

1073 

1074 instance = object.__new__(cls) 

1075 instance._frozen = False 

1076 instance._name = name 

1077 instance._storage = {} 

1078 instance._history = {} 

1079 instance._imports = set() 

1080 # load up defaults 

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

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

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

1084 # set custom default-overrides 

1085 instance.setDefaults() 

1086 # set constructor overrides 

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

1088 return instance 

1089 

1090 def copy(self) -> Config: 

1091 """Return a deep copy of this config. 

1092 

1093 Notes 

1094 ----- 

1095 The returned config object is not frozen, even if the original was. 

1096 If a nested config object is copied, it retains the name from its 

1097 original hierarchy. 

1098 

1099 Nested objects are only shared between the new and old configs if they 

1100 are not possible to modify via the config's interfaces (e.g. entries 

1101 in the the history list are not copied, but the lists themselves are, 

1102 so modifications to one copy do not modify the other). 

1103 """ 

1104 instance = object.__new__(type(self)) 

1105 instance._frozen = False 

1106 instance._name = self._name 

1107 instance._history = {k: list(v) for k, v in self._history.items()} 

1108 instance._imports = set(self._imports) 

1109 # Important to set up storage last, since fields sometimes store 

1110 # proxy objects that reference their parent (especially for history). 

1111 instance._storage = {k: self._fields[k]._copy_storage(self, instance) for k in self._storage} 

1112 return instance 

1113 

1114 def __reduce__(self): 

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

1116 

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

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

1119 be pickled. 

1120 """ 

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

1122 # requires bytes 

1123 stream = io.StringIO() 

1124 self.saveToStream(stream) 

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

1126 

1127 def setDefaults(self): 

1128 """Subclass hook for computing defaults. 

1129 

1130 Notes 

1131 ----- 

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

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

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

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

1136 ``setDefaults``. 

1137 """ 

1138 pass 

1139 

1140 def update(self, **kw): 

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

1142 

1143 Parameters 

1144 ---------- 

1145 **kw 

1146 Keywords are configuration field names. Values are configuration 

1147 field values. 

1148 

1149 Notes 

1150 ----- 

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

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

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

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

1155 

1156 Examples 

1157 -------- 

1158 This is a config with three fields: 

1159 

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

1161 >>> class DemoConfig(Config): 

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

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

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

1165 >>> config = DemoConfig() 

1166 

1167 These are the default values of each field: 

1168 

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

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

1171 fieldA: 42 

1172 fieldB: True 

1173 fieldC: 'Hello world' 

1174 

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

1176 

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

1178 

1179 Now the values of each field are: 

1180 

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

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

1183 fieldA: 13 

1184 fieldB: True 

1185 fieldC: 'Updated!' 

1186 """ 

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

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

1189 

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

1191 try: 

1192 field = self._fields[name] 

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

1194 except KeyError as e: 

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

1196 raise 

1197 

1198 def _filename_to_resource( 

1199 self, filename: ResourcePathExpression | None = None 

1200 ) -> tuple[ResourcePath | None, str]: 

1201 """Create resource path from filename. 

1202 

1203 Parameters 

1204 ---------- 

1205 filename : `lsst.resources.ResourcePathExpression` or `None` 

1206 The URI expression associated with this config. Can be `None` 

1207 if no file URI is known. 

1208 

1209 Returns 

1210 ------- 

1211 resource : `lsst.resources.ResourcePath` or `None` 

1212 The resource version of the filename. Returns `None` if no filename 

1213 was given or refers to unspecified value. 

1214 file_string : `str` 

1215 String form of the resource for use in ``__file__`` 

1216 """ 

1217 if filename is None or filename in ("?", "<unknown>"): 1217 ↛ 1219line 1217 didn't jump to line 1219 because the condition on line 1217 was always true

1218 return None, "<unknown>" 

1219 base = _get_config_root() 

1220 resource = ResourcePath(filename, forceAbsolute=True, forceDirectory=False, root=base) 

1221 

1222 # Preferred definition of __file__ is the full OS path. If a config 

1223 # is loaded with a relative path it must be converted to the absolute 

1224 # path to avoid confusion with later relative paths referenced inside 

1225 # the config. 

1226 if resource.scheme == "file": 

1227 file_string = resource.ospath 

1228 else: 

1229 file_string = str(resource) 

1230 

1231 return resource, file_string 

1232 

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

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

1235 configuration file. 

1236 

1237 Parameters 

1238 ---------- 

1239 filename : `lsst.resources.ResourcePathExpression` 

1240 Name of the configuration URI. A configuration file is a Python 

1241 module. Since configuration files are Python code, remote URIs 

1242 are not allowed. 

1243 root : `str`, optional 

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

1245 overridden. 

1246 

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

1248 contains:: 

1249 

1250 config.myField = 5 

1251 

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

1253 

1254 See Also 

1255 -------- 

1256 lsst.pex.config.Config.loadFromStream 

1257 lsst.pex.config.Config.loadFromString 

1258 lsst.pex.config.Config.save 

1259 lsst.pex.config.Config.saveToStream 

1260 lsst.pex.config.Config.saveToString 

1261 """ 

1262 resource, file_string = self._filename_to_resource(filename) 

1263 if resource is None: 

1264 # A filename is required. 

1265 raise ValueError(f"Undefined URI provided to load command: {filename}.") 

1266 

1267 if resource.scheme not in ("file", "eups", "resource"): 

1268 raise ValueError(f"Remote URI ({resource}) can not be used to load configurations.") 

1269 

1270 # Push the directory of the file we are now reading onto the stack 

1271 # so that nested loads are relative to this file. 

1272 with _push_config_root(resource.dirname()): 

1273 _LOG.debug("Updating config from URI %s", str(resource)) 

1274 with resource.open("r") as f: 

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

1276 self._loadFromString(code, root=root, filename=file_string) 

1277 

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

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

1280 provided stream. 

1281 

1282 Parameters 

1283 ---------- 

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

1285 Stream containing configuration override code. If this is a 

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

1287 root : `str`, optional 

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

1289 overridden. 

1290 

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

1292 contains:: 

1293 

1294 config.myField = 5 

1295 

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

1297 filename : `str`, optional 

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

1299 in the stream. Used for error reporting and to set ``__file__`` 

1300 variable in config. 

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

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

1303 

1304 Notes 

1305 ----- 

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

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

1308 `loadFromString` instead for most of these types. 

1309 

1310 See Also 

1311 -------- 

1312 lsst.pex.config.Config.load 

1313 lsst.pex.config.Config.loadFromString 

1314 lsst.pex.config.Config.save 

1315 lsst.pex.config.Config.saveToStream 

1316 lsst.pex.config.Config.saveToString 

1317 """ 

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

1319 if filename is None: 

1320 filename = getattr(stream, "name", "<unknown>") 

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

1322 else: 

1323 code = stream 

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

1325 

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

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

1328 provided string. 

1329 

1330 Parameters 

1331 ---------- 

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

1333 Stream containing configuration override code. 

1334 root : `str`, optional 

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

1336 overridden. 

1337 

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

1339 contains:: 

1340 

1341 config.myField = 5 

1342 

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

1344 filename : `lsst.resources.ResourcePathExpression`, optional 

1345 URI of the configuration file, or `None` if unknown or contained 

1346 in the stream. Used for error reporting and to set ``__file__`` 

1347 variable. Required to be set if the string config attempts to 

1348 load other configs using either relative path or ``__file__``. 

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

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

1351 

1352 Raises 

1353 ------ 

1354 ValueError 

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

1356 the root argument. 

1357 

1358 See Also 

1359 -------- 

1360 lsst.pex.config.Config.load 

1361 lsst.pex.config.Config.loadFromStream 

1362 lsst.pex.config.Config.save 

1363 lsst.pex.config.Config.saveToStream 

1364 lsst.pex.config.Config.saveToString 

1365 """ 

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

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

1368 # has attribute "co_filename", 

1369 filename = getattr(code, "co_filename", "<unknown>") 

1370 

1371 resource, file_string = self._filename_to_resource(filename) 

1372 if resource is None: 1372 ↛ 1379line 1372 didn't jump to line 1379 because the condition on line 1372 was always true

1373 # No idea where this config came from so no ability to deal with 

1374 # relative paths. No reason to use context. 

1375 self._loadFromString(code, root=root, filename=filename, extraLocals=extraLocals) 

1376 else: 

1377 # Push the directory of the file we are now reading onto the stack 

1378 # so that nested loads are relative to this file. 

1379 with _push_config_root(resource.dirname()): 

1380 self._loadFromString(code, root=root, filename=file_string, extraLocals=extraLocals) 

1381 

1382 def _loadFromString(self, code, root="config", filename=None, extraLocals=None): 

1383 """Update config from string. 

1384 

1385 Assumes relative directory path context has been setup by caller. 

1386 """ 

1387 with RecordingImporter() as importer: 

1388 globals = {"__file__": filename} 

1389 local = {root: self} 

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

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

1392 if root in extraLocals: 

1393 raise ValueError( 

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

1395 ) 

1396 local.update(extraLocals) 

1397 exec(code, globals, local) 

1398 

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

1400 

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

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

1403 reproduces this config. 

1404 

1405 Parameters 

1406 ---------- 

1407 filename : `str` 

1408 Desination filename of this configuration. 

1409 root : `str`, optional 

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

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

1412 

1413 See Also 

1414 -------- 

1415 lsst.pex.config.Config.saveToStream 

1416 lsst.pex.config.Config.saveToString 

1417 lsst.pex.config.Config.load 

1418 lsst.pex.config.Config.loadFromStream 

1419 lsst.pex.config.Config.loadFromString 

1420 """ 

1421 d = os.path.dirname(filename) 

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

1423 self.saveToStream(outfile, root) 

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

1425 # for an explantion of these antics see: 

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

1427 umask = os.umask(0o077) 

1428 os.umask(umask) 

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

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

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

1432 # os.rename may not work across filesystems 

1433 shutil.move(outfile.name, filename) 

1434 

1435 def saveToString(self, skipImports=False): 

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

1437 string. 

1438 

1439 Parameters 

1440 ---------- 

1441 skipImports : `bool`, optional 

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

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

1444 additional clutter is not useful. 

1445 

1446 Returns 

1447 ------- 

1448 code : `str` 

1449 A code string readable by `loadFromString`. 

1450 

1451 See Also 

1452 -------- 

1453 lsst.pex.config.Config.save 

1454 lsst.pex.config.Config.saveToStream 

1455 lsst.pex.config.Config.load 

1456 lsst.pex.config.Config.loadFromStream 

1457 lsst.pex.config.Config.loadFromString 

1458 """ 

1459 buffer = io.StringIO() 

1460 self.saveToStream(buffer, skipImports=skipImports) 

1461 return buffer.getvalue() 

1462 

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

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

1465 reproduces this config. 

1466 

1467 Parameters 

1468 ---------- 

1469 outfile : file-like object 

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

1471 bytes. 

1472 root : `str`, optional 

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

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

1475 skipImports : `bool`, optional 

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

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

1478 additional clutter is not useful. 

1479 

1480 See Also 

1481 -------- 

1482 lsst.pex.config.Config.save 

1483 lsst.pex.config.Config.saveToString 

1484 lsst.pex.config.Config.load 

1485 lsst.pex.config.Config.loadFromStream 

1486 lsst.pex.config.Config.loadFromString 

1487 """ 

1488 tmp = self._name 

1489 self._rename(root) 

1490 try: 

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

1492 self._collectImports() 

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

1494 self._imports.remove(self.__module__) 

1495 configType = type(self) 

1496 typeString = _typeStr(configType) 

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

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

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

1500 # formatting. 

1501 outfile.write( 

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

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

1504 ) 

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

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

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

1508 self._save(outfile) 

1509 finally: 

1510 self._rename(tmp) 

1511 

1512 def freeze(self): 

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

1514 self._frozen = True 

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

1516 field.freeze(self) 

1517 

1518 def _save(self, outfile): 

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

1520 

1521 Parameters 

1522 ---------- 

1523 outfile : file-like object 

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

1525 bytes. 

1526 """ 

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

1528 field.save(outfile, self) 

1529 

1530 def _collectImports(self): 

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

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

1533 collect method. 

1534 

1535 The field method will call _collectImports on any 

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

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

1538 class. 

1539 """ 

1540 self._imports.add(self.__module__) 

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

1542 field._collectImports(self, self._imports) 

1543 

1544 def toDict(self): 

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

1546 

1547 Returns 

1548 ------- 

1549 dict_ : `dict` 

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

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

1552 

1553 See Also 

1554 -------- 

1555 lsst.pex.config.Field.toDict 

1556 

1557 Notes 

1558 ----- 

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

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

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

1562 """ 

1563 dict_ = {} 

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

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

1566 return dict_ 

1567 

1568 def names(self): 

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

1570 

1571 Returns 

1572 ------- 

1573 names : `list` of `str` 

1574 Field names. 

1575 """ 

1576 # 

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

1578 # pre-existing saveToStream() 

1579 # 

1580 with io.StringIO() as strFd: 

1581 self.saveToStream(strFd, "config") 

1582 contents = strFd.getvalue() 

1583 strFd.close() 

1584 # 

1585 # Pull the names out of the dumped config 

1586 # 

1587 keys = [] 

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

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

1590 continue 

1591 

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

1593 if mat: 

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

1595 

1596 return keys 

1597 

1598 def _rename(self, name): 

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

1600 

1601 Parameters 

1602 ---------- 

1603 name : `str` 

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

1605 

1606 Notes 

1607 ----- 

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

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

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

1611 method for *this* method to work. 

1612 

1613 See Also 

1614 -------- 

1615 lsst.pex.config.Field.rename 

1616 """ 

1617 self._name = name 

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

1619 field.rename(self) 

1620 

1621 def validate(self): 

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

1623 

1624 Raises 

1625 ------ 

1626 lsst.pex.config.FieldValidationError 

1627 Raised if verification fails. 

1628 

1629 Notes 

1630 ----- 

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

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

1633 

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

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

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

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

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

1639 

1640 Inter-field relationships should only be checked in derived 

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

1642 validation is complete. 

1643 """ 

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

1645 field.validate(self) 

1646 

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

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

1649 

1650 Parameters 

1651 ---------- 

1652 name : `str` 

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

1654 **kwargs 

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

1656 

1657 Returns 

1658 ------- 

1659 history : `str` 

1660 A string containing the formatted history. 

1661 

1662 See Also 

1663 -------- 

1664 lsst.pex.config.history.format 

1665 """ 

1666 import lsst.pex.config.history as pexHist 

1667 

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

1669 

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

1671 """Read-only history. 

1672 """ 

1673 

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

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

1676 

1677 Notes 

1678 ----- 

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

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

1681 to them dynamically. 

1682 

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

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

1685 non-existent field. 

1686 """ 

1687 if attr in self._fields: 

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

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

1690 warnings.warn( 

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

1692 FutureWarning, 

1693 stacklevel=2, 

1694 ) 

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

1696 at = getCallStack() 

1697 # This allows Field descriptors to work. 

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

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

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

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

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

1703 # This allows specific private attributes to work. 

1704 self.__dict__[attr] = value 

1705 else: 

1706 # We throw everything else. 

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

1708 

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

1710 if attr in self._fields: 

1711 if at is None: 

1712 at = getCallStack() 

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

1714 else: 

1715 object.__delattr__(self, attr) 

1716 

1717 def __eq__(self, other): 

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

1719 for name in self._fields: 

1720 thisValue = getattr(self, name) 

1721 otherValue = getattr(other, name) 

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

1723 if not math.isnan(otherValue): 

1724 return False 

1725 elif thisValue != otherValue: 

1726 return False 

1727 return True 

1728 return False 

1729 

1730 def __ne__(self, other): 

1731 return not self.__eq__(other) 

1732 

1733 def __str__(self): 

1734 return str(self.toDict()) 

1735 

1736 def __repr__(self): 

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

1738 _typeStr(self), 

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

1740 ) 

1741 

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

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

1744 equality. 

1745 

1746 Parameters 

1747 ---------- 

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

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

1750 config. 

1751 shortcut : `bool`, optional 

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

1753 `True`. 

1754 rtol : `float`, optional 

1755 Relative tolerance for floating point comparisons. 

1756 atol : `float`, optional 

1757 Absolute tolerance for floating point comparisons. 

1758 output : callable, optional 

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

1760 report inequalities. 

1761 

1762 Returns 

1763 ------- 

1764 isEqual : `bool` 

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

1766 `False` if there is an inequality. 

1767 

1768 See Also 

1769 -------- 

1770 lsst.pex.config.compareConfigs 

1771 

1772 Notes 

1773 ----- 

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

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

1776 are not considered by this method. 

1777 

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

1779 """ 

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

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

1782 name = getComparisonName(name1, name2) 

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

1784 

1785 @classmethod 

1786 def __init_subclass__(cls, **kwargs): 

1787 """Run initialization for every subclass. 

1788 

1789 Specifically registers the subclass with a YAML representer 

1790 and YAML constructor (if pyyaml is available) 

1791 """ 

1792 super().__init_subclass__(**kwargs) 

1793 

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

1795 return 

1796 

1797 yaml.add_representer(cls, _yaml_config_representer) 

1798 

1799 @classmethod 

1800 def _fromPython(cls, config_py): 

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

1802 

1803 Parameters 

1804 ---------- 

1805 config_py : `str` 

1806 A serialized form of the Config as created by 

1807 `Config.saveToStream`. 

1808 

1809 Returns 

1810 ------- 

1811 config : `Config` 

1812 Reconstructed `Config` instant. 

1813 """ 

1814 cls = _classFromPython(config_py) 

1815 return unreduceConfig(cls, config_py) 

1816 

1817 

1818def _classFromPython(config_py): 

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

1820 

1821 Parameters 

1822 ---------- 

1823 config_py : `str` 

1824 A serialized form of the Config as created by 

1825 `Config.saveToStream`. 

1826 

1827 Returns 

1828 ------- 

1829 cls : `type` 

1830 The `Config` subclass associated with this config. 

1831 """ 

1832 # standard serialization has the form: 

1833 # import config.class 

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

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

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

1837 

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

1839 # large config into separate lines. 

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

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

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

1843 

1844 if not matches: 

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

1846 raise ValueError( 

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

1848 ) 

1849 

1850 module_name = matches.group(1) 

1851 module = importlib.import_module(module_name) 

1852 

1853 # Second line 

1854 full_name = matches.group(2) 

1855 

1856 # Remove the module name from the full name 

1857 if not full_name.startswith(module_name): 

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

1859 

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

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

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

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

1864 components = remainder.split(".") 

1865 pytype = module 

1866 for component in components: 

1867 pytype = getattr(pytype, component) 

1868 return pytype 

1869 

1870 

1871def unreduceConfig(cls_, stream): 

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

1873 

1874 Parameters 

1875 ---------- 

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

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

1878 with configurations in the ``stream``. 

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

1880 Stream containing configuration override code. 

1881 

1882 Returns 

1883 ------- 

1884 config : `lsst.pex.config.Config` 

1885 Config instance. 

1886 

1887 See Also 

1888 -------- 

1889 lsst.pex.config.Config.loadFromStream 

1890 """ 

1891 config = cls_() 

1892 config.loadFromStream(stream) 

1893 return config