Coverage for python/lsst/pex/config/config.py: 58%
454 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:27 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:27 -0700
1# This file is part of pex_config.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
27from __future__ import annotations
29__all__ = (
30 "Config",
31 "ConfigMeta",
32 "Field",
33 "FieldTypeVar",
34 "FieldValidationError",
35 "UnexpectedProxyUsageError",
36)
38import copy
39import importlib
40import io
41import math
42import numbers
43import os
44import re
45import shutil
46import sys
47import tempfile
48import warnings
49from collections.abc import Mapping
50from types import GenericAlias
51from typing import Any, ForwardRef, Generic, TypeVar, cast, overload
53# if YAML is not available that's fine and we simply don't register
54# the yaml representer since we know it won't be used.
55try:
56 import yaml
57except ImportError:
58 yaml = None
60from .callStack import getCallStack, getStackFrame
61from .comparison import compareConfigs, compareScalars, getComparisonName
63if yaml: 63 ↛ 74line 63 didn't jump to line 74 because the condition on line 63 was always true
64 YamlLoaders: tuple[Any, ...] = (yaml.Loader, yaml.FullLoader, yaml.SafeLoader, yaml.UnsafeLoader)
66 try:
67 # CLoader is not always available
68 from yaml import CLoader
70 YamlLoaders += (CLoader,)
71 except ImportError:
72 pass
73else:
74 YamlLoaders = ()
75 doImport = None
78class _PexConfigGenericAlias(GenericAlias):
79 """A Subclass of python's GenericAlias used in defining and instantiating
80 Generics.
82 This class differs from `types.GenericAlias` in that it calls a method
83 named _parseTypingArgs defined on Fields. This method gives Field and its
84 subclasses an opportunity to transform type parameters into class key word
85 arguments. Code authors do not need to implement any returns of this object
86 directly, and instead only need implement _parseTypingArgs, if a Field
87 subclass differs from the base class implementation.
89 This class is intended to be an implementation detail, returned from a
90 Field's `__class_getitem__` method.
91 """
93 def __call__(self, *args: Any, **kwds: Any) -> Any:
94 origin_kwargs = self._parseTypingArgs(self.__args__, kwds)
95 return super().__call__(*args, **{**kwds, **origin_kwargs})
98FieldTypeVar = TypeVar("FieldTypeVar")
101class UnexpectedProxyUsageError(TypeError):
102 """Exception raised when a proxy class is used in a context that suggests
103 it should have already been converted to the thing it proxies.
104 """
107def _joinNamePath(prefix=None, name=None, index=None):
108 """Generate nested configuration names."""
109 if not prefix and not name: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 raise ValueError("Invalid name: cannot be None")
111 elif not name: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 name = prefix
113 elif prefix and name: 113 ↛ 116line 113 didn't jump to line 116 because the condition on line 113 was always true
114 name = prefix + "." + name
116 if index is not None: 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true
117 return f"{name}[{index!r}]"
118 else:
119 return name
122def _autocast(x, dtype):
123 """Cast a value to a type, if appropriate.
125 Parameters
126 ----------
127 x : object
128 A value.
129 dtype : type
130 Data type, such as `float`, `int`, or `str`.
132 Returns
133 -------
134 values : object
135 If appropriate, the returned value is ``x`` cast to the given type
136 ``dtype``. If the cast cannot be performed the original value of
137 ``x`` is returned.
139 Notes
140 -----
141 Will convert numpy scalar types to the standard Python equivalents.
142 """
143 if dtype is float and isinstance(x, numbers.Real): 143 ↛ 145line 143 didn't jump to line 145 because the condition on line 143 was always true
144 return float(x)
145 if dtype is int and isinstance(x, numbers.Integral):
146 return int(x)
147 return x
150def _typeStr(x):
151 """Generate a fully-qualified type name.
153 Returns
154 -------
155 `str`
156 Fully-qualified type name.
158 Notes
159 -----
160 This function is used primarily for writing config files to be executed
161 later upon with the 'load' function.
162 """
163 if hasattr(x, "__module__") and hasattr(x, "__name__"):
164 xtype = x
165 else:
166 xtype = type(x)
167 if xtype.__module__ == "builtins": 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true
168 return xtype.__name__
169 else:
170 return f"{xtype.__module__}.{xtype.__name__}"
173if yaml: 173 ↛ 206line 173 didn't jump to line 206 because the condition on line 173 was always true
175 def _yaml_config_representer(dumper, data):
176 """Represent a Config object in a form suitable for YAML.
178 Stores the serialized stream as a scalar block string.
179 """
180 stream = io.StringIO()
181 data.saveToStream(stream)
182 config_py = stream.getvalue()
184 # Strip multiple newlines from the end of the config
185 # This simplifies the YAML to use | and not |+
186 config_py = config_py.rstrip() + "\n"
188 # Trailing spaces force pyyaml to use non-block form.
189 # Remove the trailing spaces so it has no choice
190 config_py = re.sub(r"\s+$", "\n", config_py, flags=re.MULTILINE)
192 # Store the Python as a simple scalar
193 return dumper.represent_scalar("lsst.pex.config.Config", config_py, style="|")
195 def _yaml_config_constructor(loader, node):
196 """Construct a config from YAML."""
197 config_py = loader.construct_scalar(node)
198 return Config._fromPython(config_py)
200 # Register a generic constructor for Config and all subclasses
201 # Need to register for all the loaders we would like to use
202 for loader in YamlLoaders:
203 yaml.add_constructor("lsst.pex.config.Config", _yaml_config_constructor, Loader=loader)
206class ConfigMeta(type):
207 """A metaclass for `lsst.pex.config.Config`.
209 Parameters
210 ----------
211 name : `str`
212 Name to use for class.
213 bases : `~collections.abc.Iterable`
214 Base classes.
215 dict_ : `dict`
216 Additional parameters.
218 Notes
219 -----
220 ``ConfigMeta`` adds a dictionary containing all `~lsst.pex.config.Field`
221 class attributes as a class attribute called ``_fields``, and adds
222 the name of each field as an instance variable of the field itself (so you
223 don't have to pass the name of the field to the field constructor).
224 """
226 def __init__(cls, name, bases, dict_):
227 type.__init__(cls, name, bases, dict_)
228 cls._fields = {}
229 cls._source = getStackFrame()
231 def getFields(classtype):
232 fields = {}
233 bases = list(classtype.__bases__)
234 bases.reverse()
235 for b in bases:
236 fields.update(getFields(b))
238 for k, v in classtype.__dict__.items():
239 if isinstance(v, Field):
240 fields[k] = v
241 return fields
243 fields = getFields(cls)
244 for k, v in fields.items():
245 setattr(cls, k, copy.deepcopy(v))
247 def __setattr__(cls, name, value):
248 if isinstance(value, Field):
249 value.name = name
250 cls._fields[name] = value
251 type.__setattr__(cls, name, value)
254class FieldValidationError(ValueError):
255 """Raised when a ``~lsst.pex.config.Field`` is not valid in a
256 particular ``~lsst.pex.config.Config``.
258 Parameters
259 ----------
260 field : `lsst.pex.config.Field`
261 The field that was not valid.
262 config : `lsst.pex.config.Config`
263 The config containing the invalid field.
264 msg : `str`
265 Text describing why the field was not valid.
266 """
268 def __init__(self, field, config, msg):
269 self.fieldType = type(field)
270 """Type of the `~lsst.pex.config.Field` that incurred the error.
271 """
273 self.fieldName = field.name
274 """Name of the `~lsst.pex.config.Field` instance that incurred the
275 error (`str`).
277 See also
278 --------
279 lsst.pex.config.Field.name
280 """
282 self.fullname = _joinNamePath(config._name, field.name)
283 """Fully-qualified name of the `~lsst.pex.config.Field` instance
284 (`str`).
285 """
287 self.history = config.history.setdefault(field.name, [])
288 """Full history of all changes to the `~lsst.pex.config.Field`
289 instance.
290 """
292 self.fieldSource = field.source
293 """File and line number of the `~lsst.pex.config.Field` definition.
294 """
296 self.configSource = config._source
297 error = (
298 f"{self.fieldType.__name__} '{self.fullname}' failed validation: {msg}\n"
299 f"For more information see the Field definition at:\n{self.fieldSource.format()}"
300 f" and the Config definition at:\n{self.configSource.format()}"
301 )
302 super().__init__(error)
305class Field(Generic[FieldTypeVar]):
306 """A field in a `~lsst.pex.config.Config` that supports `int`, `float`,
307 `complex`, `bool`, and `str` data types.
309 Parameters
310 ----------
311 doc : `str`
312 A description of the field for users.
313 dtype : type, optional
314 The field's data type. ``Field`` only supports basic data types:
315 `int`, `float`, `complex`, `bool`, and `str`. See
316 `Field.supportedTypes`. Optional if supplied as a typing argument to
317 the class.
318 default : object, optional
319 The field's default value.
320 check : callable, optional
321 A callable that is called with the field's value. This callable should
322 return `False` if the value is invalid. More complex inter-field
323 validation can be written as part of the
324 `lsst.pex.config.Config.validate` method.
325 optional : `bool`, optional
326 This sets whether the field is considered optional, and therefore
327 doesn't need to be set by the user. When `False`,
328 `lsst.pex.config.Config.validate` fails if the field's value is `None`.
329 deprecated : None or `str`, optional
330 A description of why this Field is deprecated, including removal date.
331 If not None, the string is appended to the docstring for this Field.
333 Raises
334 ------
335 ValueError
336 Raised when the ``dtype`` parameter is not one of the supported types
337 (see `Field.supportedTypes`).
339 See Also
340 --------
341 ChoiceField
342 ConfigChoiceField
343 ConfigDictField
344 ConfigField
345 ConfigurableField
346 DictField
347 ListField
348 RangeField
349 RegistryField
351 Notes
352 -----
353 ``Field`` instances (including those of any subclass of ``Field``) are used
354 as class attributes of `~lsst.pex.config.Config` subclasses (see the
355 example, below). ``Field`` attributes work like the `property` attributes
356 of classes that implement custom setters and getters. `Field` attributes
357 belong to the class, but operate on the instance. Formally speaking,
358 `Field` attributes are `descriptors
359 <https://docs.python.org/3/howto/descriptor.html>`_.
361 When you access a `Field` attribute on a `Config` instance, you don't
362 get the `Field` instance itself. Instead, you get the value of that field,
363 which might be a simple type (`int`, `float`, `str`, `bool`) or a custom
364 container type (like a `lsst.pex.config.List`) depending on the field's
365 type. See the example, below.
367 Fields can be annotated with a type similar to other python classes (python
368 specification `here <https://peps.python.org/pep-0484/#generics>`_ ).
369 See the name field in the Config example below for an example of this.
370 Unlike most other uses in python, this has an effect at type checking *and*
371 runtime. If the type is specified with a class annotation, it will be used
372 as the value of the ``dtype`` in the ``Field`` and there is no need to
373 specify it as an argument during instantiation.
375 There are Some notes on dtype through type annotation syntax. Type
376 annotation syntax supports supplying the argument as a string of a type
377 name. i.e. "float", but this cannot be used to resolve circular references.
378 Type annotation syntax can be used on an identifier in addition to Class
379 assignment i.e. ``variable: Field[str] = Config.someField`` vs
380 ``someField = Field[str](doc="some doc"). However, this syntax is only
381 useful for annotating the type of the identifier (i.e. variable in previous
382 example) and does nothing for assigning the dtype of the ``Field``.
384 Examples
385 --------
386 Instances of ``Field`` should be used as class attributes of
387 `lsst.pex.config.Config` subclasses:
389 >>> from lsst.pex.config import Config, Field
390 >>> class Example(Config):
391 ... myInt = Field("An integer field.", int, default=0)
392 ... name = Field[str](doc="A string Field")
393 >>> print(config.myInt)
394 0
395 >>> config.myInt = 5
396 >>> print(config.myInt)
397 5
398 """
400 name: str
401 """Identifier (variable name) used to refer to a Field within a Config
402 Class.
403 """
405 supportedTypes = {str, bool, float, int, complex}
406 """Supported data types for field values (`set` of types).
407 """
409 @staticmethod
410 def _parseTypingArgs(
411 params: tuple[type, ...] | tuple[str, ...], kwds: Mapping[str, Any]
412 ) -> Mapping[str, Any]:
413 """Parse type annotations into keyword constructor arguments.
415 This is a special private method that interprets type arguments (i.e.
416 Field[str]) into keyword arguments to be passed on to the constructor.
418 Subclasses of Field can implement this method to customize how they
419 handle turning type parameters into keyword arguments (see DictField
420 for an example)
422 Parameters
423 ----------
424 params : `tuple` of `type` or `tuple` of str
425 Parameters passed to the type annotation. These will either be
426 types or strings. Strings are to interpreted as forward references
427 and will be treated as such.
428 kwds : `MutableMapping` with keys of `str` and values of `Any`
429 These are the user supplied keywords that are to be passed to the
430 Field constructor.
432 Returns
433 -------
434 kwds : `MutableMapping` with keys of `str` and values of `Any`
435 The mapping of keywords that will be passed onto the constructor
436 of the Field. Should be filled in with any information gleaned
437 from the input parameters.
439 Raises
440 ------
441 ValueError
442 Raised if params is of incorrect length.
443 Raised if a forward reference could not be resolved
444 Raised if there is a conflict between params and values in kwds
445 """
446 if len(params) > 1:
447 raise ValueError("Only single type parameters are supported")
448 unpackedParams = params[0]
449 if isinstance(unpackedParams, str):
450 _typ = ForwardRef(unpackedParams)
451 # type ignore below because typeshed seems to be wrong. It
452 # indicates there are only 2 args, as it was in python 3.8, but
453 # 3.9+ takes 3 args.
454 result = _typ._evaluate(globals(), locals(), recursive_guard=set()) # type: ignore
455 if result is None:
456 raise ValueError("Could not deduce type from input")
457 unpackedParams = cast(type, result)
458 if "dtype" in kwds and kwds["dtype"] != unpackedParams:
459 raise ValueError("Conflicting definition for dtype")
460 elif "dtype" not in kwds:
461 kwds = {**kwds, **{"dtype": unpackedParams}}
462 return kwds
464 def __class_getitem__(cls, params: tuple[type, ...] | type | ForwardRef):
465 return _PexConfigGenericAlias(cls, params)
467 def __init__(self, doc, dtype=None, default=None, check=None, optional=False, deprecated=None):
468 if dtype is None: 468 ↛ 469line 468 didn't jump to line 469 because the condition on line 468 was never true
469 raise ValueError(
470 "dtype must either be supplied as an argument or as a type argument to the class"
471 )
472 if dtype not in self.supportedTypes: 472 ↛ 473line 472 didn't jump to line 473 because the condition on line 472 was never true
473 raise ValueError(f"Unsupported Field dtype {_typeStr(dtype)}")
475 source = getStackFrame()
476 self._setup(
477 doc=doc,
478 dtype=dtype,
479 default=default,
480 check=check,
481 optional=optional,
482 source=source,
483 deprecated=deprecated,
484 )
486 def _setup(self, doc, dtype, default, check, optional, source, deprecated):
487 """Set attributes, usually during initialization."""
488 self.dtype = dtype
489 """Data type for the field.
490 """
492 if not doc: 492 ↛ 493line 492 didn't jump to line 493 because the condition on line 492 was never true
493 raise ValueError("Docstring is empty.")
495 # append the deprecation message to the docstring.
496 if deprecated is not None:
497 doc = f"{doc} Deprecated: {deprecated}"
498 self.doc = doc
499 """A description of the field (`str`).
500 """
502 self.deprecated = deprecated
503 """If not None, a description of why this field is deprecated (`str`).
504 """
506 self.__doc__ = f"{doc} (`{dtype.__name__}`"
507 if optional or default is not None:
508 self.__doc__ += f", default ``{default!r}``"
509 self.__doc__ += ")"
511 self.default = default
512 """Default value for this field.
513 """
515 self.check = check
516 """A user-defined function that validates the value of the field.
517 """
519 self.optional = optional
520 """Flag that determines if the field is required to be set (`bool`).
522 When `False`, `lsst.pex.config.Config.validate` will fail if the
523 field's value is `None`.
524 """
526 self.source = source
527 """The stack frame where this field is defined (`list` of
528 `~lsst.pex.config.callStack.StackFrame`).
529 """
531 def rename(self, instance):
532 r"""Rename the field in a `~lsst.pex.config.Config` (for internal use
533 only).
535 Parameters
536 ----------
537 instance : `lsst.pex.config.Config`
538 The config instance that contains this field.
540 Notes
541 -----
542 This method is invoked by the `lsst.pex.config.Config` object that
543 contains this field and should not be called directly.
545 Renaming is only relevant for `~lsst.pex.config.Field` instances that
546 hold subconfigs. `~lsst.pex.config.Field`\s that hold subconfigs should
547 rename each subconfig with the full field name as generated by
548 `lsst.pex.config.config._joinNamePath`.
549 """
550 pass
552 def validate(self, instance):
553 """Validate the field (for internal use only).
555 Parameters
556 ----------
557 instance : `lsst.pex.config.Config`
558 The config instance that contains this field.
560 Raises
561 ------
562 lsst.pex.config.FieldValidationError
563 Raised if verification fails.
565 Notes
566 -----
567 This method provides basic validation:
569 - Ensures that the value is not `None` if the field is not optional.
570 - Ensures type correctness.
571 - Ensures that the user-provided ``check`` function is valid.
573 Most `~lsst.pex.config.Field` subclasses should call
574 `lsst.pex.config.Field.validate` if they re-implement
575 `~lsst.pex.config.Field.validate`.
576 """
577 value = self.__get__(instance)
578 if not self.optional and value is None:
579 raise FieldValidationError(self, instance, "Required value cannot be None")
581 def freeze(self, instance):
582 """Make this field read-only (for internal use only).
584 Parameters
585 ----------
586 instance : `lsst.pex.config.Config`
587 The config instance that contains this field.
589 Notes
590 -----
591 Freezing is only relevant for fields that hold subconfigs. Fields which
592 hold subconfigs should freeze each subconfig.
594 **Subclasses should implement this method.**
595 """
596 pass
598 def _validateValue(self, value):
599 """Validate a value.
601 Parameters
602 ----------
603 value : object
604 The value being validated.
606 Raises
607 ------
608 TypeError
609 Raised if the value's type is incompatible with the field's
610 ``dtype``.
611 ValueError
612 Raised if the value is rejected by the ``check`` method.
613 """
614 if value is None: 614 ↛ 615line 614 didn't jump to line 615 because the condition on line 614 was never true
615 return
617 if not isinstance(value, self.dtype): 617 ↛ 618line 617 didn't jump to line 618 because the condition on line 617 was never true
618 msg = (
619 f"Value {value} is of incorrect type {_typeStr(value)}. Expected type {_typeStr(self.dtype)}"
620 )
621 raise TypeError(msg)
622 if self.check is not None and not self.check(value): 622 ↛ 623line 622 didn't jump to line 623 because the condition on line 622 was never true
623 msg = f"Value {value} is not a valid value"
624 raise ValueError(msg)
626 def _collectImports(self, instance, imports):
627 """Call the _collectImports method on all config
628 objects the field may own, and union them with the supplied imports
629 set.
631 Parameters
632 ----------
633 instance : instance or subclass of `lsst.pex.config.Config`
634 A config object that has this field defined on it
635 imports : `set`
636 Set of python modules that need imported after persistence
637 """
638 pass
640 def save(self, outfile, instance):
641 """Save this field to a file (for internal use only).
643 Parameters
644 ----------
645 outfile : file-like object
646 A writeable field handle.
647 instance : `~lsst.pex.config.Config`
648 The `~lsst.pex.config.Config` instance that contains this field.
650 Notes
651 -----
652 This method is invoked by the `~lsst.pex.config.Config` object that
653 contains this field and should not be called directly.
655 The output consists of the documentation string
656 (`lsst.pex.config.Field.doc`) formatted as a Python comment. The second
657 line is formatted as an assignment: ``{fullname}={value}``.
659 This output can be executed with Python.
660 """
661 value = self.__get__(instance)
662 fullname = _joinNamePath(instance._name, self.name)
664 if self.deprecated and value == self.default: 664 ↛ 665line 664 didn't jump to line 665 because the condition on line 664 was never true
665 return
667 # write full documentation string as comment lines
668 # (i.e. first character is #)
669 doc = "# " + str(self.doc).replace("\n", "\n# ")
670 if isinstance(value, float) and not math.isfinite(value): 670 ↛ 672line 670 didn't jump to line 672 because the condition on line 670 was never true
671 # non-finite numbers need special care
672 outfile.write(f"{doc}\n{fullname}=float('{value!r}')\n\n")
673 else:
674 outfile.write(f"{doc}\n{fullname}={value!r}\n\n")
676 def toDict(self, instance):
677 """Convert the field value so that it can be set as the value of an
678 item in a `dict` (for internal use only).
680 Parameters
681 ----------
682 instance : `~lsst.pex.config.Config`
683 The `~lsst.pex.config.Config` that contains this field.
685 Returns
686 -------
687 value : object
688 The field's value. See *Notes*.
690 Notes
691 -----
692 This method invoked by the owning `~lsst.pex.config.Config` object and
693 should not be called directly.
695 Simple values are passed through. Complex data structures must be
696 manipulated. For example, a `~lsst.pex.config.Field` holding a
697 subconfig should, instead of the subconfig object, return a `dict`
698 where the keys are the field names in the subconfig, and the values are
699 the field values in the subconfig.
700 """
701 return self.__get__(instance)
703 @overload
704 def __get__(
705 self, instance: None, owner: Any = None, at: Any = None, label: str = "default"
706 ) -> Field[FieldTypeVar]: ...
708 @overload
709 def __get__(
710 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
711 ) -> FieldTypeVar: ...
713 def __get__(self, instance, owner=None, at=None, label="default"):
714 """Define how attribute access should occur on the Config instance
715 This is invoked by the owning config object and should not be called
716 directly.
718 When the field attribute is accessed on a Config class object, it
719 returns the field object itself in order to allow inspection of
720 Config classes.
722 When the field attribute is access on a config instance, the actual
723 value described by the field (and held by the Config instance) is
724 returned.
725 """
726 if instance is None: 726 ↛ 727line 726 didn't jump to line 727 because the condition on line 726 was never true
727 return self
728 else:
729 # try statements are almost free in python if they succeed
730 try:
731 return instance._storage[self.name]
732 except AttributeError:
733 if not isinstance(instance, Config):
734 return self
735 else:
736 raise AttributeError(
737 f"Config {instance} is missing _storage attribute, likely incorrectly initialized"
738 ) from None
740 def __set__(
741 self, instance: Config, value: FieldTypeVar | None, at: Any = None, label: str = "assignment"
742 ) -> None:
743 """Set an attribute on the config instance.
745 Parameters
746 ----------
747 instance : `lsst.pex.config.Config`
748 The config instance that contains this field.
749 value : obj
750 Value to set on this field.
751 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`,\
752 optional
753 The call stack (created by
754 `lsst.pex.config.callStack.getCallStack`).
755 label : `str`, optional
756 Event label for the history.
758 Notes
759 -----
760 This method is invoked by the owning `lsst.pex.config.Config` object
761 and should not be called directly.
763 Derived `~lsst.pex.config.Field` classes may need to override the
764 behavior. When overriding ``__set__``, `~lsst.pex.config.Field` authors
765 should follow the following rules:
767 - Do not allow modification of frozen configs.
768 - Validate the new value **before** modifying the field. Except if the
769 new value is `None`. `None` is special and no attempt should be made
770 to validate it until `lsst.pex.config.Config.validate` is called.
771 - Do not modify the `~lsst.pex.config.Config` instance to contain
772 invalid values.
773 - If the field is modified, update the history of the
774 `lsst.pex.config.field.Field` to reflect the changes.
776 In order to decrease the need to implement this method in derived
777 `~lsst.pex.config.Field` types, value validation is performed in the
778 `lsst.pex.config.Field._validateValue`. If only the validation step
779 differs in the derived `~lsst.pex.config.Field`, it is simpler to
780 implement `lsst.pex.config.Field._validateValue` than to reimplement
781 ``__set__``. More complicated behavior, however, may require
782 reimplementation.
783 """
784 if instance._frozen: 784 ↛ 785line 784 didn't jump to line 785 because the condition on line 784 was never true
785 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
787 history = instance._history.setdefault(self.name, [])
788 if value is not None: 788 ↛ 795line 788 didn't jump to line 795 because the condition on line 788 was always true
789 value = _autocast(value, self.dtype)
790 try:
791 self._validateValue(value)
792 except BaseException as e:
793 raise FieldValidationError(self, instance, str(e)) from e
795 instance._storage[self.name] = value
796 if at is None: 796 ↛ 797line 796 didn't jump to line 797 because the condition on line 796 was never true
797 at = getCallStack()
798 history.append((value, at, label))
800 def __delete__(self, instance, at=None, label="deletion"):
801 """Delete an attribute from a `lsst.pex.config.Config` instance.
803 Parameters
804 ----------
805 instance : `lsst.pex.config.Config`
806 The config instance that contains this field.
807 at : `list` of `lsst.pex.config.callStack.StackFrame`
808 The call stack (created by
809 `lsst.pex.config.callStack.getCallStack`).
810 label : `str`, optional
811 Event label for the history.
813 Notes
814 -----
815 This is invoked by the owning `~lsst.pex.config.Config` object and
816 should not be called directly.
817 """
818 if at is None:
819 at = getCallStack()
820 self.__set__(instance, None, at=at, label=label)
822 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
823 """Compare a field (named `Field.name`) in two
824 `~lsst.pex.config.Config` instances for equality.
826 Parameters
827 ----------
828 instance1 : `lsst.pex.config.Config`
829 Left-hand side `Config` instance to compare.
830 instance2 : `lsst.pex.config.Config`
831 Right-hand side `Config` instance to compare.
832 shortcut : `bool`, optional
833 **Unused.**
834 rtol : `float`, optional
835 Relative tolerance for floating point comparisons.
836 atol : `float`, optional
837 Absolute tolerance for floating point comparisons.
838 output : callable, optional
839 A callable that takes a string, used (possibly repeatedly) to
840 report inequalities.
842 Notes
843 -----
844 This method must be overridden by more complex `Field` subclasses.
846 See Also
847 --------
848 lsst.pex.config.compareScalars
849 """
850 v1 = getattr(instance1, self.name)
851 v2 = getattr(instance2, self.name)
852 name = getComparisonName(
853 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
854 )
855 return compareScalars(name, v1, v2, dtype=self.dtype, rtol=rtol, atol=atol, output=output)
858class RecordingImporter:
859 """Importer (for `sys.meta_path`) that records which modules are being
860 imported.
862 *This class does not do any importing itself.*
864 Examples
865 --------
866 Use this class as a context manager to ensure it is properly uninstalled
867 when done:
869 >>> with RecordingImporter() as importer:
870 ... # import stuff
871 ... import numpy as np
872 ... print("Imported: " + importer.getModules())
873 """
875 def __init__(self):
876 self._modules = set()
878 def __enter__(self):
879 self.origMetaPath = sys.meta_path
880 sys.meta_path = [self] + sys.meta_path # type: ignore
881 return self
883 def __exit__(self, *args):
884 self.uninstall()
885 return False # Don't suppress exceptions
887 def uninstall(self):
888 """Uninstall the importer."""
889 sys.meta_path = self.origMetaPath
891 def find_spec(self, fullname, path, target=None):
892 """Find a module.
894 Called as part of the ``import`` chain of events.
896 Parameters
897 ----------
898 fullname : `str`
899 Name of module.
900 path : `list` [`str`]
901 Search path. Unused.
902 target : `~typing.Any`, optional
903 Unused.
904 """
905 self._modules.add(fullname)
906 # Return None because we don't do any importing.
907 return None
909 def getModules(self):
910 """Get the set of modules that were imported.
912 Returns
913 -------
914 modules : `set` of `str`
915 Set of imported module names.
916 """
917 return self._modules
920# type ignore because type checker thinks ConfigMeta is Generic when it is not
921class Config(metaclass=ConfigMeta): # type: ignore
922 """Base class for configuration (*config*) objects.
924 Notes
925 -----
926 A ``Config`` object will usually have several `~lsst.pex.config.Field`
927 instances as class attributes. These are used to define most of the base
928 class behavior.
930 ``Config`` implements a mapping API that provides many `dict`-like methods,
931 such as `keys`, `values`, and `items`. ``Config`` instances also support
932 the ``in`` operator to test if a field is in the config. Unlike a `dict`,
933 ``Config`` classes are not subscriptable. Instead, access individual
934 fields as attributes of the configuration instance.
936 Examples
937 --------
938 Config classes are subclasses of ``Config`` that have
939 `~lsst.pex.config.Field` instances (or instances of
940 `~lsst.pex.config.Field` subclasses) as class attributes:
942 >>> from lsst.pex.config import Config, Field, ListField
943 >>> class DemoConfig(Config):
944 ... intField = Field(doc="An integer field", dtype=int, default=42)
945 ... listField = ListField(
946 ... doc="List of favorite beverages.",
947 ... dtype=str,
948 ... default=["coffee", "green tea", "water"],
949 ... )
950 >>> config = DemoConfig()
952 Configs support many `dict`-like APIs:
954 >>> config.keys()
955 ['intField', 'listField']
956 >>> "intField" in config
957 True
959 Individual fields can be accessed as attributes of the configuration:
961 >>> config.intField
962 42
963 >>> config.listField.append("earl grey tea")
964 >>> print(config.listField)
965 ['coffee', 'green tea', 'water', 'earl grey tea']
966 """
968 _storage: dict[str, Any]
969 _fields: dict[str, Field]
970 _history: dict[str, list[Any]]
971 _imports: set[Any]
973 def __iter__(self):
974 """Iterate over fields."""
975 return self._fields.__iter__()
977 def keys(self):
978 """Get field names.
980 Returns
981 -------
982 names : `~collections.abc.KeysView`
983 List of `lsst.pex.config.Field` names.
984 """
985 return self._storage.keys()
987 def values(self):
988 """Get field values.
990 Returns
991 -------
992 values : `~collections.abc.ValuesView`
993 Iterator of field values.
994 """
995 return self._storage.values()
997 def items(self):
998 """Get configurations as ``(field name, field value)`` pairs.
1000 Returns
1001 -------
1002 items : `~collections.abc.ItemsView`
1003 Iterator of tuples for each configuration. Tuple items are:
1005 0. Field name.
1006 1. Field value.
1007 """
1008 return self._storage.items()
1010 def __contains__(self, name):
1011 """Return `True` if the specified field exists in this config.
1013 Parameters
1014 ----------
1015 name : `str`
1016 Field name to test for.
1018 Returns
1019 -------
1020 in : `bool`
1021 `True` if the specified field exists in the config.
1022 """
1023 return self._storage.__contains__(name)
1025 def __new__(cls, *args, **kw):
1026 """Allocate a new `lsst.pex.config.Config` object.
1028 In order to ensure that all Config object are always in a proper state
1029 when handed to users or to derived `~lsst.pex.config.Config` classes,
1030 some attributes are handled at allocation time rather than at
1031 initialization.
1033 This ensures that even if a derived `~lsst.pex.config.Config` class
1034 implements ``__init__``, its author does not need to be concerned about
1035 when or even the base ``Config.__init__`` should be called.
1036 """
1037 name = kw.pop("__name", None)
1038 at = kw.pop("__at", getCallStack())
1039 # remove __label and ignore it
1040 kw.pop("__label", "default")
1042 instance = object.__new__(cls)
1043 instance._frozen = False
1044 instance._name = name
1045 instance._storage = {}
1046 instance._history = {}
1047 instance._imports = set()
1048 # load up defaults
1049 for field in instance._fields.values():
1050 instance._history[field.name] = []
1051 field.__set__(instance, field.default, at=at + [field.source], label="default")
1052 # set custom default-overrides
1053 instance.setDefaults()
1054 # set constructor overrides
1055 instance.update(__at=at, **kw)
1056 return instance
1058 def __reduce__(self):
1059 """Reduction for pickling (function with arguments to reproduce).
1061 We need to condense and reconstitute the `~lsst.pex.config.Config`,
1062 since it may contain lambdas (as the ``check`` elements) that cannot
1063 be pickled.
1064 """
1065 # The stream must be in characters to match the API but pickle
1066 # requires bytes
1067 stream = io.StringIO()
1068 self.saveToStream(stream)
1069 return (unreduceConfig, (self.__class__, stream.getvalue().encode()))
1071 def setDefaults(self):
1072 """Subclass hook for computing defaults.
1074 Notes
1075 -----
1076 Derived `~lsst.pex.config.Config` classes that must compute defaults
1077 rather than using the `~lsst.pex.config.Field` instances's defaults
1078 should do so here. To correctly use inherited defaults,
1079 implementations of ``setDefaults`` must call their base class's
1080 ``setDefaults``.
1081 """
1082 pass
1084 def update(self, **kw):
1085 """Update values of fields specified by the keyword arguments.
1087 Parameters
1088 ----------
1089 **kw
1090 Keywords are configuration field names. Values are configuration
1091 field values.
1093 Notes
1094 -----
1095 The ``__at`` and ``__label`` keyword arguments are special internal
1096 keywords. They are used to strip out any internal steps from the
1097 history tracebacks of the config. Do not modify these keywords to
1098 subvert a `~lsst.pex.config.Config` instance's history.
1100 Examples
1101 --------
1102 This is a config with three fields:
1104 >>> from lsst.pex.config import Config, Field
1105 >>> class DemoConfig(Config):
1106 ... fieldA = Field(doc="Field A", dtype=int, default=42)
1107 ... fieldB = Field(doc="Field B", dtype=bool, default=True)
1108 ... fieldC = Field(doc="Field C", dtype=str, default="Hello world")
1109 >>> config = DemoConfig()
1111 These are the default values of each field:
1113 >>> for name, value in config.iteritems():
1114 ... print(f"{name}: {value}")
1115 fieldA: 42
1116 fieldB: True
1117 fieldC: 'Hello world'
1119 Using this method to update ``fieldA`` and ``fieldC``:
1121 >>> config.update(fieldA=13, fieldC="Updated!")
1123 Now the values of each field are:
1125 >>> for name, value in config.iteritems():
1126 ... print(f"{name}: {value}")
1127 fieldA: 13
1128 fieldB: True
1129 fieldC: 'Updated!'
1130 """
1131 at = kw.pop("__at", getCallStack())
1132 label = kw.pop("__label", "update")
1134 for name, value in kw.items():
1135 try:
1136 field = self._fields[name]
1137 field.__set__(self, value, at=at, label=label)
1138 except KeyError as e:
1139 e.add_note(f"No field of name {name} exists in config type {_typeStr(self)}")
1140 raise
1142 def load(self, filename, root="config"):
1143 """Modify this config in place by executing the Python code in a
1144 configuration file.
1146 Parameters
1147 ----------
1148 filename : `str`
1149 Name of the configuration file. A configuration file is Python
1150 module.
1151 root : `str`, optional
1152 Name of the variable in file that refers to the config being
1153 overridden.
1155 For example, the value of root is ``"config"`` and the file
1156 contains::
1158 config.myField = 5
1160 Then this config's field ``myField`` is set to ``5``.
1162 See Also
1163 --------
1164 lsst.pex.config.Config.loadFromStream
1165 lsst.pex.config.Config.loadFromString
1166 lsst.pex.config.Config.save
1167 lsst.pex.config.Config.saveToStream
1168 lsst.pex.config.Config.saveToString
1169 """
1170 with open(filename) as f:
1171 code = compile(f.read(), filename=filename, mode="exec")
1172 self.loadFromString(code, root=root, filename=filename)
1174 def loadFromStream(self, stream, root="config", filename=None, extraLocals=None):
1175 """Modify this Config in place by executing the Python code in the
1176 provided stream.
1178 Parameters
1179 ----------
1180 stream : file-like object, `str`, `bytes`, or `~types.CodeType`
1181 Stream containing configuration override code. If this is a
1182 code object, it should be compiled with ``mode="exec"``.
1183 root : `str`, optional
1184 Name of the variable in file that refers to the config being
1185 overridden.
1187 For example, the value of root is ``"config"`` and the file
1188 contains::
1190 config.myField = 5
1192 Then this config's field ``myField`` is set to ``5``.
1193 filename : `str`, optional
1194 Name of the configuration file, or `None` if unknown or contained
1195 in the stream. Used for error reporting.
1196 extraLocals : `dict` of `str` to `object`, optional
1197 Any extra variables to include in local scope when loading.
1199 Notes
1200 -----
1201 For backwards compatibility reasons, this method accepts strings, bytes
1202 and code objects as well as file-like objects. New code should use
1203 `loadFromString` instead for most of these types.
1205 See Also
1206 --------
1207 lsst.pex.config.Config.load
1208 lsst.pex.config.Config.loadFromString
1209 lsst.pex.config.Config.save
1210 lsst.pex.config.Config.saveToStream
1211 lsst.pex.config.Config.saveToString
1212 """
1213 if hasattr(stream, "read"): 1213 ↛ 1214line 1213 didn't jump to line 1214 because the condition on line 1213 was never true
1214 if filename is None:
1215 filename = getattr(stream, "name", "?")
1216 code = compile(stream.read(), filename=filename, mode="exec")
1217 else:
1218 code = stream
1219 self.loadFromString(code, root=root, filename=filename, extraLocals=extraLocals)
1221 def loadFromString(self, code, root="config", filename=None, extraLocals=None):
1222 """Modify this Config in place by executing the Python code in the
1223 provided string.
1225 Parameters
1226 ----------
1227 code : `str`, `bytes`, or `~types.CodeType`
1228 Stream containing configuration override code.
1229 root : `str`, optional
1230 Name of the variable in file that refers to the config being
1231 overridden.
1233 For example, the value of root is ``"config"`` and the file
1234 contains::
1236 config.myField = 5
1238 Then this config's field ``myField`` is set to ``5``.
1239 filename : `str`, optional
1240 Name of the configuration file, or `None` if unknown or contained
1241 in the stream. Used for error reporting.
1242 extraLocals : `dict` of `str` to `object`, optional
1243 Any extra variables to include in local scope when loading.
1245 Raises
1246 ------
1247 ValueError
1248 Raised if a key in extraLocals is the same value as the value of
1249 the root argument.
1251 See Also
1252 --------
1253 lsst.pex.config.Config.load
1254 lsst.pex.config.Config.loadFromStream
1255 lsst.pex.config.Config.save
1256 lsst.pex.config.Config.saveToStream
1257 lsst.pex.config.Config.saveToString
1258 """
1259 if filename is None: 1259 ↛ 1263line 1259 didn't jump to line 1263 because the condition on line 1259 was always true
1260 # try to determine the file name; a compiled string
1261 # has attribute "co_filename",
1262 filename = getattr(code, "co_filename", "?")
1263 with RecordingImporter() as importer:
1264 globals = {"__file__": filename}
1265 local = {root: self}
1266 if extraLocals is not None: 1266 ↛ 1268line 1266 didn't jump to line 1268 because the condition on line 1266 was never true
1267 # verify the value of root was not passed as extra local args
1268 if root in extraLocals:
1269 raise ValueError(
1270 f"{root} is reserved and cannot be used as a variable name in extraLocals"
1271 )
1272 local.update(extraLocals)
1273 exec(code, globals, local)
1275 self._imports.update(importer.getModules())
1277 def save(self, filename, root="config"):
1278 """Save a Python script to the named file, which, when loaded,
1279 reproduces this config.
1281 Parameters
1282 ----------
1283 filename : `str`
1284 Desination filename of this configuration.
1285 root : `str`, optional
1286 Name to use for the root config variable. The same value must be
1287 used when loading (see `lsst.pex.config.Config.load`).
1289 See Also
1290 --------
1291 lsst.pex.config.Config.saveToStream
1292 lsst.pex.config.Config.saveToString
1293 lsst.pex.config.Config.load
1294 lsst.pex.config.Config.loadFromStream
1295 lsst.pex.config.Config.loadFromString
1296 """
1297 d = os.path.dirname(filename)
1298 with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=d) as outfile:
1299 self.saveToStream(outfile, root)
1300 # tempfile is hardcoded to create files with mode '0600'
1301 # for an explantion of these antics see:
1302 # https://stackoverflow.com/questions/10291131/how-to-use-os-umask-in-python
1303 umask = os.umask(0o077)
1304 os.umask(umask)
1305 os.chmod(outfile.name, (~umask & 0o666))
1306 # chmod before the move so we get quasi-atomic behavior if the
1307 # source and dest. are on the same filesystem.
1308 # os.rename may not work across filesystems
1309 shutil.move(outfile.name, filename)
1311 def saveToString(self, skipImports=False):
1312 """Return the Python script form of this configuration as an executable
1313 string.
1315 Parameters
1316 ----------
1317 skipImports : `bool`, optional
1318 If `True` then do not include ``import`` statements in output,
1319 this is to support human-oriented output from ``pipetask`` where
1320 additional clutter is not useful.
1322 Returns
1323 -------
1324 code : `str`
1325 A code string readable by `loadFromString`.
1327 See Also
1328 --------
1329 lsst.pex.config.Config.save
1330 lsst.pex.config.Config.saveToStream
1331 lsst.pex.config.Config.load
1332 lsst.pex.config.Config.loadFromStream
1333 lsst.pex.config.Config.loadFromString
1334 """
1335 buffer = io.StringIO()
1336 self.saveToStream(buffer, skipImports=skipImports)
1337 return buffer.getvalue()
1339 def saveToStream(self, outfile, root="config", skipImports=False):
1340 """Save a configuration file to a stream, which, when loaded,
1341 reproduces this config.
1343 Parameters
1344 ----------
1345 outfile : file-like object
1346 Destination file object write the config into. Accepts strings not
1347 bytes.
1348 root : `str`, optional
1349 Name to use for the root config variable. The same value must be
1350 used when loading (see `lsst.pex.config.Config.load`).
1351 skipImports : `bool`, optional
1352 If `True` then do not include ``import`` statements in output,
1353 this is to support human-oriented output from ``pipetask`` where
1354 additional clutter is not useful.
1356 See Also
1357 --------
1358 lsst.pex.config.Config.save
1359 lsst.pex.config.Config.saveToString
1360 lsst.pex.config.Config.load
1361 lsst.pex.config.Config.loadFromStream
1362 lsst.pex.config.Config.loadFromString
1363 """
1364 tmp = self._name
1365 self._rename(root)
1366 try:
1367 if not skipImports: 1367 ↛ 1384line 1367 didn't jump to line 1384 because the condition on line 1367 was always true
1368 self._collectImports()
1369 # Remove self from the set, as it is handled explicitly below
1370 self._imports.remove(self.__module__)
1371 configType = type(self)
1372 typeString = _typeStr(configType)
1373 outfile.write(f"import {configType.__module__}\n")
1374 # We are required to write this on a single line because
1375 # of later regex matching, rather than adopting black style
1376 # formatting.
1377 outfile.write(
1378 f'assert type({root}) is {typeString}, f"config is of type '
1379 f'{{type({root}).__module__}}.{{type({root}).__name__}} instead of {typeString}"\n\n'
1380 )
1381 for imp in sorted(self._imports): 1381 ↛ 1382line 1381 didn't jump to line 1382 because the loop on line 1381 never started
1382 if imp in sys.modules and sys.modules[imp] is not None:
1383 outfile.write(f"import {imp}\n")
1384 self._save(outfile)
1385 finally:
1386 self._rename(tmp)
1388 def freeze(self):
1389 """Make this config, and all subconfigs, read-only."""
1390 self._frozen = True
1391 for field in self._fields.values():
1392 field.freeze(self)
1394 def _save(self, outfile):
1395 """Save this config to an open stream object.
1397 Parameters
1398 ----------
1399 outfile : file-like object
1400 Destination file object write the config into. Accepts strings not
1401 bytes.
1402 """
1403 for field in self._fields.values():
1404 field.save(outfile, self)
1406 def _collectImports(self):
1407 """Add module containing self to the list of things to import and
1408 then loops over all the fields in the config calling a corresponding
1409 collect method.
1411 The field method will call _collectImports on any
1412 configs it may own and return the set of things to import. This
1413 returned set will be merged with the set of imports for this config
1414 class.
1415 """
1416 self._imports.add(self.__module__)
1417 for field in self._fields.values():
1418 field._collectImports(self, self._imports)
1420 def toDict(self):
1421 """Make a dictionary of field names and their values.
1423 Returns
1424 -------
1425 dict_ : `dict`
1426 Dictionary with keys that are `~lsst.pex.config.Field` names.
1427 Values are `~lsst.pex.config.Field` values.
1429 See Also
1430 --------
1431 lsst.pex.config.Field.toDict
1433 Notes
1434 -----
1435 This method uses the `~lsst.pex.config.Field.toDict` method of
1436 individual fields. Subclasses of `~lsst.pex.config.Field` may need to
1437 implement a ``toDict`` method for *this* method to work.
1438 """
1439 dict_ = {}
1440 for name, field in self._fields.items():
1441 dict_[name] = field.toDict(self)
1442 return dict_
1444 def names(self):
1445 """Get all the field names in the config, recursively.
1447 Returns
1448 -------
1449 names : `list` of `str`
1450 Field names.
1451 """
1452 #
1453 # Rather than sort out the recursion all over again use the
1454 # pre-existing saveToStream()
1455 #
1456 with io.StringIO() as strFd:
1457 self.saveToStream(strFd, "config")
1458 contents = strFd.getvalue()
1459 strFd.close()
1460 #
1461 # Pull the names out of the dumped config
1462 #
1463 keys = []
1464 for line in contents.split("\n"):
1465 if re.search(r"^((assert|import)\s+|\s*$|#)", line):
1466 continue
1468 mat = re.search(r"^(?:config\.)?([^=]+)\s*=\s*.*", line)
1469 if mat:
1470 keys.append(mat.group(1))
1472 return keys
1474 def _rename(self, name):
1475 """Rename this config object in its parent `~lsst.pex.config.Config`.
1477 Parameters
1478 ----------
1479 name : `str`
1480 New name for this config in its parent `~lsst.pex.config.Config`.
1482 Notes
1483 -----
1484 This method uses the `~lsst.pex.config.Field.rename` method of
1485 individual `lsst.pex.config.Field` instances.
1486 `lsst.pex.config.Field` subclasses may need to implement a ``rename``
1487 method for *this* method to work.
1489 See Also
1490 --------
1491 lsst.pex.config.Field.rename
1492 """
1493 self._name = name
1494 for field in self._fields.values():
1495 field.rename(self)
1497 def validate(self):
1498 """Validate the Config, raising an exception if invalid.
1500 Raises
1501 ------
1502 lsst.pex.config.FieldValidationError
1503 Raised if verification fails.
1505 Notes
1506 -----
1507 The base class implementation performs type checks on all fields by
1508 calling their `~lsst.pex.config.Field.validate` methods.
1510 Complex single-field validation can be defined by deriving new Field
1511 types. For convenience, some derived `lsst.pex.config.Field`-types
1512 (`~lsst.pex.config.ConfigField` and
1513 `~lsst.pex.config.ConfigChoiceField`) are defined in
1514 ``lsst.pex.config`` that handle recursing into subconfigs.
1516 Inter-field relationships should only be checked in derived
1517 `~lsst.pex.config.Config` classes after calling this method, and base
1518 validation is complete.
1519 """
1520 for field in self._fields.values():
1521 field.validate(self)
1523 def formatHistory(self, name, **kwargs):
1524 """Format a configuration field's history to a human-readable format.
1526 Parameters
1527 ----------
1528 name : `str`
1529 Name of a `~lsst.pex.config.Field` in this config.
1530 **kwargs
1531 Keyword arguments passed to `lsst.pex.config.history.format`.
1533 Returns
1534 -------
1535 history : `str`
1536 A string containing the formatted history.
1538 See Also
1539 --------
1540 lsst.pex.config.history.format
1541 """
1542 import lsst.pex.config.history as pexHist
1544 return pexHist.format(self, name, **kwargs)
1546 history = property(lambda x: x._history)
1547 """Read-only history.
1548 """
1550 def __setattr__(self, attr, value, at=None, label="assignment"):
1551 """Set an attribute (such as a field's value).
1553 Notes
1554 -----
1555 Unlike normal Python objects, `~lsst.pex.config.Config` objects are
1556 locked such that no additional attributes nor properties may be added
1557 to them dynamically.
1559 Although this is not the standard Python behavior, it helps to protect
1560 users from accidentally mispelling a field name, or trying to set a
1561 non-existent field.
1562 """
1563 if attr in self._fields:
1564 if self._fields[attr].deprecated is not None: 1564 ↛ 1565line 1564 didn't jump to line 1565 because the condition on line 1564 was never true
1565 fullname = _joinNamePath(self._name, self._fields[attr].name)
1566 warnings.warn(
1567 f"Config field {fullname} is deprecated: {self._fields[attr].deprecated}",
1568 FutureWarning,
1569 stacklevel=2,
1570 )
1571 if at is None: 1571 ↛ 1574line 1571 didn't jump to line 1574 because the condition on line 1571 was always true
1572 at = getCallStack()
1573 # This allows Field descriptors to work.
1574 self._fields[attr].__set__(self, value, at=at, label=label)
1575 elif hasattr(getattr(self.__class__, attr, None), "__set__"): 1575 ↛ 1577line 1575 didn't jump to line 1577 because the condition on line 1575 was never true
1576 # This allows properties and other non-Field descriptors to work.
1577 return object.__setattr__(self, attr, value)
1578 elif attr in self.__dict__ or attr in ("_name", "_history", "_storage", "_frozen", "_imports"): 1578 ↛ 1583line 1578 didn't jump to line 1583 because the condition on line 1578 was always true
1579 # This allows specific private attributes to work.
1580 self.__dict__[attr] = value
1581 else:
1582 # We throw everything else.
1583 raise AttributeError(f"{_typeStr(self)} has no attribute {attr}")
1585 def __delattr__(self, attr, at=None, label="deletion"):
1586 if attr in self._fields:
1587 if at is None:
1588 at = getCallStack()
1589 self._fields[attr].__delete__(self, at=at, label=label)
1590 else:
1591 object.__delattr__(self, attr)
1593 def __eq__(self, other):
1594 if type(other) is type(self): 1594 ↛ 1595line 1594 didn't jump to line 1595 because the condition on line 1594 was never true
1595 for name in self._fields:
1596 thisValue = getattr(self, name)
1597 otherValue = getattr(other, name)
1598 if isinstance(thisValue, float) and math.isnan(thisValue):
1599 if not math.isnan(otherValue):
1600 return False
1601 elif thisValue != otherValue:
1602 return False
1603 return True
1604 return False
1606 def __ne__(self, other):
1607 return not self.__eq__(other)
1609 def __str__(self):
1610 return str(self.toDict())
1612 def __repr__(self):
1613 return "{}({})".format(
1614 _typeStr(self),
1615 ", ".join(f"{k}={v!r}" for k, v in self.toDict().items() if v is not None),
1616 )
1618 def compare(self, other, shortcut=True, rtol=1e-8, atol=1e-8, output=None):
1619 """Compare this configuration to another `~lsst.pex.config.Config` for
1620 equality.
1622 Parameters
1623 ----------
1624 other : `lsst.pex.config.Config`
1625 Other `~lsst.pex.config.Config` object to compare against this
1626 config.
1627 shortcut : `bool`, optional
1628 If `True`, return as soon as an inequality is found. Default is
1629 `True`.
1630 rtol : `float`, optional
1631 Relative tolerance for floating point comparisons.
1632 atol : `float`, optional
1633 Absolute tolerance for floating point comparisons.
1634 output : callable, optional
1635 A callable that takes a string, used (possibly repeatedly) to
1636 report inequalities.
1638 Returns
1639 -------
1640 isEqual : `bool`
1641 `True` when the two `lsst.pex.config.Config` instances are equal.
1642 `False` if there is an inequality.
1644 See Also
1645 --------
1646 lsst.pex.config.compareConfigs
1648 Notes
1649 -----
1650 Unselected targets of `~lsst.pex.config.RegistryField` fields and
1651 unselected choices of `~lsst.pex.config.ConfigChoiceField` fields
1652 are not considered by this method.
1654 Floating point comparisons are performed by `numpy.allclose`.
1655 """
1656 name1 = self._name if self._name is not None else "config"
1657 name2 = other._name if other._name is not None else "config"
1658 name = getComparisonName(name1, name2)
1659 return compareConfigs(name, self, other, shortcut=shortcut, rtol=rtol, atol=atol, output=output)
1661 @classmethod
1662 def __init_subclass__(cls, **kwargs):
1663 """Run initialization for every subclass.
1665 Specifically registers the subclass with a YAML representer
1666 and YAML constructor (if pyyaml is available)
1667 """
1668 super().__init_subclass__(**kwargs)
1670 if not yaml: 1670 ↛ 1671line 1670 didn't jump to line 1671 because the condition on line 1670 was never true
1671 return
1673 yaml.add_representer(cls, _yaml_config_representer)
1675 @classmethod
1676 def _fromPython(cls, config_py):
1677 """Instantiate a `Config`-subclass from serialized Python form.
1679 Parameters
1680 ----------
1681 config_py : `str`
1682 A serialized form of the Config as created by
1683 `Config.saveToStream`.
1685 Returns
1686 -------
1687 config : `Config`
1688 Reconstructed `Config` instant.
1689 """
1690 cls = _classFromPython(config_py)
1691 return unreduceConfig(cls, config_py)
1694def _classFromPython(config_py):
1695 """Return the Config subclass required by this Config serialization.
1697 Parameters
1698 ----------
1699 config_py : `str`
1700 A serialized form of the Config as created by
1701 `Config.saveToStream`.
1703 Returns
1704 -------
1705 cls : `type`
1706 The `Config` subclass associated with this config.
1707 """
1708 # standard serialization has the form:
1709 # import config.class
1710 # assert type(config) is config.class.Config, ...
1711 # Older files use "type(config)==" instead.
1712 # We want to parse these two lines so we can get the class itself
1714 # Do a single regex to avoid large string copies when splitting a
1715 # large config into separate lines.
1716 # The assert regex cannot be greedy because the assert error string
1717 # can include both "," and " is ".
1718 matches = re.search(r"^import ([\w.]+)\nassert type\(\S+\)(?:\s*==\s*| is )(.*?),", config_py)
1720 if not matches:
1721 first_line, second_line, _ = config_py.split("\n", 2)
1722 raise ValueError(
1723 f"First two lines did not match expected form. Got:\n - {first_line}\n - {second_line}"
1724 )
1726 module_name = matches.group(1)
1727 module = importlib.import_module(module_name)
1729 # Second line
1730 full_name = matches.group(2)
1732 # Remove the module name from the full name
1733 if not full_name.startswith(module_name):
1734 raise ValueError(f"Module name ({module_name}) inconsistent with full name ({full_name})")
1736 # if module name is a.b.c and full name is a.b.c.d.E then
1737 # we need to remove a.b.c. and iterate over the remainder
1738 # The +1 is for the extra dot after a.b.c
1739 remainder = full_name[len(module_name) + 1 :]
1740 components = remainder.split(".")
1741 pytype = module
1742 for component in components:
1743 pytype = getattr(pytype, component)
1744 return pytype
1747def unreduceConfig(cls_, stream):
1748 """Create a `~lsst.pex.config.Config` from a stream.
1750 Parameters
1751 ----------
1752 cls_ : `lsst.pex.config.Config`-type
1753 A `lsst.pex.config.Config` type (not an instance) that is instantiated
1754 with configurations in the ``stream``.
1755 stream : file-like object, `str`, or `~types.CodeType`
1756 Stream containing configuration override code.
1758 Returns
1759 -------
1760 config : `lsst.pex.config.Config`
1761 Config instance.
1763 See Also
1764 --------
1765 lsst.pex.config.Config.loadFromStream
1766 """
1767 config = cls_()
1768 config.loadFromStream(stream)
1769 return config