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

500 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:41 +0000

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 for {_typeStr(config)} 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 : `collections.abc.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.ListField`) depending on the 

389 field's 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 >>> config = Example() 

418 >>> print(config.myInt) 

419 0 

420 >>> config.myInt = 5 

421 >>> print(config.myInt) 

422 5 

423 """ 

424 

425 name: str 

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

427 Class. 

428 """ 

429 

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

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

432 """ 

433 

434 @staticmethod 

435 def _parseTypingArgs( 

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

437 ) -> Mapping[str, Any]: 

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

439 

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

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

442 

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

444 handle turning type parameters into keyword arguments (see DictField 

445 for an example) 

446 

447 Parameters 

448 ---------- 

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

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

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

452 and will be treated as such. 

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

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

455 Field constructor. 

456 

457 Returns 

458 ------- 

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

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

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

462 from the input parameters. 

463 

464 Raises 

465 ------ 

466 ValueError 

467 Raised if params is of incorrect length. 

468 Raised if a forward reference could not be resolved 

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

470 """ 

471 if len(params) > 1: 

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

473 unpackedParams = params[0] 

474 if isinstance(unpackedParams, str): 

475 _typ = ForwardRef(unpackedParams) 

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

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

478 # 3.9+ takes 3 args. 

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

480 if result is None: 

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

482 unpackedParams = cast(type, result) 

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

484 raise ValueError("Conflicting definition for dtype") 

485 elif "dtype" not in kwds: 

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

487 return kwds 

488 

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

490 return _PexConfigGenericAlias(cls, params) 

491 

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

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

494 raise ValueError( 

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

496 ) 

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

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

499 

500 source = getStackFrame() 

501 self._setup( 

502 doc=doc, 

503 dtype=dtype, 

504 default=default, 

505 check=check, 

506 optional=optional, 

507 source=source, 

508 deprecated=deprecated, 

509 ) 

510 

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

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

513 self.dtype = dtype 

514 """Data type for the field. 

515 """ 

516 

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

518 raise ValueError("Docstring is empty.") 

519 

520 # append the deprecation message to the docstring. 

521 if deprecated is not None: 

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

523 self.doc = doc 

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

525 """ 

526 

527 self.deprecated = deprecated 

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

529 """ 

530 

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

532 if optional or default is not None: 

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

534 self.__doc__ += ")" 

535 

536 self.default = default 

537 """Default value for this field. 

538 """ 

539 

540 self.check = check 

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

542 """ 

543 

544 self.optional = optional 

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

546 

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

548 field's value is `None`. 

549 """ 

550 

551 self.source = source 

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

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

554 """ 

555 

556 def rename(self, instance): 

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

558 only). 

559 

560 Parameters 

561 ---------- 

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

563 The config instance that contains this field. 

564 

565 Notes 

566 ----- 

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

568 contains this field and should not be called directly. 

569 

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

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

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

573 `lsst.pex.config.config._joinNamePath`. 

574 """ 

575 pass 

576 

577 def validate(self, instance): 

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

579 

580 Parameters 

581 ---------- 

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

583 The config instance that contains this field. 

584 

585 Raises 

586 ------ 

587 lsst.pex.config.FieldValidationError 

588 Raised if verification fails. 

589 

590 Notes 

591 ----- 

592 This method provides basic validation: 

593 

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

595 - Ensures type correctness. 

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

597 

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

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

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

601 """ 

602 value = self.__get__(instance) 

603 if not self.optional and value is None: 

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

605 

606 def freeze(self, instance): 

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

608 

609 Parameters 

610 ---------- 

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

612 The config instance that contains this field. 

613 

614 Notes 

615 ----- 

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

617 hold subconfigs should freeze each subconfig. 

618 

619 **Subclasses should implement this method.** 

620 """ 

621 pass 

622 

623 def _validateValue(self, value): 

624 """Validate a value. 

625 

626 Parameters 

627 ---------- 

628 value : `object` 

629 The value being validated. 

630 

631 Raises 

632 ------ 

633 TypeError 

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

635 ``dtype``. 

636 ValueError 

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

638 """ 

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

640 return 

641 

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

643 msg = ( 

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

645 ) 

646 raise TypeError(msg) 

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

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

649 raise ValueError(msg) 

650 

651 def _collectImports(self, instance, imports): 

652 """Call the _collectImports method on all config 

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

654 set. 

655 

656 Parameters 

657 ---------- 

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

659 A config object that has this field defined on it 

660 imports : `set` 

661 Set of python modules that need imported after persistence 

662 """ 

663 pass 

664 

665 def save(self, outfile, instance): 

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

667 

668 Parameters 

669 ---------- 

670 outfile : `typing.IO` 

671 A writeable file handle. 

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

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

674 

675 Notes 

676 ----- 

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

678 contains this field and should not be called directly. 

679 

680 The output consists of the documentation string 

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

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

683 

684 This output can be executed with Python. 

685 """ 

686 value = self.__get__(instance) 

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

688 

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

690 return 

691 

692 # write full documentation string as comment lines 

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

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

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

696 # non-finite numbers need special care 

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

698 else: 

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

700 

701 def toDict(self, instance): 

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

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

704 

705 Parameters 

706 ---------- 

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

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

709 

710 Returns 

711 ------- 

712 value : `object` 

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

714 

715 Notes 

716 ----- 

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

718 should not be called directly. 

719 

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

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

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

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

724 the field values in the subconfig. 

725 """ 

726 return self.__get__(instance) 

727 

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

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

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

731 

732 Any frozen storage should be unfrozen. 

733 """ 

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

735 

736 @overload 

737 def __get__( 

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

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

740 

741 @overload 

742 def __get__( 

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

744 ) -> FieldTypeVar: ... 

745 

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

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

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

749 directly. 

750 

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

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

753 Config classes. 

754 

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

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

757 returned. 

758 """ 

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

760 return self 

761 else: 

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

763 try: 

764 return instance._storage[self.name] 

765 except AttributeError: 

766 if not isinstance(instance, Config): 

767 return self 

768 else: 

769 raise AttributeError( 

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

771 ) from None 

772 

773 def __set__( 

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

775 ) -> None: 

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

777 

778 Parameters 

779 ---------- 

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

781 The config instance that contains this field. 

782 value : obj 

783 Value to set on this field. 

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

785 optional 

786 The call stack (created by 

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

788 label : `str`, optional 

789 Event label for the history. 

790 

791 Notes 

792 ----- 

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

794 and should not be called directly. 

795 

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

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

798 should follow the following rules: 

799 

800 - Do not allow modification of frozen configs. 

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

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

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

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

805 invalid values. 

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

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

808 

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

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

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

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

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

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

815 reimplementation. 

816 """ 

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

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

819 

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

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

822 value = _autocast(value, self.dtype) 

823 try: 

824 self._validateValue(value) 

825 except BaseException as e: 

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

827 

828 instance._storage[self.name] = value 

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

830 at = getCallStack() 

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

832 

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

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

835 

836 Parameters 

837 ---------- 

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

839 The config instance that contains this field. 

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

841 The call stack (created by 

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

843 label : `str`, optional 

844 Event label for the history. 

845 

846 Notes 

847 ----- 

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

849 should not be called directly. 

850 """ 

851 if at is None: 

852 at = getCallStack() 

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

854 

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

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

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

858 

859 Parameters 

860 ---------- 

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

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

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

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

865 shortcut : `bool`, optional 

866 **Unused.** 

867 rtol : `float`, optional 

868 Relative tolerance for floating point comparisons. 

869 atol : `float`, optional 

870 Absolute tolerance for floating point comparisons. 

871 output : `collections.abc.Callable`, optional 

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

873 report inequalities. 

874 

875 Notes 

876 ----- 

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

878 

879 See Also 

880 -------- 

881 lsst.pex.config.compareScalars 

882 """ 

883 v1 = getattr(instance1, self.name) 

884 v2 = getattr(instance2, self.name) 

885 name = getComparisonName( 

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

887 ) 

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

889 

890 

891class RecordingImporter: 

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

893 imported. 

894 

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

896 

897 Examples 

898 -------- 

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

900 when done: 

901 

902 >>> with RecordingImporter() as importer: 

903 ... # import stuff 

904 ... import numpy as np 

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

906 """ 

907 

908 def __init__(self): 

909 self._modules = set() 

910 

911 def __enter__(self): 

912 self.origMetaPath = sys.meta_path 

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

914 return self 

915 

916 def __exit__(self, *args): 

917 self.uninstall() 

918 return False # Don't suppress exceptions 

919 

920 def uninstall(self): 

921 """Uninstall the importer.""" 

922 sys.meta_path = self.origMetaPath 

923 

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

925 """Find a module. 

926 

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

928 

929 Parameters 

930 ---------- 

931 fullname : `str` 

932 Name of module. 

933 path : `list` [`str`] 

934 Search path. Unused. 

935 target : `~typing.Any`, optional 

936 Unused. 

937 """ 

938 self._modules.add(fullname) 

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

940 return None 

941 

942 def getModules(self): 

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

944 

945 Returns 

946 ------- 

947 modules : `set` of `str` 

948 Set of imported module names. 

949 """ 

950 return self._modules 

951 

952 

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

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

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

956 

957 Notes 

958 ----- 

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

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

961 class behavior. 

962 

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

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

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

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

967 fields as attributes of the configuration instance. 

968 

969 Examples 

970 -------- 

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

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

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

974 

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

976 >>> class DemoConfig(Config): 

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

978 ... listField = ListField( 

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

980 ... dtype=str, 

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

982 ... ) 

983 >>> config = DemoConfig() 

984 

985 Configs support many `dict`-like APIs: 

986 

987 >>> config.keys() 

988 ['intField', 'listField'] 

989 >>> "intField" in config 

990 True 

991 

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

993 

994 >>> config.intField 

995 42 

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

997 >>> print(config.listField) 

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

999 """ 

1000 

1001 _storage: dict[str, Any] 

1002 _fields: dict[str, Field] 

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

1004 _imports: set[Any] 

1005 

1006 def __iter__(self): 

1007 """Iterate over fields.""" 

1008 return self._fields.__iter__() 

1009 

1010 def keys(self): 

1011 """Get field names. 

1012 

1013 Returns 

1014 ------- 

1015 names : `~collections.abc.KeysView` 

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

1017 """ 

1018 return self._storage.keys() 

1019 

1020 def values(self): 

1021 """Get field values. 

1022 

1023 Returns 

1024 ------- 

1025 values : `~collections.abc.ValuesView` 

1026 Iterator of field values. 

1027 """ 

1028 return self._storage.values() 

1029 

1030 def items(self): 

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

1032 

1033 Returns 

1034 ------- 

1035 items : `~collections.abc.ItemsView` 

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

1037 

1038 0. Field name. 

1039 1. Field value. 

1040 """ 

1041 return self._storage.items() 

1042 

1043 def __contains__(self, name): 

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

1045 

1046 Parameters 

1047 ---------- 

1048 name : `str` 

1049 Field name to test for. 

1050 

1051 Returns 

1052 ------- 

1053 in : `bool` 

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

1055 """ 

1056 return self._storage.__contains__(name) 

1057 

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

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

1060 

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

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

1063 some attributes are handled at allocation time rather than at 

1064 initialization. 

1065 

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

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

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

1069 """ 

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

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

1072 # remove __label and ignore it 

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

1074 

1075 instance = object.__new__(cls) 

1076 instance._frozen = False 

1077 instance._name = name 

1078 instance._storage = {} 

1079 instance._history = {} 

1080 instance._imports = set() 

1081 # load up defaults 

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

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

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

1085 # set custom default-overrides 

1086 instance.setDefaults() 

1087 # set constructor overrides 

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

1089 return instance 

1090 

1091 def copy(self) -> Config: 

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

1093 

1094 Notes 

1095 ----- 

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

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

1098 original hierarchy. 

1099 

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

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

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

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

1104 """ 

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

1106 instance._frozen = False 

1107 instance._name = self._name 

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

1109 instance._imports = set(self._imports) 

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

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

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

1113 return instance 

1114 

1115 def __reduce__(self): 

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

1117 

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

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

1120 be pickled. 

1121 """ 

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

1123 # requires bytes 

1124 stream = io.StringIO() 

1125 self.saveToStream(stream) 

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

1127 

1128 def setDefaults(self): 

1129 """Subclass hook for computing defaults. 

1130 

1131 Notes 

1132 ----- 

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

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

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

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

1137 ``setDefaults``. 

1138 """ 

1139 pass 

1140 

1141 def update(self, **kw): 

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

1143 

1144 Parameters 

1145 ---------- 

1146 **kw 

1147 Keywords are configuration field names. Values are configuration 

1148 field values. 

1149 

1150 Notes 

1151 ----- 

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

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

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

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

1156 

1157 Examples 

1158 -------- 

1159 This is a config with three fields: 

1160 

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

1162 >>> class DemoConfig(Config): 

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

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

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

1166 >>> config = DemoConfig() 

1167 

1168 These are the default values of each field: 

1169 

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

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

1172 fieldA: 42 

1173 fieldB: True 

1174 fieldC: 'Hello world' 

1175 

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

1177 

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

1179 

1180 Now the values of each field are: 

1181 

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

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

1184 fieldA: 13 

1185 fieldB: True 

1186 fieldC: 'Updated!' 

1187 """ 

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

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

1190 

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

1192 try: 

1193 field = self._fields[name] 

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

1195 except KeyError as e: 

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

1197 raise 

1198 

1199 def _filename_to_resource( 

1200 self, filename: ResourcePathExpression | None = None 

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

1202 """Create resource path from filename. 

1203 

1204 Parameters 

1205 ---------- 

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

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

1208 if no file URI is known. 

1209 

1210 Returns 

1211 ------- 

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

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

1214 was given or refers to unspecified value. 

1215 file_string : `str` 

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

1217 """ 

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

1219 return None, "<unknown>" 

1220 base = _get_config_root() 

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

1222 

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

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

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

1226 # the config. 

1227 if resource.scheme == "file": 

1228 file_string = resource.ospath 

1229 else: 

1230 file_string = str(resource) 

1231 

1232 return resource, file_string 

1233 

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

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

1236 configuration file. 

1237 

1238 Parameters 

1239 ---------- 

1240 filename : `lsst.resources.ResourcePathExpression` 

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

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

1243 are not allowed. 

1244 root : `str`, optional 

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

1246 overridden. 

1247 

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

1249 contains:: 

1250 

1251 config.myField = 5 

1252 

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

1254 

1255 See Also 

1256 -------- 

1257 lsst.pex.config.Config.loadFromStream 

1258 lsst.pex.config.Config.loadFromString 

1259 lsst.pex.config.Config.save 

1260 lsst.pex.config.Config.saveToStream 

1261 lsst.pex.config.Config.saveToString 

1262 """ 

1263 resource, file_string = self._filename_to_resource(filename) 

1264 if resource is None: 

1265 # A filename is required. 

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

1267 

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

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

1270 

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

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

1273 with _push_config_root(resource.dirname()): 

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

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

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

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

1278 

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

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

1281 provided stream. 

1282 

1283 Parameters 

1284 ---------- 

1285 stream : `typing.IO`, `str`, `bytes`, or `~types.CodeType` 

1286 Stream containing configuration override code. If this is a 

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

1288 root : `str`, optional 

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

1290 overridden. 

1291 

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

1293 contains:: 

1294 

1295 config.myField = 5 

1296 

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

1298 filename : `str`, optional 

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

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

1301 variable in config. 

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

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

1304 

1305 Notes 

1306 ----- 

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

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

1309 `loadFromString` instead for most of these types. 

1310 

1311 See Also 

1312 -------- 

1313 lsst.pex.config.Config.load 

1314 lsst.pex.config.Config.loadFromString 

1315 lsst.pex.config.Config.save 

1316 lsst.pex.config.Config.saveToStream 

1317 lsst.pex.config.Config.saveToString 

1318 """ 

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

1320 if filename is None: 

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

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

1323 else: 

1324 code = stream 

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

1326 

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

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

1329 provided string. 

1330 

1331 Parameters 

1332 ---------- 

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

1334 Stream containing configuration override code. 

1335 root : `str`, optional 

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

1337 overridden. 

1338 

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

1340 contains:: 

1341 

1342 config.myField = 5 

1343 

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

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

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

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

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

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

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

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

1352 

1353 Raises 

1354 ------ 

1355 ValueError 

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

1357 the root argument. 

1358 

1359 See Also 

1360 -------- 

1361 lsst.pex.config.Config.load 

1362 lsst.pex.config.Config.loadFromStream 

1363 lsst.pex.config.Config.save 

1364 lsst.pex.config.Config.saveToStream 

1365 lsst.pex.config.Config.saveToString 

1366 """ 

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

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

1369 # has attribute "co_filename", 

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

1371 

1372 resource, file_string = self._filename_to_resource(filename) 

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

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

1375 # relative paths. No reason to use context. 

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

1377 else: 

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

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

1380 with _push_config_root(resource.dirname()): 

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

1382 

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

1384 """Update config from string. 

1385 

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

1387 """ 

1388 with RecordingImporter() as importer: 

1389 globals = {"__file__": filename} 

1390 local = {root: self} 

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

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

1393 if root in extraLocals: 

1394 raise ValueError( 

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

1396 ) 

1397 local.update(extraLocals) 

1398 exec(code, globals, local) 

1399 

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

1401 

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

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

1404 reproduces this config. 

1405 

1406 Parameters 

1407 ---------- 

1408 filename : `str` 

1409 Desination filename of this configuration. 

1410 root : `str`, optional 

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

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

1413 

1414 See Also 

1415 -------- 

1416 lsst.pex.config.Config.saveToStream 

1417 lsst.pex.config.Config.saveToString 

1418 lsst.pex.config.Config.load 

1419 lsst.pex.config.Config.loadFromStream 

1420 lsst.pex.config.Config.loadFromString 

1421 """ 

1422 d = os.path.dirname(filename) 

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

1424 self.saveToStream(outfile, root) 

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

1426 # for an explantion of these antics see: 

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

1428 umask = os.umask(0o077) 

1429 os.umask(umask) 

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

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

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

1433 # os.rename may not work across filesystems 

1434 shutil.move(outfile.name, filename) 

1435 

1436 def saveToString(self, skipImports=False): 

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

1438 string. 

1439 

1440 Parameters 

1441 ---------- 

1442 skipImports : `bool`, optional 

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

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

1445 additional clutter is not useful. 

1446 

1447 Returns 

1448 ------- 

1449 code : `str` 

1450 A code string readable by `loadFromString`. 

1451 

1452 See Also 

1453 -------- 

1454 lsst.pex.config.Config.save 

1455 lsst.pex.config.Config.saveToStream 

1456 lsst.pex.config.Config.load 

1457 lsst.pex.config.Config.loadFromStream 

1458 lsst.pex.config.Config.loadFromString 

1459 """ 

1460 buffer = io.StringIO() 

1461 self.saveToStream(buffer, skipImports=skipImports) 

1462 return buffer.getvalue() 

1463 

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

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

1466 reproduces this config. 

1467 

1468 Parameters 

1469 ---------- 

1470 outfile : `typing.TextIO` 

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

1472 bytes. 

1473 root : `str`, optional 

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

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

1476 skipImports : `bool`, optional 

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

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

1479 additional clutter is not useful. 

1480 

1481 See Also 

1482 -------- 

1483 lsst.pex.config.Config.save 

1484 lsst.pex.config.Config.saveToString 

1485 lsst.pex.config.Config.load 

1486 lsst.pex.config.Config.loadFromStream 

1487 lsst.pex.config.Config.loadFromString 

1488 """ 

1489 tmp = self._name 

1490 self._rename(root) 

1491 try: 

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

1493 self._collectImports() 

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

1495 self._imports.remove(self.__module__) 

1496 configType = type(self) 

1497 typeString = _typeStr(configType) 

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

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

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

1501 # formatting. 

1502 outfile.write( 

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

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

1505 ) 

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

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

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

1509 self._save(outfile) 

1510 finally: 

1511 self._rename(tmp) 

1512 

1513 def freeze(self): 

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

1515 self._frozen = True 

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

1517 field.freeze(self) 

1518 

1519 def _save(self, outfile): 

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

1521 

1522 Parameters 

1523 ---------- 

1524 outfile : `typing.TextIO` 

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

1526 bytes. 

1527 """ 

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

1529 field.save(outfile, self) 

1530 

1531 def _collectImports(self): 

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

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

1534 collect method. 

1535 

1536 The field method will call _collectImports on any 

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

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

1539 class. 

1540 """ 

1541 self._imports.add(self.__module__) 

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

1543 field._collectImports(self, self._imports) 

1544 

1545 def toDict(self): 

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

1547 

1548 Returns 

1549 ------- 

1550 dict_ : `dict` 

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

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

1553 

1554 See Also 

1555 -------- 

1556 lsst.pex.config.Field.toDict 

1557 

1558 Notes 

1559 ----- 

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

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

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

1563 """ 

1564 dict_ = {} 

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

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

1567 return dict_ 

1568 

1569 def names(self): 

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

1571 

1572 Returns 

1573 ------- 

1574 names : `list` of `str` 

1575 Field names. 

1576 """ 

1577 # 

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

1579 # pre-existing saveToStream() 

1580 # 

1581 with io.StringIO() as strFd: 

1582 self.saveToStream(strFd, "config") 

1583 contents = strFd.getvalue() 

1584 strFd.close() 

1585 # 

1586 # Pull the names out of the dumped config 

1587 # 

1588 keys = [] 

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

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

1591 continue 

1592 

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

1594 if mat: 

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

1596 

1597 return keys 

1598 

1599 def _rename(self, name): 

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

1601 

1602 Parameters 

1603 ---------- 

1604 name : `str` 

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

1606 

1607 Notes 

1608 ----- 

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

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

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

1612 method for *this* method to work. 

1613 

1614 See Also 

1615 -------- 

1616 lsst.pex.config.Field.rename 

1617 """ 

1618 self._name = name 

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

1620 field.rename(self) 

1621 

1622 def validate(self): 

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

1624 

1625 Raises 

1626 ------ 

1627 lsst.pex.config.FieldValidationError 

1628 Raised if verification fails. 

1629 

1630 Notes 

1631 ----- 

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

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

1634 

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

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

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

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

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

1640 

1641 Inter-field relationships should only be checked in derived 

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

1643 validation is complete. 

1644 """ 

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

1646 field.validate(self) 

1647 

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

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

1650 

1651 Parameters 

1652 ---------- 

1653 name : `str` 

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

1655 **kwargs 

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

1657 

1658 Returns 

1659 ------- 

1660 history : `str` 

1661 A string containing the formatted history. 

1662 

1663 See Also 

1664 -------- 

1665 lsst.pex.config.history.format 

1666 """ 

1667 import lsst.pex.config.history as pexHist 

1668 

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

1670 

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

1672 """Read-only history. 

1673 """ 

1674 

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

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

1677 

1678 Notes 

1679 ----- 

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

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

1682 to them dynamically. 

1683 

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

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

1686 non-existent field. 

1687 """ 

1688 if attr in self._fields: 

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

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

1691 warnings.warn( 

1692 f"Config field {_typeStr(type(self))}.{fullname} is deprecated: " 

1693 f"{self._fields[attr].deprecated}", 

1694 FutureWarning, 

1695 stacklevel=2, 

1696 ) 

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

1698 at = getCallStack() 

1699 # This allows Field descriptors to work. 

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

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

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

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

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

1705 # This allows specific private attributes to work. 

1706 self.__dict__[attr] = value 

1707 else: 

1708 # We throw everything else. 

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

1710 

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

1712 if attr in self._fields: 

1713 if at is None: 

1714 at = getCallStack() 

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

1716 else: 

1717 object.__delattr__(self, attr) 

1718 

1719 def __eq__(self, other): 

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

1721 for name in self._fields: 

1722 thisValue = getattr(self, name) 

1723 otherValue = getattr(other, name) 

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

1725 if not math.isnan(otherValue): 

1726 return False 

1727 elif thisValue != otherValue: 

1728 return False 

1729 return True 

1730 return False 

1731 

1732 def __ne__(self, other): 

1733 return not self.__eq__(other) 

1734 

1735 def __str__(self): 

1736 return str(self.toDict()) 

1737 

1738 def __repr__(self): 

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

1740 _typeStr(self), 

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

1742 ) 

1743 

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

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

1746 equality. 

1747 

1748 Parameters 

1749 ---------- 

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

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

1752 config. 

1753 shortcut : `bool`, optional 

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

1755 `True`. 

1756 rtol : `float`, optional 

1757 Relative tolerance for floating point comparisons. 

1758 atol : `float`, optional 

1759 Absolute tolerance for floating point comparisons. 

1760 output : `collections.abc.Callable`, optional 

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

1762 report inequalities. 

1763 

1764 Returns 

1765 ------- 

1766 isEqual : `bool` 

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

1768 `False` if there is an inequality. 

1769 

1770 See Also 

1771 -------- 

1772 lsst.pex.config.compareConfigs 

1773 

1774 Notes 

1775 ----- 

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

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

1778 are not considered by this method. 

1779 

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

1781 """ 

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

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

1784 name = getComparisonName(name1, name2) 

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

1786 

1787 @classmethod 

1788 def __init_subclass__(cls, **kwargs): 

1789 """Run initialization for every subclass. 

1790 

1791 Specifically registers the subclass with a YAML representer 

1792 and YAML constructor (if pyyaml is available) 

1793 """ 

1794 super().__init_subclass__(**kwargs) 

1795 

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

1797 return 

1798 

1799 yaml.add_representer(cls, _yaml_config_representer) 

1800 

1801 @classmethod 

1802 def _fromPython(cls, config_py): 

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

1804 

1805 Parameters 

1806 ---------- 

1807 config_py : `str` 

1808 A serialized form of the Config as created by 

1809 `Config.saveToStream`. 

1810 

1811 Returns 

1812 ------- 

1813 config : `Config` 

1814 Reconstructed `Config` instant. 

1815 """ 

1816 cls = _classFromPython(config_py) 

1817 return unreduceConfig(cls, config_py) 

1818 

1819 

1820def _classFromPython(config_py): 

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

1822 

1823 Parameters 

1824 ---------- 

1825 config_py : `str` 

1826 A serialized form of the Config as created by 

1827 `Config.saveToStream`. 

1828 

1829 Returns 

1830 ------- 

1831 cls : `type` 

1832 The `Config` subclass associated with this config. 

1833 """ 

1834 # standard serialization has the form: 

1835 # import config.class 

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

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

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

1839 

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

1841 # large config into separate lines. 

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

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

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

1845 

1846 if not matches: 

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

1848 raise ValueError( 

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

1850 ) 

1851 

1852 module_name = matches.group(1) 

1853 module = importlib.import_module(module_name) 

1854 

1855 # Second line 

1856 full_name = matches.group(2) 

1857 

1858 # Remove the module name from the full name 

1859 if not full_name.startswith(module_name): 

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

1861 

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

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

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

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

1866 components = remainder.split(".") 

1867 pytype = module 

1868 for component in components: 

1869 pytype = getattr(pytype, component) 

1870 return pytype 

1871 

1872 

1873def unreduceConfig(cls_, stream): 

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

1875 

1876 Parameters 

1877 ---------- 

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

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

1880 with configurations in the ``stream``. 

1881 stream : `typing.IO`, `str`, or `~types.CodeType` 

1882 Stream containing configuration override code. 

1883 

1884 Returns 

1885 ------- 

1886 config : `lsst.pex.config.Config` 

1887 Config instance. 

1888 

1889 See Also 

1890 -------- 

1891 lsst.pex.config.Config.loadFromStream 

1892 """ 

1893 config = cls_() 

1894 config.loadFromStream(stream) 

1895 return config