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