Coverage for python/lsst/pex/config/config.py: 60%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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/>.
28__all__ = ("Config", "ConfigMeta", "Field", "FieldValidationError")
30import copy
31import importlib
32import io
33import math
34import os
35import re
36import shutil
37import sys
38import tempfile
39import warnings
41# if YAML is not available that's fine and we simply don't register
42# the yaml representer since we know it won't be used.
43try:
44 import yaml
45except ImportError:
46 yaml = None
47 YamlLoaders = ()
48 doImport = None
50from .callStack import getCallStack, getStackFrame
51from .comparison import compareConfigs, compareScalars, getComparisonName
53if yaml: 53 ↛ 65line 53 didn't jump to line 65, because the condition on line 53 was never false
54 YamlLoaders = (yaml.Loader, yaml.FullLoader, yaml.SafeLoader, yaml.UnsafeLoader)
56 try:
57 # CLoader is not always available
58 from yaml import CLoader
60 YamlLoaders += (CLoader,)
61 except ImportError:
62 pass
65def _joinNamePath(prefix=None, name=None, index=None):
66 """Generate nested configuration names."""
67 if not prefix and not name: 67 ↛ 68line 67 didn't jump to line 68, because the condition on line 67 was never true
68 raise ValueError("Invalid name: cannot be None")
69 elif not name: 69 ↛ 70line 69 didn't jump to line 70, because the condition on line 69 was never true
70 name = prefix
71 elif prefix and name: 71 ↛ 74line 71 didn't jump to line 74, because the condition on line 71 was never false
72 name = prefix + "." + name
74 if index is not None: 74 ↛ 75line 74 didn't jump to line 75, because the condition on line 74 was never true
75 return "%s[%r]" % (name, index)
76 else:
77 return name
80def _autocast(x, dtype):
81 """Cast a value to a type, if appropriate.
83 Parameters
84 ----------
85 x : object
86 A value.
87 dtype : tpye
88 Data type, such as `float`, `int`, or `str`.
90 Returns
91 -------
92 values : object
93 If appropriate, the returned value is ``x`` cast to the given type
94 ``dtype``. If the cast cannot be performed the original value of
95 ``x`` is returned.
96 """
97 if dtype == float and isinstance(x, int):
98 return float(x)
99 return x
102def _typeStr(x):
103 """Generate a fully-qualified type name.
105 Returns
106 -------
107 `str`
108 Fully-qualified type name.
110 Notes
111 -----
112 This function is used primarily for writing config files to be executed
113 later upon with the 'load' function.
114 """
115 if hasattr(x, "__module__") and hasattr(x, "__name__"):
116 xtype = x
117 else:
118 xtype = type(x)
119 if (sys.version_info.major <= 2 and xtype.__module__ == "__builtin__") or xtype.__module__ == "builtins": 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true
120 return xtype.__name__
121 else:
122 return "%s.%s" % (xtype.__module__, xtype.__name__)
125if yaml: 125 ↛ 158line 125 didn't jump to line 158, because the condition on line 125 was never false
127 def _yaml_config_representer(dumper, data):
128 """Represent a Config object in a form suitable for YAML.
130 Stores the serialized stream as a scalar block string.
131 """
132 stream = io.StringIO()
133 data.saveToStream(stream)
134 config_py = stream.getvalue()
136 # Strip multiple newlines from the end of the config
137 # This simplifies the YAML to use | and not |+
138 config_py = config_py.rstrip() + "\n"
140 # Trailing spaces force pyyaml to use non-block form.
141 # Remove the trailing spaces so it has no choice
142 config_py = re.sub(r"\s+$", "\n", config_py, flags=re.MULTILINE)
144 # Store the Python as a simple scalar
145 return dumper.represent_scalar("lsst.pex.config.Config", config_py, style="|")
147 def _yaml_config_constructor(loader, node):
148 """Construct a config from YAML"""
149 config_py = loader.construct_scalar(node)
150 return Config._fromPython(config_py)
152 # Register a generic constructor for Config and all subclasses
153 # Need to register for all the loaders we would like to use
154 for loader in YamlLoaders:
155 yaml.add_constructor("lsst.pex.config.Config", _yaml_config_constructor, Loader=loader)
158class ConfigMeta(type):
159 """A metaclass for `lsst.pex.config.Config`.
161 Notes
162 -----
163 ``ConfigMeta`` adds a dictionary containing all `~lsst.pex.config.Field`
164 class attributes as a class attribute called ``_fields``, and adds
165 the name of each field as an instance variable of the field itself (so you
166 don't have to pass the name of the field to the field constructor).
167 """
169 def __init__(cls, name, bases, dict_):
170 type.__init__(cls, name, bases, dict_)
171 cls._fields = {}
172 cls._source = getStackFrame()
174 def getFields(classtype):
175 fields = {}
176 bases = list(classtype.__bases__)
177 bases.reverse()
178 for b in bases:
179 fields.update(getFields(b))
181 for k, v in classtype.__dict__.items():
182 if isinstance(v, Field):
183 fields[k] = v
184 return fields
186 fields = getFields(cls)
187 for k, v in fields.items():
188 setattr(cls, k, copy.deepcopy(v))
190 def __setattr__(cls, name, value):
191 if isinstance(value, Field):
192 value.name = name
193 cls._fields[name] = value
194 type.__setattr__(cls, name, value)
197class FieldValidationError(ValueError):
198 """Raised when a ``~lsst.pex.config.Field`` is not valid in a
199 particular ``~lsst.pex.config.Config``.
201 Parameters
202 ----------
203 field : `lsst.pex.config.Field`
204 The field that was not valid.
205 config : `lsst.pex.config.Config`
206 The config containing the invalid field.
207 msg : `str`
208 Text describing why the field was not valid.
209 """
211 def __init__(self, field, config, msg):
212 self.fieldType = type(field)
213 """Type of the `~lsst.pex.config.Field` that incurred the error.
214 """
216 self.fieldName = field.name
217 """Name of the `~lsst.pex.config.Field` instance that incurred the
218 error (`str`).
220 See also
221 --------
222 lsst.pex.config.Field.name
223 """
225 self.fullname = _joinNamePath(config._name, field.name)
226 """Fully-qualified name of the `~lsst.pex.config.Field` instance
227 (`str`).
228 """
230 self.history = config.history.setdefault(field.name, [])
231 """Full history of all changes to the `~lsst.pex.config.Field`
232 instance.
233 """
235 self.fieldSource = field.source
236 """File and line number of the `~lsst.pex.config.Field` definition.
237 """
239 self.configSource = config._source
240 error = (
241 "%s '%s' failed validation: %s\n"
242 "For more information see the Field definition at:\n%s"
243 " and the Config definition at:\n%s"
244 % (
245 self.fieldType.__name__,
246 self.fullname,
247 msg,
248 self.fieldSource.format(),
249 self.configSource.format(),
250 )
251 )
252 super().__init__(error)
255class Field:
256 """A field in a `~lsst.pex.config.Config` that supports `int`, `float`,
257 `complex`, `bool`, and `str` data types.
259 Parameters
260 ----------
261 doc : `str`
262 A description of the field for users.
263 dtype : type
264 The field's data type. ``Field`` only supports basic data types:
265 `int`, `float`, `complex`, `bool`, and `str`. See
266 `Field.supportedTypes`.
267 default : object, optional
268 The field's default value.
269 check : callable, optional
270 A callable that is called with the field's value. This callable should
271 return `False` if the value is invalid. More complex inter-field
272 validation can be written as part of the
273 `lsst.pex.config.Config.validate` method.
274 optional : `bool`, optional
275 This sets whether the field is considered optional, and therefore
276 doesn't need to be set by the user. When `False`,
277 `lsst.pex.config.Config.validate` fails if the field's value is `None`.
278 deprecated : None or `str`, optional
279 A description of why this Field is deprecated, including removal date.
280 If not None, the string is appended to the docstring for this Field.
282 Raises
283 ------
284 ValueError
285 Raised when the ``dtype`` parameter is not one of the supported types
286 (see `Field.supportedTypes`).
288 See also
289 --------
290 ChoiceField
291 ConfigChoiceField
292 ConfigDictField
293 ConfigField
294 ConfigurableField
295 DictField
296 ListField
297 RangeField
298 RegistryField
300 Notes
301 -----
302 ``Field`` instances (including those of any subclass of ``Field``) are used
303 as class attributes of `~lsst.pex.config.Config` subclasses (see the
304 example, below). ``Field`` attributes work like the `property` attributes
305 of classes that implement custom setters and getters. `Field` attributes
306 belong to the class, but operate on the instance. Formally speaking,
307 `Field` attributes are `descriptors
308 <https://docs.python.org/3/howto/descriptor.html>`_.
310 When you access a `Field` attribute on a `Config` instance, you don't
311 get the `Field` instance itself. Instead, you get the value of that field,
312 which might be a simple type (`int`, `float`, `str`, `bool`) or a custom
313 container type (like a `lsst.pex.config.List`) depending on the field's
314 type. See the example, below.
316 Examples
317 --------
318 Instances of ``Field`` should be used as class attributes of
319 `lsst.pex.config.Config` subclasses:
321 >>> from lsst.pex.config import Config, Field
322 >>> class Example(Config):
323 ... myInt = Field("An integer field.", int, default=0)
324 ...
325 >>> print(config.myInt)
326 0
327 >>> config.myInt = 5
328 >>> print(config.myInt)
329 5
330 """
332 supportedTypes = set((str, bool, float, int, complex))
333 """Supported data types for field values (`set` of types).
334 """
336 def __init__(self, doc, dtype, default=None, check=None, optional=False, deprecated=None):
337 if dtype not in self.supportedTypes: 337 ↛ 338line 337 didn't jump to line 338, because the condition on line 337 was never true
338 raise ValueError("Unsupported Field dtype %s" % _typeStr(dtype))
340 source = getStackFrame()
341 self._setup(
342 doc=doc,
343 dtype=dtype,
344 default=default,
345 check=check,
346 optional=optional,
347 source=source,
348 deprecated=deprecated,
349 )
351 def _setup(self, doc, dtype, default, check, optional, source, deprecated):
352 """Set attributes, usually during initialization."""
353 self.dtype = dtype
354 """Data type for the field.
355 """
357 # append the deprecation message to the docstring.
358 if deprecated is not None:
359 doc = f"{doc} Deprecated: {deprecated}"
360 self.doc = doc
361 """A description of the field (`str`).
362 """
364 self.deprecated = deprecated
365 """If not None, a description of why this field is deprecated (`str`).
366 """
368 self.__doc__ = f"{doc} (`{dtype.__name__}`"
369 if optional or default is not None:
370 self.__doc__ += f", default ``{default!r}``"
371 self.__doc__ += ")"
373 self.default = default
374 """Default value for this field.
375 """
377 self.check = check
378 """A user-defined function that validates the value of the field.
379 """
381 self.optional = optional
382 """Flag that determines if the field is required to be set (`bool`).
384 When `False`, `lsst.pex.config.Config.validate` will fail if the
385 field's value is `None`.
386 """
388 self.source = source
389 """The stack frame where this field is defined (`list` of
390 `lsst.pex.config.callStack.StackFrame`).
391 """
393 def rename(self, instance):
394 """Rename the field in a `~lsst.pex.config.Config` (for internal use
395 only).
397 Parameters
398 ----------
399 instance : `lsst.pex.config.Config`
400 The config instance that contains this field.
402 Notes
403 -----
404 This method is invoked by the `lsst.pex.config.Config` object that
405 contains this field and should not be called directly.
407 Renaming is only relevant for `~lsst.pex.config.Field` instances that
408 hold subconfigs. `~lsst.pex.config.Fields` that hold subconfigs should
409 rename each subconfig with the full field name as generated by
410 `lsst.pex.config.config._joinNamePath`.
411 """
412 pass
414 def validate(self, instance):
415 """Validate the field (for internal use only).
417 Parameters
418 ----------
419 instance : `lsst.pex.config.Config`
420 The config instance that contains this field.
422 Raises
423 ------
424 lsst.pex.config.FieldValidationError
425 Raised if verification fails.
427 Notes
428 -----
429 This method provides basic validation:
431 - Ensures that the value is not `None` if the field is not optional.
432 - Ensures type correctness.
433 - Ensures that the user-provided ``check`` function is valid.
435 Most `~lsst.pex.config.Field` subclasses should call
436 `lsst.pex.config.field.Field.validate` if they re-implement
437 `~lsst.pex.config.field.Field.validate`.
438 """
439 value = self.__get__(instance)
440 if not self.optional and value is None:
441 raise FieldValidationError(self, instance, "Required value cannot be None")
443 def freeze(self, instance):
444 """Make this field read-only (for internal use only).
446 Parameters
447 ----------
448 instance : `lsst.pex.config.Config`
449 The config instance that contains this field.
451 Notes
452 -----
453 Freezing is only relevant for fields that hold subconfigs. Fields which
454 hold subconfigs should freeze each subconfig.
456 **Subclasses should implement this method.**
457 """
458 pass
460 def _validateValue(self, value):
461 """Validate a value.
463 Parameters
464 ----------
465 value : object
466 The value being validated.
468 Raises
469 ------
470 TypeError
471 Raised if the value's type is incompatible with the field's
472 ``dtype``.
473 ValueError
474 Raised if the value is rejected by the ``check`` method.
475 """
476 if value is None: 476 ↛ 477line 476 didn't jump to line 477, because the condition on line 476 was never true
477 return
479 if not isinstance(value, self.dtype): 479 ↛ 480line 479 didn't jump to line 480, because the condition on line 479 was never true
480 msg = "Value %s is of incorrect type %s. Expected type %s" % (
481 value,
482 _typeStr(value),
483 _typeStr(self.dtype),
484 )
485 raise TypeError(msg)
486 if self.check is not None and not self.check(value): 486 ↛ 487line 486 didn't jump to line 487, because the condition on line 486 was never true
487 msg = "Value %s is not a valid value" % str(value)
488 raise ValueError(msg)
490 def _collectImports(self, instance, imports):
491 """This function should call the _collectImports method on all config
492 objects the field may own, and union them with the supplied imports
493 set.
495 Parameters
496 ----------
497 instance : instance or subclass of `lsst.pex.config.Config`
498 A config object that has this field defined on it
499 imports : `set`
500 Set of python modules that need imported after persistence
501 """
502 pass
504 def save(self, outfile, instance):
505 """Save this field to a file (for internal use only).
507 Parameters
508 ----------
509 outfile : file-like object
510 A writeable field handle.
511 instance : `Config`
512 The `Config` instance that contains this field.
514 Notes
515 -----
516 This method is invoked by the `~lsst.pex.config.Config` object that
517 contains this field and should not be called directly.
519 The output consists of the documentation string
520 (`lsst.pex.config.Field.doc`) formatted as a Python comment. The second
521 line is formatted as an assignment: ``{fullname}={value}``.
523 This output can be executed with Python.
524 """
525 value = self.__get__(instance)
526 fullname = _joinNamePath(instance._name, self.name)
528 if self.deprecated and value == self.default: 528 ↛ 529line 528 didn't jump to line 529, because the condition on line 528 was never true
529 return
531 # write full documentation string as comment lines
532 # (i.e. first character is #)
533 doc = "# " + str(self.doc).replace("\n", "\n# ")
534 if isinstance(value, float) and not math.isfinite(value): 534 ↛ 536line 534 didn't jump to line 536, because the condition on line 534 was never true
535 # non-finite numbers need special care
536 outfile.write("{}\n{}=float('{!r}')\n\n".format(doc, fullname, value))
537 else:
538 outfile.write("{}\n{}={!r}\n\n".format(doc, fullname, value))
540 def toDict(self, instance):
541 """Convert the field value so that it can be set as the value of an
542 item in a `dict` (for internal use only).
544 Parameters
545 ----------
546 instance : `Config`
547 The `Config` that contains this field.
549 Returns
550 -------
551 value : object
552 The field's value. See *Notes*.
554 Notes
555 -----
556 This method invoked by the owning `~lsst.pex.config.Config` object and
557 should not be called directly.
559 Simple values are passed through. Complex data structures must be
560 manipulated. For example, a `~lsst.pex.config.Field` holding a
561 subconfig should, instead of the subconfig object, return a `dict`
562 where the keys are the field names in the subconfig, and the values are
563 the field values in the subconfig.
564 """
565 return self.__get__(instance)
567 def __get__(self, instance, owner=None, at=None, label="default"):
568 """Define how attribute access should occur on the Config instance
569 This is invoked by the owning config object and should not be called
570 directly
572 When the field attribute is accessed on a Config class object, it
573 returns the field object itself in order to allow inspection of
574 Config classes.
576 When the field attribute is access on a config instance, the actual
577 value described by the field (and held by the Config instance) is
578 returned.
579 """
580 if instance is None: 580 ↛ 581line 580 didn't jump to line 581, because the condition on line 580 was never true
581 return self
582 else:
583 # try statements are almost free in python if they succeed
584 try:
585 return instance._storage[self.name]
586 except AttributeError:
587 if not isinstance(instance, Config):
588 return self
589 else:
590 raise AttributeError(
591 f"Config {instance} is missing "
592 "_storage attribute, likely"
593 " incorrectly initialized"
594 )
596 def __set__(self, instance, value, at=None, label="assignment"):
597 """Set an attribute on the config instance.
599 Parameters
600 ----------
601 instance : `lsst.pex.config.Config`
602 The config instance that contains this field.
603 value : obj
604 Value to set on this field.
605 at : `list` of `lsst.pex.config.callStack.StackFrame`
606 The call stack (created by
607 `lsst.pex.config.callStack.getCallStack`).
608 label : `str`, optional
609 Event label for the history.
611 Notes
612 -----
613 This method is invoked by the owning `lsst.pex.config.Config` object
614 and should not be called directly.
616 Derived `~lsst.pex.config.Field` classes may need to override the
617 behavior. When overriding ``__set__``, `~lsst.pex.config.Field` authors
618 should follow the following rules:
620 - Do not allow modification of frozen configs.
621 - Validate the new value **before** modifying the field. Except if the
622 new value is `None`. `None` is special and no attempt should be made
623 to validate it until `lsst.pex.config.Config.validate` is called.
624 - Do not modify the `~lsst.pex.config.Config` instance to contain
625 invalid values.
626 - If the field is modified, update the history of the
627 `lsst.pex.config.field.Field` to reflect the changes.
629 In order to decrease the need to implement this method in derived
630 `~lsst.pex.config.Field` types, value validation is performed in the
631 `lsst.pex.config.Field._validateValue`. If only the validation step
632 differs in the derived `~lsst.pex.config.Field`, it is simpler to
633 implement `lsst.pex.config.Field._validateValue` than to reimplement
634 ``__set__``. More complicated behavior, however, may require
635 reimplementation.
636 """
637 if instance._frozen: 637 ↛ 638line 637 didn't jump to line 638, because the condition on line 637 was never true
638 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
640 history = instance._history.setdefault(self.name, [])
641 if value is not None: 641 ↛ 648line 641 didn't jump to line 648, because the condition on line 641 was never false
642 value = _autocast(value, self.dtype)
643 try:
644 self._validateValue(value)
645 except BaseException as e:
646 raise FieldValidationError(self, instance, str(e))
648 instance._storage[self.name] = value
649 if at is None: 649 ↛ 650line 649 didn't jump to line 650, because the condition on line 649 was never true
650 at = getCallStack()
651 history.append((value, at, label))
653 def __delete__(self, instance, at=None, label="deletion"):
654 """Delete an attribute from a `lsst.pex.config.Config` instance.
656 Parameters
657 ----------
658 instance : `lsst.pex.config.Config`
659 The config instance that contains this field.
660 at : `list` of `lsst.pex.config.callStack.StackFrame`
661 The call stack (created by
662 `lsst.pex.config.callStack.getCallStack`).
663 label : `str`, optional
664 Event label for the history.
666 Notes
667 -----
668 This is invoked by the owning `~lsst.pex.config.Config` object and
669 should not be called directly.
670 """
671 if at is None:
672 at = getCallStack()
673 self.__set__(instance, None, at=at, label=label)
675 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
676 """Compare a field (named `Field.name`) in two
677 `~lsst.pex.config.Config` instances for equality.
679 Parameters
680 ----------
681 instance1 : `lsst.pex.config.Config`
682 Left-hand side `Config` instance to compare.
683 instance2 : `lsst.pex.config.Config`
684 Right-hand side `Config` instance to compare.
685 shortcut : `bool`, optional
686 **Unused.**
687 rtol : `float`, optional
688 Relative tolerance for floating point comparisons.
689 atol : `float`, optional
690 Absolute tolerance for floating point comparisons.
691 output : callable, optional
692 A callable that takes a string, used (possibly repeatedly) to
693 report inequalities.
695 Notes
696 -----
697 This method must be overridden by more complex `Field` subclasses.
699 See also
700 --------
701 lsst.pex.config.compareScalars
702 """
703 v1 = getattr(instance1, self.name)
704 v2 = getattr(instance2, self.name)
705 name = getComparisonName(
706 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
707 )
708 return compareScalars(name, v1, v2, dtype=self.dtype, rtol=rtol, atol=atol, output=output)
711class RecordingImporter:
712 """Importer (for `sys.meta_path`) that records which modules are being
713 imported.
715 *This class does not do any importing itself.*
717 Examples
718 --------
719 Use this class as a context manager to ensure it is properly uninstalled
720 when done:
722 >>> with RecordingImporter() as importer:
723 ... # import stuff
724 ... import numpy as np
725 ... print("Imported: " + importer.getModules())
726 """
728 def __init__(self):
729 self._modules = set()
731 def __enter__(self):
732 self.origMetaPath = sys.meta_path
733 sys.meta_path = [self] + sys.meta_path
734 return self
736 def __exit__(self, *args):
737 self.uninstall()
738 return False # Don't suppress exceptions
740 def uninstall(self):
741 """Uninstall the importer."""
742 sys.meta_path = self.origMetaPath
744 def find_module(self, fullname, path=None):
745 """Called as part of the ``import`` chain of events."""
746 self._modules.add(fullname)
747 # Return None because we don't do any importing.
748 return None
750 def getModules(self):
751 """Get the set of modules that were imported.
753 Returns
754 -------
755 modules : `set` of `str`
756 Set of imported module names.
757 """
758 return self._modules
761class Config(metaclass=ConfigMeta):
762 """Base class for configuration (*config*) objects.
764 Notes
765 -----
766 A ``Config`` object will usually have several `~lsst.pex.config.Field`
767 instances as class attributes. These are used to define most of the base
768 class behavior.
770 ``Config`` implements a mapping API that provides many `dict`-like methods,
771 such as `keys`, `values`, `items`, `iteritems`, `iterkeys`, and
772 `itervalues`. ``Config`` instances also support the ``in`` operator to
773 test if a field is in the config. Unlike a `dict`, ``Config`` classes are
774 not subscriptable. Instead, access individual fields as attributes of the
775 configuration instance.
777 Examples
778 --------
779 Config classes are subclasses of ``Config`` that have
780 `~lsst.pex.config.Field` instances (or instances of
781 `~lsst.pex.config.Field` subclasses) as class attributes:
783 >>> from lsst.pex.config import Config, Field, ListField
784 >>> class DemoConfig(Config):
785 ... intField = Field(doc="An integer field", dtype=int, default=42)
786 ... listField = ListField(doc="List of favorite beverages.", dtype=str,
787 ... default=['coffee', 'green tea', 'water'])
788 ...
789 >>> config = DemoConfig()
791 Configs support many `dict`-like APIs:
793 >>> config.keys()
794 ['intField', 'listField']
795 >>> 'intField' in config
796 True
798 Individual fields can be accessed as attributes of the configuration:
800 >>> config.intField
801 42
802 >>> config.listField.append('earl grey tea')
803 >>> print(config.listField)
804 ['coffee', 'green tea', 'water', 'earl grey tea']
805 """
807 def __iter__(self):
808 """Iterate over fields."""
809 return self._fields.__iter__()
811 def keys(self):
812 """Get field names.
814 Returns
815 -------
816 names : `list`
817 List of `lsst.pex.config.Field` names.
819 See also
820 --------
821 lsst.pex.config.Config.iterkeys
822 """
823 return list(self._storage.keys())
825 def values(self):
826 """Get field values.
828 Returns
829 -------
830 values : `list`
831 List of field values.
833 See also
834 --------
835 lsst.pex.config.Config.itervalues
836 """
837 return list(self._storage.values())
839 def items(self):
840 """Get configurations as ``(field name, field value)`` pairs.
842 Returns
843 -------
844 items : `list`
845 List of tuples for each configuration. Tuple items are:
847 0. Field name.
848 1. Field value.
850 See also
851 --------
852 lsst.pex.config.Config.iteritems
853 """
854 return list(self._storage.items())
856 def iteritems(self):
857 """Iterate over (field name, field value) pairs.
859 Yields
860 ------
861 item : `tuple`
862 Tuple items are:
864 0. Field name.
865 1. Field value.
867 See also
868 --------
869 lsst.pex.config.Config.items
870 """
871 return iter(self._storage.items())
873 def itervalues(self):
874 """Iterate over field values.
876 Yields
877 ------
878 value : obj
879 A field value.
881 See also
882 --------
883 lsst.pex.config.Config.values
884 """
885 return iter(self.storage.values())
887 def iterkeys(self):
888 """Iterate over field names
890 Yields
891 ------
892 key : `str`
893 A field's key (attribute name).
895 See also
896 --------
897 lsst.pex.config.Config.values
898 """
899 return iter(self.storage.keys())
901 def __contains__(self, name):
902 """!Return True if the specified field exists in this config
904 @param[in] name field name to test for
905 """
906 return self._storage.__contains__(name)
908 def __new__(cls, *args, **kw):
909 """Allocate a new `lsst.pex.config.Config` object.
911 In order to ensure that all Config object are always in a proper state
912 when handed to users or to derived `~lsst.pex.config.Config` classes,
913 some attributes are handled at allocation time rather than at
914 initialization.
916 This ensures that even if a derived `~lsst.pex.config.Config` class
917 implements ``__init__``, its author does not need to be concerned about
918 when or even the base ``Config.__init__`` should be called.
919 """
920 name = kw.pop("__name", None)
921 at = kw.pop("__at", getCallStack())
922 # remove __label and ignore it
923 kw.pop("__label", "default")
925 instance = object.__new__(cls)
926 instance._frozen = False
927 instance._name = name
928 instance._storage = {}
929 instance._history = {}
930 instance._imports = set()
931 # load up defaults
932 for field in instance._fields.values():
933 instance._history[field.name] = []
934 field.__set__(instance, field.default, at=at + [field.source], label="default")
935 # set custom default-overides
936 instance.setDefaults()
937 # set constructor overides
938 instance.update(__at=at, **kw)
939 return instance
941 def __reduce__(self):
942 """Reduction for pickling (function with arguments to reproduce).
944 We need to condense and reconstitute the `~lsst.pex.config.Config`,
945 since it may contain lambdas (as the ``check`` elements) that cannot
946 be pickled.
947 """
948 # The stream must be in characters to match the API but pickle
949 # requires bytes
950 stream = io.StringIO()
951 self.saveToStream(stream)
952 return (unreduceConfig, (self.__class__, stream.getvalue().encode()))
954 def setDefaults(self):
955 """Subclass hook for computing defaults.
957 Notes
958 -----
959 Derived `~lsst.pex.config.Config` classes that must compute defaults
960 rather than using the `~lsst.pex.config.Field` instances's defaults
961 should do so here. To correctly use inherited defaults,
962 implementations of ``setDefaults`` must call their base class's
963 ``setDefaults``.
964 """
965 pass
967 def update(self, **kw):
968 """Update values of fields specified by the keyword arguments.
970 Parameters
971 ----------
972 kw
973 Keywords are configuration field names. Values are configuration
974 field values.
976 Notes
977 -----
978 The ``__at`` and ``__label`` keyword arguments are special internal
979 keywords. They are used to strip out any internal steps from the
980 history tracebacks of the config. Do not modify these keywords to
981 subvert a `~lsst.pex.config.Config` instance's history.
983 Examples
984 --------
985 This is a config with three fields:
987 >>> from lsst.pex.config import Config, Field
988 >>> class DemoConfig(Config):
989 ... fieldA = Field(doc='Field A', dtype=int, default=42)
990 ... fieldB = Field(doc='Field B', dtype=bool, default=True)
991 ... fieldC = Field(doc='Field C', dtype=str, default='Hello world')
992 ...
993 >>> config = DemoConfig()
995 These are the default values of each field:
997 >>> for name, value in config.iteritems():
998 ... print(f"{name}: {value}")
999 ...
1000 fieldA: 42
1001 fieldB: True
1002 fieldC: 'Hello world'
1004 Using this method to update ``fieldA`` and ``fieldC``:
1006 >>> config.update(fieldA=13, fieldC='Updated!')
1008 Now the values of each field are:
1010 >>> for name, value in config.iteritems():
1011 ... print(f"{name}: {value}")
1012 ...
1013 fieldA: 13
1014 fieldB: True
1015 fieldC: 'Updated!'
1016 """
1017 at = kw.pop("__at", getCallStack())
1018 label = kw.pop("__label", "update")
1020 for name, value in kw.items():
1021 try:
1022 field = self._fields[name]
1023 field.__set__(self, value, at=at, label=label)
1024 except KeyError:
1025 raise KeyError("No field of name %s exists in config type %s" % (name, _typeStr(self)))
1027 def load(self, filename, root="config"):
1028 """Modify this config in place by executing the Python code in a
1029 configuration file.
1031 Parameters
1032 ----------
1033 filename : `str`
1034 Name of the configuration file. A configuration file is Python
1035 module.
1036 root : `str`, optional
1037 Name of the variable in file that refers to the config being
1038 overridden.
1040 For example, the value of root is ``"config"`` and the file
1041 contains::
1043 config.myField = 5
1045 Then this config's field ``myField`` is set to ``5``.
1047 **Deprecated:** For backwards compatibility, older config files
1048 that use ``root="root"`` instead of ``root="config"`` will be
1049 loaded with a warning printed to `sys.stderr`. This feature will be
1050 removed at some point.
1052 See also
1053 --------
1054 lsst.pex.config.Config.loadFromStream
1055 lsst.pex.config.Config.loadFromString
1056 lsst.pex.config.Config.save
1057 lsst.pex.config.Config.saveToStream
1058 lsst.pex.config.Config.saveToString
1059 """
1060 with open(filename, "r") as f:
1061 code = compile(f.read(), filename=filename, mode="exec")
1062 self.loadFromString(code, root=root, filename=filename)
1064 def loadFromStream(self, stream, root="config", filename=None):
1065 """Modify this Config in place by executing the Python code in the
1066 provided stream.
1068 Parameters
1069 ----------
1070 stream : file-like object, `str`, `bytes`, or compiled string
1071 Stream containing configuration override code. If this is a
1072 code object, it should be compiled with ``mode="exec"``.
1073 root : `str`, optional
1074 Name of the variable in file that refers to the config being
1075 overridden.
1077 For example, the value of root is ``"config"`` and the file
1078 contains::
1080 config.myField = 5
1082 Then this config's field ``myField`` is set to ``5``.
1084 **Deprecated:** For backwards compatibility, older config files
1085 that use ``root="root"`` instead of ``root="config"`` will be
1086 loaded with a warning printed to `sys.stderr`. This feature will be
1087 removed at some point.
1088 filename : `str`, optional
1089 Name of the configuration file, or `None` if unknown or contained
1090 in the stream. Used for error reporting.
1092 Notes
1093 -----
1094 For backwards compatibility reasons, this method accepts strings, bytes
1095 and code objects as well as file-like objects. New code should use
1096 `loadFromString` instead for most of these types.
1098 See also
1099 --------
1100 lsst.pex.config.Config.load
1101 lsst.pex.config.Config.loadFromString
1102 lsst.pex.config.Config.save
1103 lsst.pex.config.Config.saveToStream
1104 lsst.pex.config.Config.saveToString
1105 """
1106 if hasattr(stream, "read"): 1106 ↛ 1107line 1106 didn't jump to line 1107, because the condition on line 1106 was never true
1107 if filename is None:
1108 filename = getattr(stream, "name", "?")
1109 code = compile(stream.read(), filename=filename, mode="exec")
1110 else:
1111 code = stream
1112 self.loadFromString(code, root=root, filename=filename)
1114 def loadFromString(self, code, root="config", filename=None):
1115 """Modify this Config in place by executing the Python code in the
1116 provided string.
1118 Parameters
1119 ----------
1120 code : `str`, `bytes`, or compiled string
1121 Stream containing configuration override code.
1122 root : `str`, optional
1123 Name of the variable in file that refers to the config being
1124 overridden.
1126 For example, the value of root is ``"config"`` and the file
1127 contains::
1129 config.myField = 5
1131 Then this config's field ``myField`` is set to ``5``.
1133 **Deprecated:** For backwards compatibility, older config files
1134 that use ``root="root"`` instead of ``root="config"`` will be
1135 loaded with a warning printed to `sys.stderr`. This feature will be
1136 removed at some point.
1137 filename : `str`, optional
1138 Name of the configuration file, or `None` if unknown or contained
1139 in the stream. Used for error reporting.
1141 See also
1142 --------
1143 lsst.pex.config.Config.load
1144 lsst.pex.config.Config.loadFromStream
1145 lsst.pex.config.Config.save
1146 lsst.pex.config.Config.saveToStream
1147 lsst.pex.config.Config.saveToString
1148 """
1149 if filename is None: 1149 ↛ 1153line 1149 didn't jump to line 1153, because the condition on line 1149 was never false
1150 # try to determine the file name; a compiled string
1151 # has attribute "co_filename",
1152 filename = getattr(code, "co_filename", "?")
1153 with RecordingImporter() as importer:
1154 globals = {"__file__": filename}
1155 try:
1156 local = {root: self}
1157 exec(code, globals, local)
1158 except NameError as e:
1159 if root == "config" and "root" in e.args[0]:
1160 print(
1161 f"Config override file {filename!r}"
1162 " appears to use 'root' instead of 'config'; trying with 'root'",
1163 file=sys.stderr,
1164 )
1165 local = {"root": self}
1166 exec(code, globals, local)
1167 else:
1168 raise
1170 self._imports.update(importer.getModules())
1172 def save(self, filename, root="config"):
1173 """Save a Python script to the named file, which, when loaded,
1174 reproduces this config.
1176 Parameters
1177 ----------
1178 filename : `str`
1179 Desination filename of this configuration.
1180 root : `str`, optional
1181 Name to use for the root config variable. The same value must be
1182 used when loading (see `lsst.pex.config.Config.load`).
1184 See also
1185 --------
1186 lsst.pex.config.Config.saveToStream
1187 lsst.pex.config.Config.saveToString
1188 lsst.pex.config.Config.load
1189 lsst.pex.config.Config.loadFromStream
1190 lsst.pex.config.Config.loadFromString
1191 """
1192 d = os.path.dirname(filename)
1193 with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=d) as outfile:
1194 self.saveToStream(outfile, root)
1195 # tempfile is hardcoded to create files with mode '0600'
1196 # for an explantion of these antics see:
1197 # https://stackoverflow.com/questions/10291131/how-to-use-os-umask-in-python
1198 umask = os.umask(0o077)
1199 os.umask(umask)
1200 os.chmod(outfile.name, (~umask & 0o666))
1201 # chmod before the move so we get quasi-atomic behavior if the
1202 # source and dest. are on the same filesystem.
1203 # os.rename may not work across filesystems
1204 shutil.move(outfile.name, filename)
1206 def saveToString(self, skipImports=False):
1207 """Return the Python script form of this configuration as an executable
1208 string.
1210 Parameters
1211 ----------
1212 skipImports : `bool`, optional
1213 If `True` then do not include ``import`` statements in output,
1214 this is to support human-oriented output from ``pipetask`` where
1215 additional clutter is not useful.
1217 Returns
1218 -------
1219 code : `str`
1220 A code string readable by `loadFromString`.
1222 See also
1223 --------
1224 lsst.pex.config.Config.save
1225 lsst.pex.config.Config.saveToStream
1226 lsst.pex.config.Config.load
1227 lsst.pex.config.Config.loadFromStream
1228 lsst.pex.config.Config.loadFromString
1229 """
1230 buffer = io.StringIO()
1231 self.saveToStream(buffer, skipImports=skipImports)
1232 return buffer.getvalue()
1234 def saveToStream(self, outfile, root="config", skipImports=False):
1235 """Save a configuration file to a stream, which, when loaded,
1236 reproduces this config.
1238 Parameters
1239 ----------
1240 outfile : file-like object
1241 Destination file object write the config into. Accepts strings not
1242 bytes.
1243 root
1244 Name to use for the root config variable. The same value must be
1245 used when loading (see `lsst.pex.config.Config.load`).
1246 skipImports : `bool`, optional
1247 If `True` then do not include ``import`` statements in output,
1248 this is to support human-oriented output from ``pipetask`` where
1249 additional clutter is not useful.
1251 See also
1252 --------
1253 lsst.pex.config.Config.save
1254 lsst.pex.config.Config.saveToString
1255 lsst.pex.config.Config.load
1256 lsst.pex.config.Config.loadFromStream
1257 lsst.pex.config.Config.loadFromString
1258 """
1259 tmp = self._name
1260 self._rename(root)
1261 try:
1262 if not skipImports: 1262 ↛ 1276line 1262 didn't jump to line 1276, because the condition on line 1262 was never false
1263 self._collectImports()
1264 # Remove self from the set, as it is handled explicitly below
1265 self._imports.remove(self.__module__)
1266 configType = type(self)
1267 typeString = _typeStr(configType)
1268 outfile.write(f"import {configType.__module__}\n")
1269 outfile.write(
1270 f"assert type({root})=={typeString}, 'config is of type %s.%s instead of "
1271 f"{typeString}' % (type({root}).__module__, type({root}).__name__)\n"
1272 )
1273 for imp in self._imports: 1273 ↛ 1274line 1273 didn't jump to line 1274, because the loop on line 1273 never started
1274 if imp in sys.modules and sys.modules[imp] is not None:
1275 outfile.write("import {}\n".format(imp))
1276 self._save(outfile)
1277 finally:
1278 self._rename(tmp)
1280 def freeze(self):
1281 """Make this config, and all subconfigs, read-only."""
1282 self._frozen = True
1283 for field in self._fields.values():
1284 field.freeze(self)
1286 def _save(self, outfile):
1287 """Save this config to an open stream object.
1289 Parameters
1290 ----------
1291 outfile : file-like object
1292 Destination file object write the config into. Accepts strings not
1293 bytes.
1294 """
1295 for field in self._fields.values():
1296 field.save(outfile, self)
1298 def _collectImports(self):
1299 """Adds module containing self to the list of things to import and
1300 then loops over all the fields in the config calling a corresponding
1301 collect method. The field method will call _collectImports on any
1302 configs it may own and return the set of things to import. This
1303 returned set will be merged with the set of imports for this config
1304 class.
1305 """
1306 self._imports.add(self.__module__)
1307 for name, field in self._fields.items():
1308 field._collectImports(self, self._imports)
1310 def toDict(self):
1311 """Make a dictionary of field names and their values.
1313 Returns
1314 -------
1315 dict_ : `dict`
1316 Dictionary with keys that are `~lsst.pex.config.Field` names.
1317 Values are `~lsst.pex.config.Field` values.
1319 See also
1320 --------
1321 lsst.pex.config.Field.toDict
1323 Notes
1324 -----
1325 This method uses the `~lsst.pex.config.Field.toDict` method of
1326 individual fields. Subclasses of `~lsst.pex.config.Field` may need to
1327 implement a ``toDict`` method for *this* method to work.
1328 """
1329 dict_ = {}
1330 for name, field in self._fields.items():
1331 dict_[name] = field.toDict(self)
1332 return dict_
1334 def names(self):
1335 """Get all the field names in the config, recursively.
1337 Returns
1338 -------
1339 names : `list` of `str`
1340 Field names.
1341 """
1342 #
1343 # Rather than sort out the recursion all over again use the
1344 # pre-existing saveToStream()
1345 #
1346 with io.StringIO() as strFd:
1347 self.saveToStream(strFd, "config")
1348 contents = strFd.getvalue()
1349 strFd.close()
1350 #
1351 # Pull the names out of the dumped config
1352 #
1353 keys = []
1354 for line in contents.split("\n"):
1355 if re.search(r"^((assert|import)\s+|\s*$|#)", line):
1356 continue
1358 mat = re.search(r"^(?:config\.)?([^=]+)\s*=\s*.*", line)
1359 if mat:
1360 keys.append(mat.group(1))
1362 return keys
1364 def _rename(self, name):
1365 """Rename this config object in its parent `~lsst.pex.config.Config`.
1367 Parameters
1368 ----------
1369 name : `str`
1370 New name for this config in its parent `~lsst.pex.config.Config`.
1372 Notes
1373 -----
1374 This method uses the `~lsst.pex.config.Field.rename` method of
1375 individual `lsst.pex.config.Field` instances.
1376 `lsst.pex.config.Field` subclasses may need to implement a ``rename``
1377 method for *this* method to work.
1379 See also
1380 --------
1381 lsst.pex.config.Field.rename
1382 """
1383 self._name = name
1384 for field in self._fields.values():
1385 field.rename(self)
1387 def validate(self):
1388 """Validate the Config, raising an exception if invalid.
1390 Raises
1391 ------
1392 lsst.pex.config.FieldValidationError
1393 Raised if verification fails.
1395 Notes
1396 -----
1397 The base class implementation performs type checks on all fields by
1398 calling their `~lsst.pex.config.Field.validate` methods.
1400 Complex single-field validation can be defined by deriving new Field
1401 types. For convenience, some derived `lsst.pex.config.Field`-types
1402 (`~lsst.pex.config.ConfigField` and
1403 `~lsst.pex.config.ConfigChoiceField`) are defined in `lsst.pex.config`
1404 that handle recursing into subconfigs.
1406 Inter-field relationships should only be checked in derived
1407 `~lsst.pex.config.Config` classes after calling this method, and base
1408 validation is complete.
1409 """
1410 for field in self._fields.values():
1411 field.validate(self)
1413 def formatHistory(self, name, **kwargs):
1414 """Format a configuration field's history to a human-readable format.
1416 Parameters
1417 ----------
1418 name : `str`
1419 Name of a `~lsst.pex.config.Field` in this config.
1420 kwargs
1421 Keyword arguments passed to `lsst.pex.config.history.format`.
1423 Returns
1424 -------
1425 history : `str`
1426 A string containing the formatted history.
1428 See also
1429 --------
1430 lsst.pex.config.history.format
1431 """
1432 import lsst.pex.config.history as pexHist
1434 return pexHist.format(self, name, **kwargs)
1436 history = property(lambda x: x._history) 1436 ↛ exitline 1436 didn't run the lambda on line 1436
1437 """Read-only history.
1438 """
1440 def __setattr__(self, attr, value, at=None, label="assignment"):
1441 """Set an attribute (such as a field's value).
1443 Notes
1444 -----
1445 Unlike normal Python objects, `~lsst.pex.config.Config` objects are
1446 locked such that no additional attributes nor properties may be added
1447 to them dynamically.
1449 Although this is not the standard Python behavior, it helps to protect
1450 users from accidentally mispelling a field name, or trying to set a
1451 non-existent field.
1452 """
1453 if attr in self._fields:
1454 if self._fields[attr].deprecated is not None: 1454 ↛ 1455line 1454 didn't jump to line 1455, because the condition on line 1454 was never true
1455 fullname = _joinNamePath(self._name, self._fields[attr].name)
1456 warnings.warn(
1457 f"Config field {fullname} is deprecated: {self._fields[attr].deprecated}",
1458 FutureWarning,
1459 stacklevel=2,
1460 )
1461 if at is None: 1461 ↛ 1464line 1461 didn't jump to line 1464, because the condition on line 1461 was never false
1462 at = getCallStack()
1463 # This allows Field descriptors to work.
1464 self._fields[attr].__set__(self, value, at=at, label=label)
1465 elif hasattr(getattr(self.__class__, attr, None), "__set__"): 1465 ↛ 1467line 1465 didn't jump to line 1467, because the condition on line 1465 was never true
1466 # This allows properties and other non-Field descriptors to work.
1467 return object.__setattr__(self, attr, value)
1468 elif attr in self.__dict__ or attr in ("_name", "_history", "_storage", "_frozen", "_imports"): 1468 ↛ 1473line 1468 didn't jump to line 1473, because the condition on line 1468 was never false
1469 # This allows specific private attributes to work.
1470 self.__dict__[attr] = value
1471 else:
1472 # We throw everything else.
1473 raise AttributeError("%s has no attribute %s" % (_typeStr(self), attr))
1475 def __delattr__(self, attr, at=None, label="deletion"):
1476 if attr in self._fields:
1477 if at is None:
1478 at = getCallStack()
1479 self._fields[attr].__delete__(self, at=at, label=label)
1480 else:
1481 object.__delattr__(self, attr)
1483 def __eq__(self, other):
1484 if type(other) == type(self): 1484 ↛ 1485line 1484 didn't jump to line 1485, because the condition on line 1484 was never true
1485 for name in self._fields:
1486 thisValue = getattr(self, name)
1487 otherValue = getattr(other, name)
1488 if isinstance(thisValue, float) and math.isnan(thisValue):
1489 if not math.isnan(otherValue):
1490 return False
1491 elif thisValue != otherValue:
1492 return False
1493 return True
1494 return False
1496 def __ne__(self, other):
1497 return not self.__eq__(other)
1499 def __str__(self):
1500 return str(self.toDict())
1502 def __repr__(self):
1503 return "%s(%s)" % (
1504 _typeStr(self),
1505 ", ".join("%s=%r" % (k, v) for k, v in self.toDict().items() if v is not None),
1506 )
1508 def compare(self, other, shortcut=True, rtol=1e-8, atol=1e-8, output=None):
1509 """Compare this configuration to another `~lsst.pex.config.Config` for
1510 equality.
1512 Parameters
1513 ----------
1514 other : `lsst.pex.config.Config`
1515 Other `~lsst.pex.config.Config` object to compare against this
1516 config.
1517 shortcut : `bool`, optional
1518 If `True`, return as soon as an inequality is found. Default is
1519 `True`.
1520 rtol : `float`, optional
1521 Relative tolerance for floating point comparisons.
1522 atol : `float`, optional
1523 Absolute tolerance for floating point comparisons.
1524 output : callable, optional
1525 A callable that takes a string, used (possibly repeatedly) to
1526 report inequalities.
1528 Returns
1529 -------
1530 isEqual : `bool`
1531 `True` when the two `lsst.pex.config.Config` instances are equal.
1532 `False` if there is an inequality.
1534 See also
1535 --------
1536 lsst.pex.config.compareConfigs
1538 Notes
1539 -----
1540 Unselected targets of `~lsst.pex.config.RegistryField` fields and
1541 unselected choices of `~lsst.pex.config.ConfigChoiceField` fields
1542 are not considered by this method.
1544 Floating point comparisons are performed by `numpy.allclose`.
1545 """
1546 name1 = self._name if self._name is not None else "config"
1547 name2 = other._name if other._name is not None else "config"
1548 name = getComparisonName(name1, name2)
1549 return compareConfigs(name, self, other, shortcut=shortcut, rtol=rtol, atol=atol, output=output)
1551 @classmethod
1552 def __init_subclass__(cls, **kwargs):
1553 """Run initialization for every subclass.
1555 Specifically registers the subclass with a YAML representer
1556 and YAML constructor (if pyyaml is available)
1557 """
1558 super().__init_subclass__(**kwargs)
1560 if not yaml: 1560 ↛ 1561line 1560 didn't jump to line 1561, because the condition on line 1560 was never true
1561 return
1563 yaml.add_representer(cls, _yaml_config_representer)
1565 @classmethod
1566 def _fromPython(cls, config_py):
1567 """Instantiate a `Config`-subclass from serialized Python form.
1569 Parameters
1570 ----------
1571 config_py : `str`
1572 A serialized form of the Config as created by
1573 `Config.saveToStream`.
1575 Returns
1576 -------
1577 config : `Config`
1578 Reconstructed `Config` instant.
1579 """
1580 cls = _classFromPython(config_py)
1581 return unreduceConfig(cls, config_py)
1584def _classFromPython(config_py):
1585 """Return the Config subclass required by this Config serialization.
1587 Parameters
1588 ----------
1589 config_py : `str`
1590 A serialized form of the Config as created by
1591 `Config.saveToStream`.
1593 Returns
1594 -------
1595 cls : `type`
1596 The `Config` subclass associated with this config.
1597 """
1598 # standard serialization has the form:
1599 # import config.class
1600 # assert type(config)==config.class.Config, ...
1601 # We want to parse these two lines so we can get the class itself
1603 # Do a single regex to avoid large string copies when splitting a
1604 # large config into separate lines.
1605 matches = re.search(r"^import ([\w.]+)\nassert .*==(.*?),", config_py)
1607 if not matches:
1608 first_line, second_line, _ = config_py.split("\n", 2)
1609 raise ValueError(
1610 "First two lines did not match expected form. Got:\n" f" - {first_line}\n" f" - {second_line}"
1611 )
1613 module_name = matches.group(1)
1614 module = importlib.import_module(module_name)
1616 # Second line
1617 full_name = matches.group(2)
1619 # Remove the module name from the full name
1620 if not full_name.startswith(module_name):
1621 raise ValueError(f"Module name ({module_name}) inconsistent with full name ({full_name})")
1623 # if module name is a.b.c and full name is a.b.c.d.E then
1624 # we need to remove a.b.c. and iterate over the remainder
1625 # The +1 is for the extra dot after a.b.c
1626 remainder = full_name[len(module_name) + 1 :]
1627 components = remainder.split(".")
1628 pytype = module
1629 for component in components:
1630 pytype = getattr(pytype, component)
1631 return pytype
1634def unreduceConfig(cls, stream):
1635 """Create a `~lsst.pex.config.Config` from a stream.
1637 Parameters
1638 ----------
1639 cls : `lsst.pex.config.Config`-type
1640 A `lsst.pex.config.Config` type (not an instance) that is instantiated
1641 with configurations in the ``stream``.
1642 stream : file-like object, `str`, or compiled string
1643 Stream containing configuration override code.
1645 Returns
1646 -------
1647 config : `lsst.pex.config.Config`
1648 Config instance.
1650 See also
1651 --------
1652 lsst.pex.config.Config.loadFromStream
1653 """
1654 config = cls()
1655 config.loadFromStream(stream)
1656 return config