Coverage for python/lsst/pex/config/config.py: 60%
416 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-18 02:21 -0700
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-18 02:21 -0700
1# This file is part of pex_config.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28__all__ = ("Config", "ConfigMeta", "Field", "FieldValidationError", "UnexpectedProxyUsageError")
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
65class UnexpectedProxyUsageError(TypeError):
66 """Exception raised when a proxy class is used in a context that suggests
67 it should have already been converted to the thing it proxies.
68 """
71def _joinNamePath(prefix=None, name=None, index=None):
72 """Generate nested configuration names."""
73 if not prefix and not name: 73 ↛ 74line 73 didn't jump to line 74, because the condition on line 73 was never true
74 raise ValueError("Invalid name: cannot be None")
75 elif not name: 75 ↛ 76line 75 didn't jump to line 76, because the condition on line 75 was never true
76 name = prefix
77 elif prefix and name: 77 ↛ 80line 77 didn't jump to line 80, because the condition on line 77 was never false
78 name = prefix + "." + name
80 if index is not None: 80 ↛ 81line 80 didn't jump to line 81, because the condition on line 80 was never true
81 return "%s[%r]" % (name, index)
82 else:
83 return name
86def _autocast(x, dtype):
87 """Cast a value to a type, if appropriate.
89 Parameters
90 ----------
91 x : object
92 A value.
93 dtype : tpye
94 Data type, such as `float`, `int`, or `str`.
96 Returns
97 -------
98 values : object
99 If appropriate, the returned value is ``x`` cast to the given type
100 ``dtype``. If the cast cannot be performed the original value of
101 ``x`` is returned.
102 """
103 if dtype == float and isinstance(x, int):
104 return float(x)
105 return x
108def _typeStr(x):
109 """Generate a fully-qualified type name.
111 Returns
112 -------
113 `str`
114 Fully-qualified type name.
116 Notes
117 -----
118 This function is used primarily for writing config files to be executed
119 later upon with the 'load' function.
120 """
121 if hasattr(x, "__module__") and hasattr(x, "__name__"):
122 xtype = x
123 else:
124 xtype = type(x)
125 if (sys.version_info.major <= 2 and xtype.__module__ == "__builtin__") or xtype.__module__ == "builtins": 125 ↛ 126line 125 didn't jump to line 126, because the condition on line 125 was never true
126 return xtype.__name__
127 else:
128 return "%s.%s" % (xtype.__module__, xtype.__name__)
131if yaml: 131 ↛ 164line 131 didn't jump to line 164, because the condition on line 131 was never false
133 def _yaml_config_representer(dumper, data):
134 """Represent a Config object in a form suitable for YAML.
136 Stores the serialized stream as a scalar block string.
137 """
138 stream = io.StringIO()
139 data.saveToStream(stream)
140 config_py = stream.getvalue()
142 # Strip multiple newlines from the end of the config
143 # This simplifies the YAML to use | and not |+
144 config_py = config_py.rstrip() + "\n"
146 # Trailing spaces force pyyaml to use non-block form.
147 # Remove the trailing spaces so it has no choice
148 config_py = re.sub(r"\s+$", "\n", config_py, flags=re.MULTILINE)
150 # Store the Python as a simple scalar
151 return dumper.represent_scalar("lsst.pex.config.Config", config_py, style="|")
153 def _yaml_config_constructor(loader, node):
154 """Construct a config from YAML"""
155 config_py = loader.construct_scalar(node)
156 return Config._fromPython(config_py)
158 # Register a generic constructor for Config and all subclasses
159 # Need to register for all the loaders we would like to use
160 for loader in YamlLoaders:
161 yaml.add_constructor("lsst.pex.config.Config", _yaml_config_constructor, Loader=loader)
164class ConfigMeta(type):
165 """A metaclass for `lsst.pex.config.Config`.
167 Notes
168 -----
169 ``ConfigMeta`` adds a dictionary containing all `~lsst.pex.config.Field`
170 class attributes as a class attribute called ``_fields``, and adds
171 the name of each field as an instance variable of the field itself (so you
172 don't have to pass the name of the field to the field constructor).
173 """
175 def __init__(cls, name, bases, dict_):
176 type.__init__(cls, name, bases, dict_)
177 cls._fields = {}
178 cls._source = getStackFrame()
180 def getFields(classtype):
181 fields = {}
182 bases = list(classtype.__bases__)
183 bases.reverse()
184 for b in bases:
185 fields.update(getFields(b))
187 for k, v in classtype.__dict__.items():
188 if isinstance(v, Field):
189 fields[k] = v
190 return fields
192 fields = getFields(cls)
193 for k, v in fields.items():
194 setattr(cls, k, copy.deepcopy(v))
196 def __setattr__(cls, name, value):
197 if isinstance(value, Field):
198 value.name = name
199 cls._fields[name] = value
200 type.__setattr__(cls, name, value)
203class FieldValidationError(ValueError):
204 """Raised when a ``~lsst.pex.config.Field`` is not valid in a
205 particular ``~lsst.pex.config.Config``.
207 Parameters
208 ----------
209 field : `lsst.pex.config.Field`
210 The field that was not valid.
211 config : `lsst.pex.config.Config`
212 The config containing the invalid field.
213 msg : `str`
214 Text describing why the field was not valid.
215 """
217 def __init__(self, field, config, msg):
218 self.fieldType = type(field)
219 """Type of the `~lsst.pex.config.Field` that incurred the error.
220 """
222 self.fieldName = field.name
223 """Name of the `~lsst.pex.config.Field` instance that incurred the
224 error (`str`).
226 See also
227 --------
228 lsst.pex.config.Field.name
229 """
231 self.fullname = _joinNamePath(config._name, field.name)
232 """Fully-qualified name of the `~lsst.pex.config.Field` instance
233 (`str`).
234 """
236 self.history = config.history.setdefault(field.name, [])
237 """Full history of all changes to the `~lsst.pex.config.Field`
238 instance.
239 """
241 self.fieldSource = field.source
242 """File and line number of the `~lsst.pex.config.Field` definition.
243 """
245 self.configSource = config._source
246 error = (
247 "%s '%s' failed validation: %s\n"
248 "For more information see the Field definition at:\n%s"
249 " and the Config definition at:\n%s"
250 % (
251 self.fieldType.__name__,
252 self.fullname,
253 msg,
254 self.fieldSource.format(),
255 self.configSource.format(),
256 )
257 )
258 super().__init__(error)
261class Field:
262 """A field in a `~lsst.pex.config.Config` that supports `int`, `float`,
263 `complex`, `bool`, and `str` data types.
265 Parameters
266 ----------
267 doc : `str`
268 A description of the field for users.
269 dtype : type
270 The field's data type. ``Field`` only supports basic data types:
271 `int`, `float`, `complex`, `bool`, and `str`. See
272 `Field.supportedTypes`.
273 default : object, optional
274 The field's default value.
275 check : callable, optional
276 A callable that is called with the field's value. This callable should
277 return `False` if the value is invalid. More complex inter-field
278 validation can be written as part of the
279 `lsst.pex.config.Config.validate` method.
280 optional : `bool`, optional
281 This sets whether the field is considered optional, and therefore
282 doesn't need to be set by the user. When `False`,
283 `lsst.pex.config.Config.validate` fails if the field's value is `None`.
284 deprecated : None or `str`, optional
285 A description of why this Field is deprecated, including removal date.
286 If not None, the string is appended to the docstring for this Field.
288 Raises
289 ------
290 ValueError
291 Raised when the ``dtype`` parameter is not one of the supported types
292 (see `Field.supportedTypes`).
294 See also
295 --------
296 ChoiceField
297 ConfigChoiceField
298 ConfigDictField
299 ConfigField
300 ConfigurableField
301 DictField
302 ListField
303 RangeField
304 RegistryField
306 Notes
307 -----
308 ``Field`` instances (including those of any subclass of ``Field``) are used
309 as class attributes of `~lsst.pex.config.Config` subclasses (see the
310 example, below). ``Field`` attributes work like the `property` attributes
311 of classes that implement custom setters and getters. `Field` attributes
312 belong to the class, but operate on the instance. Formally speaking,
313 `Field` attributes are `descriptors
314 <https://docs.python.org/3/howto/descriptor.html>`_.
316 When you access a `Field` attribute on a `Config` instance, you don't
317 get the `Field` instance itself. Instead, you get the value of that field,
318 which might be a simple type (`int`, `float`, `str`, `bool`) or a custom
319 container type (like a `lsst.pex.config.List`) depending on the field's
320 type. See the example, below.
322 Examples
323 --------
324 Instances of ``Field`` should be used as class attributes of
325 `lsst.pex.config.Config` subclasses:
327 >>> from lsst.pex.config import Config, Field
328 >>> class Example(Config):
329 ... myInt = Field("An integer field.", int, default=0)
330 ...
331 >>> print(config.myInt)
332 0
333 >>> config.myInt = 5
334 >>> print(config.myInt)
335 5
336 """
338 supportedTypes = set((str, bool, float, int, complex))
339 """Supported data types for field values (`set` of types).
340 """
342 def __init__(self, doc, dtype, default=None, check=None, optional=False, deprecated=None):
343 if dtype not in self.supportedTypes: 343 ↛ 344line 343 didn't jump to line 344, because the condition on line 343 was never true
344 raise ValueError("Unsupported Field dtype %s" % _typeStr(dtype))
346 source = getStackFrame()
347 self._setup(
348 doc=doc,
349 dtype=dtype,
350 default=default,
351 check=check,
352 optional=optional,
353 source=source,
354 deprecated=deprecated,
355 )
357 def _setup(self, doc, dtype, default, check, optional, source, deprecated):
358 """Set attributes, usually during initialization."""
359 self.dtype = dtype
360 """Data type for the field.
361 """
363 # append the deprecation message to the docstring.
364 if deprecated is not None:
365 doc = f"{doc} Deprecated: {deprecated}"
366 self.doc = doc
367 """A description of the field (`str`).
368 """
370 self.deprecated = deprecated
371 """If not None, a description of why this field is deprecated (`str`).
372 """
374 self.__doc__ = f"{doc} (`{dtype.__name__}`"
375 if optional or default is not None:
376 self.__doc__ += f", default ``{default!r}``"
377 self.__doc__ += ")"
379 self.default = default
380 """Default value for this field.
381 """
383 self.check = check
384 """A user-defined function that validates the value of the field.
385 """
387 self.optional = optional
388 """Flag that determines if the field is required to be set (`bool`).
390 When `False`, `lsst.pex.config.Config.validate` will fail if the
391 field's value is `None`.
392 """
394 self.source = source
395 """The stack frame where this field is defined (`list` of
396 `lsst.pex.config.callStack.StackFrame`).
397 """
399 def rename(self, instance):
400 """Rename the field in a `~lsst.pex.config.Config` (for internal use
401 only).
403 Parameters
404 ----------
405 instance : `lsst.pex.config.Config`
406 The config instance that contains this field.
408 Notes
409 -----
410 This method is invoked by the `lsst.pex.config.Config` object that
411 contains this field and should not be called directly.
413 Renaming is only relevant for `~lsst.pex.config.Field` instances that
414 hold subconfigs. `~lsst.pex.config.Fields` that hold subconfigs should
415 rename each subconfig with the full field name as generated by
416 `lsst.pex.config.config._joinNamePath`.
417 """
418 pass
420 def validate(self, instance):
421 """Validate the field (for internal use only).
423 Parameters
424 ----------
425 instance : `lsst.pex.config.Config`
426 The config instance that contains this field.
428 Raises
429 ------
430 lsst.pex.config.FieldValidationError
431 Raised if verification fails.
433 Notes
434 -----
435 This method provides basic validation:
437 - Ensures that the value is not `None` if the field is not optional.
438 - Ensures type correctness.
439 - Ensures that the user-provided ``check`` function is valid.
441 Most `~lsst.pex.config.Field` subclasses should call
442 `lsst.pex.config.field.Field.validate` if they re-implement
443 `~lsst.pex.config.field.Field.validate`.
444 """
445 value = self.__get__(instance)
446 if not self.optional and value is None:
447 raise FieldValidationError(self, instance, "Required value cannot be None")
449 def freeze(self, instance):
450 """Make this field read-only (for internal use only).
452 Parameters
453 ----------
454 instance : `lsst.pex.config.Config`
455 The config instance that contains this field.
457 Notes
458 -----
459 Freezing is only relevant for fields that hold subconfigs. Fields which
460 hold subconfigs should freeze each subconfig.
462 **Subclasses should implement this method.**
463 """
464 pass
466 def _validateValue(self, value):
467 """Validate a value.
469 Parameters
470 ----------
471 value : object
472 The value being validated.
474 Raises
475 ------
476 TypeError
477 Raised if the value's type is incompatible with the field's
478 ``dtype``.
479 ValueError
480 Raised if the value is rejected by the ``check`` method.
481 """
482 if value is None: 482 ↛ 483line 482 didn't jump to line 483, because the condition on line 482 was never true
483 return
485 if not isinstance(value, self.dtype): 485 ↛ 486line 485 didn't jump to line 486, because the condition on line 485 was never true
486 msg = "Value %s is of incorrect type %s. Expected type %s" % (
487 value,
488 _typeStr(value),
489 _typeStr(self.dtype),
490 )
491 raise TypeError(msg)
492 if self.check is not None and not self.check(value): 492 ↛ 493line 492 didn't jump to line 493, because the condition on line 492 was never true
493 msg = "Value %s is not a valid value" % str(value)
494 raise ValueError(msg)
496 def _collectImports(self, instance, imports):
497 """This function should call the _collectImports method on all config
498 objects the field may own, and union them with the supplied imports
499 set.
501 Parameters
502 ----------
503 instance : instance or subclass of `lsst.pex.config.Config`
504 A config object that has this field defined on it
505 imports : `set`
506 Set of python modules that need imported after persistence
507 """
508 pass
510 def save(self, outfile, instance):
511 """Save this field to a file (for internal use only).
513 Parameters
514 ----------
515 outfile : file-like object
516 A writeable field handle.
517 instance : `Config`
518 The `Config` instance that contains this field.
520 Notes
521 -----
522 This method is invoked by the `~lsst.pex.config.Config` object that
523 contains this field and should not be called directly.
525 The output consists of the documentation string
526 (`lsst.pex.config.Field.doc`) formatted as a Python comment. The second
527 line is formatted as an assignment: ``{fullname}={value}``.
529 This output can be executed with Python.
530 """
531 value = self.__get__(instance)
532 fullname = _joinNamePath(instance._name, self.name)
534 if self.deprecated and value == self.default: 534 ↛ 535line 534 didn't jump to line 535, because the condition on line 534 was never true
535 return
537 # write full documentation string as comment lines
538 # (i.e. first character is #)
539 doc = "# " + str(self.doc).replace("\n", "\n# ")
540 if isinstance(value, float) and not math.isfinite(value): 540 ↛ 542line 540 didn't jump to line 542, because the condition on line 540 was never true
541 # non-finite numbers need special care
542 outfile.write("{}\n{}=float('{!r}')\n\n".format(doc, fullname, value))
543 else:
544 outfile.write("{}\n{}={!r}\n\n".format(doc, fullname, value))
546 def toDict(self, instance):
547 """Convert the field value so that it can be set as the value of an
548 item in a `dict` (for internal use only).
550 Parameters
551 ----------
552 instance : `Config`
553 The `Config` that contains this field.
555 Returns
556 -------
557 value : object
558 The field's value. See *Notes*.
560 Notes
561 -----
562 This method invoked by the owning `~lsst.pex.config.Config` object and
563 should not be called directly.
565 Simple values are passed through. Complex data structures must be
566 manipulated. For example, a `~lsst.pex.config.Field` holding a
567 subconfig should, instead of the subconfig object, return a `dict`
568 where the keys are the field names in the subconfig, and the values are
569 the field values in the subconfig.
570 """
571 return self.__get__(instance)
573 def __get__(self, instance, owner=None, at=None, label="default"):
574 """Define how attribute access should occur on the Config instance
575 This is invoked by the owning config object and should not be called
576 directly
578 When the field attribute is accessed on a Config class object, it
579 returns the field object itself in order to allow inspection of
580 Config classes.
582 When the field attribute is access on a config instance, the actual
583 value described by the field (and held by the Config instance) is
584 returned.
585 """
586 if instance is None: 586 ↛ 587line 586 didn't jump to line 587, because the condition on line 586 was never true
587 return self
588 else:
589 # try statements are almost free in python if they succeed
590 try:
591 return instance._storage[self.name]
592 except AttributeError:
593 if not isinstance(instance, Config):
594 return self
595 else:
596 raise AttributeError(
597 f"Config {instance} is missing "
598 "_storage attribute, likely"
599 " incorrectly initialized"
600 )
602 def __set__(self, instance, value, at=None, label="assignment"):
603 """Set an attribute on the config instance.
605 Parameters
606 ----------
607 instance : `lsst.pex.config.Config`
608 The config instance that contains this field.
609 value : obj
610 Value to set on this field.
611 at : `list` of `lsst.pex.config.callStack.StackFrame`
612 The call stack (created by
613 `lsst.pex.config.callStack.getCallStack`).
614 label : `str`, optional
615 Event label for the history.
617 Notes
618 -----
619 This method is invoked by the owning `lsst.pex.config.Config` object
620 and should not be called directly.
622 Derived `~lsst.pex.config.Field` classes may need to override the
623 behavior. When overriding ``__set__``, `~lsst.pex.config.Field` authors
624 should follow the following rules:
626 - Do not allow modification of frozen configs.
627 - Validate the new value **before** modifying the field. Except if the
628 new value is `None`. `None` is special and no attempt should be made
629 to validate it until `lsst.pex.config.Config.validate` is called.
630 - Do not modify the `~lsst.pex.config.Config` instance to contain
631 invalid values.
632 - If the field is modified, update the history of the
633 `lsst.pex.config.field.Field` to reflect the changes.
635 In order to decrease the need to implement this method in derived
636 `~lsst.pex.config.Field` types, value validation is performed in the
637 `lsst.pex.config.Field._validateValue`. If only the validation step
638 differs in the derived `~lsst.pex.config.Field`, it is simpler to
639 implement `lsst.pex.config.Field._validateValue` than to reimplement
640 ``__set__``. More complicated behavior, however, may require
641 reimplementation.
642 """
643 if instance._frozen: 643 ↛ 644line 643 didn't jump to line 644, because the condition on line 643 was never true
644 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
646 history = instance._history.setdefault(self.name, [])
647 if value is not None: 647 ↛ 654line 647 didn't jump to line 654, because the condition on line 647 was never false
648 value = _autocast(value, self.dtype)
649 try:
650 self._validateValue(value)
651 except BaseException as e:
652 raise FieldValidationError(self, instance, str(e))
654 instance._storage[self.name] = value
655 if at is None: 655 ↛ 656line 655 didn't jump to line 656, because the condition on line 655 was never true
656 at = getCallStack()
657 history.append((value, at, label))
659 def __delete__(self, instance, at=None, label="deletion"):
660 """Delete an attribute from a `lsst.pex.config.Config` instance.
662 Parameters
663 ----------
664 instance : `lsst.pex.config.Config`
665 The config instance that contains this field.
666 at : `list` of `lsst.pex.config.callStack.StackFrame`
667 The call stack (created by
668 `lsst.pex.config.callStack.getCallStack`).
669 label : `str`, optional
670 Event label for the history.
672 Notes
673 -----
674 This is invoked by the owning `~lsst.pex.config.Config` object and
675 should not be called directly.
676 """
677 if at is None:
678 at = getCallStack()
679 self.__set__(instance, None, at=at, label=label)
681 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
682 """Compare a field (named `Field.name`) in two
683 `~lsst.pex.config.Config` instances for equality.
685 Parameters
686 ----------
687 instance1 : `lsst.pex.config.Config`
688 Left-hand side `Config` instance to compare.
689 instance2 : `lsst.pex.config.Config`
690 Right-hand side `Config` instance to compare.
691 shortcut : `bool`, optional
692 **Unused.**
693 rtol : `float`, optional
694 Relative tolerance for floating point comparisons.
695 atol : `float`, optional
696 Absolute tolerance for floating point comparisons.
697 output : callable, optional
698 A callable that takes a string, used (possibly repeatedly) to
699 report inequalities.
701 Notes
702 -----
703 This method must be overridden by more complex `Field` subclasses.
705 See also
706 --------
707 lsst.pex.config.compareScalars
708 """
709 v1 = getattr(instance1, self.name)
710 v2 = getattr(instance2, self.name)
711 name = getComparisonName(
712 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
713 )
714 return compareScalars(name, v1, v2, dtype=self.dtype, rtol=rtol, atol=atol, output=output)
717class RecordingImporter:
718 """Importer (for `sys.meta_path`) that records which modules are being
719 imported.
721 *This class does not do any importing itself.*
723 Examples
724 --------
725 Use this class as a context manager to ensure it is properly uninstalled
726 when done:
728 >>> with RecordingImporter() as importer:
729 ... # import stuff
730 ... import numpy as np
731 ... print("Imported: " + importer.getModules())
732 """
734 def __init__(self):
735 self._modules = set()
737 def __enter__(self):
738 self.origMetaPath = sys.meta_path
739 sys.meta_path = [self] + sys.meta_path
740 return self
742 def __exit__(self, *args):
743 self.uninstall()
744 return False # Don't suppress exceptions
746 def uninstall(self):
747 """Uninstall the importer."""
748 sys.meta_path = self.origMetaPath
750 def find_module(self, fullname, path=None):
751 """Called as part of the ``import`` chain of events."""
752 self._modules.add(fullname)
753 # Return None because we don't do any importing.
754 return None
756 def getModules(self):
757 """Get the set of modules that were imported.
759 Returns
760 -------
761 modules : `set` of `str`
762 Set of imported module names.
763 """
764 return self._modules
767class Config(metaclass=ConfigMeta):
768 """Base class for configuration (*config*) objects.
770 Notes
771 -----
772 A ``Config`` object will usually have several `~lsst.pex.config.Field`
773 instances as class attributes. These are used to define most of the base
774 class behavior.
776 ``Config`` implements a mapping API that provides many `dict`-like methods,
777 such as `keys`, `values`, `items`, `iteritems`, `iterkeys`, and
778 `itervalues`. ``Config`` instances also support the ``in`` operator to
779 test if a field is in the config. Unlike a `dict`, ``Config`` classes are
780 not subscriptable. Instead, access individual fields as attributes of the
781 configuration instance.
783 Examples
784 --------
785 Config classes are subclasses of ``Config`` that have
786 `~lsst.pex.config.Field` instances (or instances of
787 `~lsst.pex.config.Field` subclasses) as class attributes:
789 >>> from lsst.pex.config import Config, Field, ListField
790 >>> class DemoConfig(Config):
791 ... intField = Field(doc="An integer field", dtype=int, default=42)
792 ... listField = ListField(doc="List of favorite beverages.", dtype=str,
793 ... default=['coffee', 'green tea', 'water'])
794 ...
795 >>> config = DemoConfig()
797 Configs support many `dict`-like APIs:
799 >>> config.keys()
800 ['intField', 'listField']
801 >>> 'intField' in config
802 True
804 Individual fields can be accessed as attributes of the configuration:
806 >>> config.intField
807 42
808 >>> config.listField.append('earl grey tea')
809 >>> print(config.listField)
810 ['coffee', 'green tea', 'water', 'earl grey tea']
811 """
813 def __iter__(self):
814 """Iterate over fields."""
815 return self._fields.__iter__()
817 def keys(self):
818 """Get field names.
820 Returns
821 -------
822 names : `dict_keys`
823 List of `lsst.pex.config.Field` names.
825 See also
826 --------
827 lsst.pex.config.Config.iterkeys
828 """
829 return self._storage.keys()
831 def values(self):
832 """Get field values.
834 Returns
835 -------
836 values : `dict_values`
837 Iterator of field values.
838 """
839 return self._storage.values()
841 def items(self):
842 """Get configurations as ``(field name, field value)`` pairs.
844 Returns
845 -------
846 items : `dict_items`
847 Iterator of tuples for each configuration. Tuple items are:
849 0. Field name.
850 1. Field value.
851 """
852 return self._storage.items()
854 def __contains__(self, name):
855 """!Return True if the specified field exists in this config
857 @param[in] name field name to test for
858 """
859 return self._storage.__contains__(name)
861 def __new__(cls, *args, **kw):
862 """Allocate a new `lsst.pex.config.Config` object.
864 In order to ensure that all Config object are always in a proper state
865 when handed to users or to derived `~lsst.pex.config.Config` classes,
866 some attributes are handled at allocation time rather than at
867 initialization.
869 This ensures that even if a derived `~lsst.pex.config.Config` class
870 implements ``__init__``, its author does not need to be concerned about
871 when or even the base ``Config.__init__`` should be called.
872 """
873 name = kw.pop("__name", None)
874 at = kw.pop("__at", getCallStack())
875 # remove __label and ignore it
876 kw.pop("__label", "default")
878 instance = object.__new__(cls)
879 instance._frozen = False
880 instance._name = name
881 instance._storage = {}
882 instance._history = {}
883 instance._imports = set()
884 # load up defaults
885 for field in instance._fields.values():
886 instance._history[field.name] = []
887 field.__set__(instance, field.default, at=at + [field.source], label="default")
888 # set custom default-overides
889 instance.setDefaults()
890 # set constructor overides
891 instance.update(__at=at, **kw)
892 return instance
894 def __reduce__(self):
895 """Reduction for pickling (function with arguments to reproduce).
897 We need to condense and reconstitute the `~lsst.pex.config.Config`,
898 since it may contain lambdas (as the ``check`` elements) that cannot
899 be pickled.
900 """
901 # The stream must be in characters to match the API but pickle
902 # requires bytes
903 stream = io.StringIO()
904 self.saveToStream(stream)
905 return (unreduceConfig, (self.__class__, stream.getvalue().encode()))
907 def setDefaults(self):
908 """Subclass hook for computing defaults.
910 Notes
911 -----
912 Derived `~lsst.pex.config.Config` classes that must compute defaults
913 rather than using the `~lsst.pex.config.Field` instances's defaults
914 should do so here. To correctly use inherited defaults,
915 implementations of ``setDefaults`` must call their base class's
916 ``setDefaults``.
917 """
918 pass
920 def update(self, **kw):
921 """Update values of fields specified by the keyword arguments.
923 Parameters
924 ----------
925 kw
926 Keywords are configuration field names. Values are configuration
927 field values.
929 Notes
930 -----
931 The ``__at`` and ``__label`` keyword arguments are special internal
932 keywords. They are used to strip out any internal steps from the
933 history tracebacks of the config. Do not modify these keywords to
934 subvert a `~lsst.pex.config.Config` instance's history.
936 Examples
937 --------
938 This is a config with three fields:
940 >>> from lsst.pex.config import Config, Field
941 >>> class DemoConfig(Config):
942 ... fieldA = Field(doc='Field A', dtype=int, default=42)
943 ... fieldB = Field(doc='Field B', dtype=bool, default=True)
944 ... fieldC = Field(doc='Field C', dtype=str, default='Hello world')
945 ...
946 >>> config = DemoConfig()
948 These are the default values of each field:
950 >>> for name, value in config.iteritems():
951 ... print(f"{name}: {value}")
952 ...
953 fieldA: 42
954 fieldB: True
955 fieldC: 'Hello world'
957 Using this method to update ``fieldA`` and ``fieldC``:
959 >>> config.update(fieldA=13, fieldC='Updated!')
961 Now the values of each field are:
963 >>> for name, value in config.iteritems():
964 ... print(f"{name}: {value}")
965 ...
966 fieldA: 13
967 fieldB: True
968 fieldC: 'Updated!'
969 """
970 at = kw.pop("__at", getCallStack())
971 label = kw.pop("__label", "update")
973 for name, value in kw.items():
974 try:
975 field = self._fields[name]
976 field.__set__(self, value, at=at, label=label)
977 except KeyError:
978 raise KeyError("No field of name %s exists in config type %s" % (name, _typeStr(self)))
980 def load(self, filename, root="config"):
981 """Modify this config in place by executing the Python code in a
982 configuration file.
984 Parameters
985 ----------
986 filename : `str`
987 Name of the configuration file. A configuration file is Python
988 module.
989 root : `str`, optional
990 Name of the variable in file that refers to the config being
991 overridden.
993 For example, the value of root is ``"config"`` and the file
994 contains::
996 config.myField = 5
998 Then this config's field ``myField`` is set to ``5``.
1000 **Deprecated:** For backwards compatibility, older config files
1001 that use ``root="root"`` instead of ``root="config"`` will be
1002 loaded with a warning printed to `sys.stderr`. This feature will be
1003 removed at some point.
1005 See also
1006 --------
1007 lsst.pex.config.Config.loadFromStream
1008 lsst.pex.config.Config.loadFromString
1009 lsst.pex.config.Config.save
1010 lsst.pex.config.Config.saveToStream
1011 lsst.pex.config.Config.saveToString
1012 """
1013 with open(filename, "r") as f:
1014 code = compile(f.read(), filename=filename, mode="exec")
1015 self.loadFromString(code, root=root, filename=filename)
1017 def loadFromStream(self, stream, root="config", filename=None):
1018 """Modify this Config in place by executing the Python code in the
1019 provided stream.
1021 Parameters
1022 ----------
1023 stream : file-like object, `str`, `bytes`, or compiled string
1024 Stream containing configuration override code. If this is a
1025 code object, it should be compiled with ``mode="exec"``.
1026 root : `str`, optional
1027 Name of the variable in file that refers to the config being
1028 overridden.
1030 For example, the value of root is ``"config"`` and the file
1031 contains::
1033 config.myField = 5
1035 Then this config's field ``myField`` is set to ``5``.
1037 **Deprecated:** For backwards compatibility, older config files
1038 that use ``root="root"`` instead of ``root="config"`` will be
1039 loaded with a warning printed to `sys.stderr`. This feature will be
1040 removed at some point.
1041 filename : `str`, optional
1042 Name of the configuration file, or `None` if unknown or contained
1043 in the stream. Used for error reporting.
1045 Notes
1046 -----
1047 For backwards compatibility reasons, this method accepts strings, bytes
1048 and code objects as well as file-like objects. New code should use
1049 `loadFromString` instead for most of these types.
1051 See also
1052 --------
1053 lsst.pex.config.Config.load
1054 lsst.pex.config.Config.loadFromString
1055 lsst.pex.config.Config.save
1056 lsst.pex.config.Config.saveToStream
1057 lsst.pex.config.Config.saveToString
1058 """
1059 if hasattr(stream, "read"): 1059 ↛ 1060line 1059 didn't jump to line 1060, because the condition on line 1059 was never true
1060 if filename is None:
1061 filename = getattr(stream, "name", "?")
1062 code = compile(stream.read(), filename=filename, mode="exec")
1063 else:
1064 code = stream
1065 self.loadFromString(code, root=root, filename=filename)
1067 def loadFromString(self, code, root="config", filename=None):
1068 """Modify this Config in place by executing the Python code in the
1069 provided string.
1071 Parameters
1072 ----------
1073 code : `str`, `bytes`, or compiled string
1074 Stream containing configuration override code.
1075 root : `str`, optional
1076 Name of the variable in file that refers to the config being
1077 overridden.
1079 For example, the value of root is ``"config"`` and the file
1080 contains::
1082 config.myField = 5
1084 Then this config's field ``myField`` is set to ``5``.
1086 **Deprecated:** For backwards compatibility, older config files
1087 that use ``root="root"`` instead of ``root="config"`` will be
1088 loaded with a warning printed to `sys.stderr`. This feature will be
1089 removed at some point.
1090 filename : `str`, optional
1091 Name of the configuration file, or `None` if unknown or contained
1092 in the stream. Used for error reporting.
1094 See also
1095 --------
1096 lsst.pex.config.Config.load
1097 lsst.pex.config.Config.loadFromStream
1098 lsst.pex.config.Config.save
1099 lsst.pex.config.Config.saveToStream
1100 lsst.pex.config.Config.saveToString
1101 """
1102 if filename is None: 1102 ↛ 1106line 1102 didn't jump to line 1106, because the condition on line 1102 was never false
1103 # try to determine the file name; a compiled string
1104 # has attribute "co_filename",
1105 filename = getattr(code, "co_filename", "?")
1106 with RecordingImporter() as importer:
1107 globals = {"__file__": filename}
1108 try:
1109 local = {root: self}
1110 exec(code, globals, local)
1111 except NameError as e:
1112 if root == "config" and "root" in e.args[0]:
1113 print(
1114 f"Config override file {filename!r}"
1115 " appears to use 'root' instead of 'config'; trying with 'root'",
1116 file=sys.stderr,
1117 )
1118 local = {"root": self}
1119 exec(code, globals, local)
1120 else:
1121 raise
1123 self._imports.update(importer.getModules())
1125 def save(self, filename, root="config"):
1126 """Save a Python script to the named file, which, when loaded,
1127 reproduces this config.
1129 Parameters
1130 ----------
1131 filename : `str`
1132 Desination filename of this configuration.
1133 root : `str`, optional
1134 Name to use for the root config variable. The same value must be
1135 used when loading (see `lsst.pex.config.Config.load`).
1137 See also
1138 --------
1139 lsst.pex.config.Config.saveToStream
1140 lsst.pex.config.Config.saveToString
1141 lsst.pex.config.Config.load
1142 lsst.pex.config.Config.loadFromStream
1143 lsst.pex.config.Config.loadFromString
1144 """
1145 d = os.path.dirname(filename)
1146 with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=d) as outfile:
1147 self.saveToStream(outfile, root)
1148 # tempfile is hardcoded to create files with mode '0600'
1149 # for an explantion of these antics see:
1150 # https://stackoverflow.com/questions/10291131/how-to-use-os-umask-in-python
1151 umask = os.umask(0o077)
1152 os.umask(umask)
1153 os.chmod(outfile.name, (~umask & 0o666))
1154 # chmod before the move so we get quasi-atomic behavior if the
1155 # source and dest. are on the same filesystem.
1156 # os.rename may not work across filesystems
1157 shutil.move(outfile.name, filename)
1159 def saveToString(self, skipImports=False):
1160 """Return the Python script form of this configuration as an executable
1161 string.
1163 Parameters
1164 ----------
1165 skipImports : `bool`, optional
1166 If `True` then do not include ``import`` statements in output,
1167 this is to support human-oriented output from ``pipetask`` where
1168 additional clutter is not useful.
1170 Returns
1171 -------
1172 code : `str`
1173 A code string readable by `loadFromString`.
1175 See also
1176 --------
1177 lsst.pex.config.Config.save
1178 lsst.pex.config.Config.saveToStream
1179 lsst.pex.config.Config.load
1180 lsst.pex.config.Config.loadFromStream
1181 lsst.pex.config.Config.loadFromString
1182 """
1183 buffer = io.StringIO()
1184 self.saveToStream(buffer, skipImports=skipImports)
1185 return buffer.getvalue()
1187 def saveToStream(self, outfile, root="config", skipImports=False):
1188 """Save a configuration file to a stream, which, when loaded,
1189 reproduces this config.
1191 Parameters
1192 ----------
1193 outfile : file-like object
1194 Destination file object write the config into. Accepts strings not
1195 bytes.
1196 root
1197 Name to use for the root config variable. The same value must be
1198 used when loading (see `lsst.pex.config.Config.load`).
1199 skipImports : `bool`, optional
1200 If `True` then do not include ``import`` statements in output,
1201 this is to support human-oriented output from ``pipetask`` where
1202 additional clutter is not useful.
1204 See also
1205 --------
1206 lsst.pex.config.Config.save
1207 lsst.pex.config.Config.saveToString
1208 lsst.pex.config.Config.load
1209 lsst.pex.config.Config.loadFromStream
1210 lsst.pex.config.Config.loadFromString
1211 """
1212 tmp = self._name
1213 self._rename(root)
1214 try:
1215 if not skipImports: 1215 ↛ 1229line 1215 didn't jump to line 1229, because the condition on line 1215 was never false
1216 self._collectImports()
1217 # Remove self from the set, as it is handled explicitly below
1218 self._imports.remove(self.__module__)
1219 configType = type(self)
1220 typeString = _typeStr(configType)
1221 outfile.write(f"import {configType.__module__}\n")
1222 outfile.write(
1223 f"assert type({root})=={typeString}, 'config is of type %s.%s instead of "
1224 f"{typeString}' % (type({root}).__module__, type({root}).__name__)\n"
1225 )
1226 for imp in self._imports: 1226 ↛ 1227line 1226 didn't jump to line 1227, because the loop on line 1226 never started
1227 if imp in sys.modules and sys.modules[imp] is not None:
1228 outfile.write("import {}\n".format(imp))
1229 self._save(outfile)
1230 finally:
1231 self._rename(tmp)
1233 def freeze(self):
1234 """Make this config, and all subconfigs, read-only."""
1235 self._frozen = True
1236 for field in self._fields.values():
1237 field.freeze(self)
1239 def _save(self, outfile):
1240 """Save this config to an open stream object.
1242 Parameters
1243 ----------
1244 outfile : file-like object
1245 Destination file object write the config into. Accepts strings not
1246 bytes.
1247 """
1248 for field in self._fields.values():
1249 field.save(outfile, self)
1251 def _collectImports(self):
1252 """Adds module containing self to the list of things to import and
1253 then loops over all the fields in the config calling a corresponding
1254 collect method. The field method will call _collectImports on any
1255 configs it may own and return the set of things to import. This
1256 returned set will be merged with the set of imports for this config
1257 class.
1258 """
1259 self._imports.add(self.__module__)
1260 for name, field in self._fields.items():
1261 field._collectImports(self, self._imports)
1263 def toDict(self):
1264 """Make a dictionary of field names and their values.
1266 Returns
1267 -------
1268 dict_ : `dict`
1269 Dictionary with keys that are `~lsst.pex.config.Field` names.
1270 Values are `~lsst.pex.config.Field` values.
1272 See also
1273 --------
1274 lsst.pex.config.Field.toDict
1276 Notes
1277 -----
1278 This method uses the `~lsst.pex.config.Field.toDict` method of
1279 individual fields. Subclasses of `~lsst.pex.config.Field` may need to
1280 implement a ``toDict`` method for *this* method to work.
1281 """
1282 dict_ = {}
1283 for name, field in self._fields.items():
1284 dict_[name] = field.toDict(self)
1285 return dict_
1287 def names(self):
1288 """Get all the field names in the config, recursively.
1290 Returns
1291 -------
1292 names : `list` of `str`
1293 Field names.
1294 """
1295 #
1296 # Rather than sort out the recursion all over again use the
1297 # pre-existing saveToStream()
1298 #
1299 with io.StringIO() as strFd:
1300 self.saveToStream(strFd, "config")
1301 contents = strFd.getvalue()
1302 strFd.close()
1303 #
1304 # Pull the names out of the dumped config
1305 #
1306 keys = []
1307 for line in contents.split("\n"):
1308 if re.search(r"^((assert|import)\s+|\s*$|#)", line):
1309 continue
1311 mat = re.search(r"^(?:config\.)?([^=]+)\s*=\s*.*", line)
1312 if mat:
1313 keys.append(mat.group(1))
1315 return keys
1317 def _rename(self, name):
1318 """Rename this config object in its parent `~lsst.pex.config.Config`.
1320 Parameters
1321 ----------
1322 name : `str`
1323 New name for this config in its parent `~lsst.pex.config.Config`.
1325 Notes
1326 -----
1327 This method uses the `~lsst.pex.config.Field.rename` method of
1328 individual `lsst.pex.config.Field` instances.
1329 `lsst.pex.config.Field` subclasses may need to implement a ``rename``
1330 method for *this* method to work.
1332 See also
1333 --------
1334 lsst.pex.config.Field.rename
1335 """
1336 self._name = name
1337 for field in self._fields.values():
1338 field.rename(self)
1340 def validate(self):
1341 """Validate the Config, raising an exception if invalid.
1343 Raises
1344 ------
1345 lsst.pex.config.FieldValidationError
1346 Raised if verification fails.
1348 Notes
1349 -----
1350 The base class implementation performs type checks on all fields by
1351 calling their `~lsst.pex.config.Field.validate` methods.
1353 Complex single-field validation can be defined by deriving new Field
1354 types. For convenience, some derived `lsst.pex.config.Field`-types
1355 (`~lsst.pex.config.ConfigField` and
1356 `~lsst.pex.config.ConfigChoiceField`) are defined in `lsst.pex.config`
1357 that handle recursing into subconfigs.
1359 Inter-field relationships should only be checked in derived
1360 `~lsst.pex.config.Config` classes after calling this method, and base
1361 validation is complete.
1362 """
1363 for field in self._fields.values():
1364 field.validate(self)
1366 def formatHistory(self, name, **kwargs):
1367 """Format a configuration field's history to a human-readable format.
1369 Parameters
1370 ----------
1371 name : `str`
1372 Name of a `~lsst.pex.config.Field` in this config.
1373 kwargs
1374 Keyword arguments passed to `lsst.pex.config.history.format`.
1376 Returns
1377 -------
1378 history : `str`
1379 A string containing the formatted history.
1381 See also
1382 --------
1383 lsst.pex.config.history.format
1384 """
1385 import lsst.pex.config.history as pexHist
1387 return pexHist.format(self, name, **kwargs)
1389 history = property(lambda x: x._history) 1389 ↛ exitline 1389 didn't run the lambda on line 1389
1390 """Read-only history.
1391 """
1393 def __setattr__(self, attr, value, at=None, label="assignment"):
1394 """Set an attribute (such as a field's value).
1396 Notes
1397 -----
1398 Unlike normal Python objects, `~lsst.pex.config.Config` objects are
1399 locked such that no additional attributes nor properties may be added
1400 to them dynamically.
1402 Although this is not the standard Python behavior, it helps to protect
1403 users from accidentally mispelling a field name, or trying to set a
1404 non-existent field.
1405 """
1406 if attr in self._fields:
1407 if self._fields[attr].deprecated is not None: 1407 ↛ 1408line 1407 didn't jump to line 1408, because the condition on line 1407 was never true
1408 fullname = _joinNamePath(self._name, self._fields[attr].name)
1409 warnings.warn(
1410 f"Config field {fullname} is deprecated: {self._fields[attr].deprecated}",
1411 FutureWarning,
1412 stacklevel=2,
1413 )
1414 if at is None: 1414 ↛ 1417line 1414 didn't jump to line 1417, because the condition on line 1414 was never false
1415 at = getCallStack()
1416 # This allows Field descriptors to work.
1417 self._fields[attr].__set__(self, value, at=at, label=label)
1418 elif hasattr(getattr(self.__class__, attr, None), "__set__"): 1418 ↛ 1420line 1418 didn't jump to line 1420, because the condition on line 1418 was never true
1419 # This allows properties and other non-Field descriptors to work.
1420 return object.__setattr__(self, attr, value)
1421 elif attr in self.__dict__ or attr in ("_name", "_history", "_storage", "_frozen", "_imports"): 1421 ↛ 1426line 1421 didn't jump to line 1426, because the condition on line 1421 was never false
1422 # This allows specific private attributes to work.
1423 self.__dict__[attr] = value
1424 else:
1425 # We throw everything else.
1426 raise AttributeError("%s has no attribute %s" % (_typeStr(self), attr))
1428 def __delattr__(self, attr, at=None, label="deletion"):
1429 if attr in self._fields:
1430 if at is None:
1431 at = getCallStack()
1432 self._fields[attr].__delete__(self, at=at, label=label)
1433 else:
1434 object.__delattr__(self, attr)
1436 def __eq__(self, other):
1437 if type(other) == type(self): 1437 ↛ 1438line 1437 didn't jump to line 1438, because the condition on line 1437 was never true
1438 for name in self._fields:
1439 thisValue = getattr(self, name)
1440 otherValue = getattr(other, name)
1441 if isinstance(thisValue, float) and math.isnan(thisValue):
1442 if not math.isnan(otherValue):
1443 return False
1444 elif thisValue != otherValue:
1445 return False
1446 return True
1447 return False
1449 def __ne__(self, other):
1450 return not self.__eq__(other)
1452 def __str__(self):
1453 return str(self.toDict())
1455 def __repr__(self):
1456 return "%s(%s)" % (
1457 _typeStr(self),
1458 ", ".join("%s=%r" % (k, v) for k, v in self.toDict().items() if v is not None),
1459 )
1461 def compare(self, other, shortcut=True, rtol=1e-8, atol=1e-8, output=None):
1462 """Compare this configuration to another `~lsst.pex.config.Config` for
1463 equality.
1465 Parameters
1466 ----------
1467 other : `lsst.pex.config.Config`
1468 Other `~lsst.pex.config.Config` object to compare against this
1469 config.
1470 shortcut : `bool`, optional
1471 If `True`, return as soon as an inequality is found. Default is
1472 `True`.
1473 rtol : `float`, optional
1474 Relative tolerance for floating point comparisons.
1475 atol : `float`, optional
1476 Absolute tolerance for floating point comparisons.
1477 output : callable, optional
1478 A callable that takes a string, used (possibly repeatedly) to
1479 report inequalities.
1481 Returns
1482 -------
1483 isEqual : `bool`
1484 `True` when the two `lsst.pex.config.Config` instances are equal.
1485 `False` if there is an inequality.
1487 See also
1488 --------
1489 lsst.pex.config.compareConfigs
1491 Notes
1492 -----
1493 Unselected targets of `~lsst.pex.config.RegistryField` fields and
1494 unselected choices of `~lsst.pex.config.ConfigChoiceField` fields
1495 are not considered by this method.
1497 Floating point comparisons are performed by `numpy.allclose`.
1498 """
1499 name1 = self._name if self._name is not None else "config"
1500 name2 = other._name if other._name is not None else "config"
1501 name = getComparisonName(name1, name2)
1502 return compareConfigs(name, self, other, shortcut=shortcut, rtol=rtol, atol=atol, output=output)
1504 @classmethod
1505 def __init_subclass__(cls, **kwargs):
1506 """Run initialization for every subclass.
1508 Specifically registers the subclass with a YAML representer
1509 and YAML constructor (if pyyaml is available)
1510 """
1511 super().__init_subclass__(**kwargs)
1513 if not yaml: 1513 ↛ 1514line 1513 didn't jump to line 1514, because the condition on line 1513 was never true
1514 return
1516 yaml.add_representer(cls, _yaml_config_representer)
1518 @classmethod
1519 def _fromPython(cls, config_py):
1520 """Instantiate a `Config`-subclass from serialized Python form.
1522 Parameters
1523 ----------
1524 config_py : `str`
1525 A serialized form of the Config as created by
1526 `Config.saveToStream`.
1528 Returns
1529 -------
1530 config : `Config`
1531 Reconstructed `Config` instant.
1532 """
1533 cls = _classFromPython(config_py)
1534 return unreduceConfig(cls, config_py)
1537def _classFromPython(config_py):
1538 """Return the Config subclass required by this Config serialization.
1540 Parameters
1541 ----------
1542 config_py : `str`
1543 A serialized form of the Config as created by
1544 `Config.saveToStream`.
1546 Returns
1547 -------
1548 cls : `type`
1549 The `Config` subclass associated with this config.
1550 """
1551 # standard serialization has the form:
1552 # import config.class
1553 # assert type(config)==config.class.Config, ...
1554 # We want to parse these two lines so we can get the class itself
1556 # Do a single regex to avoid large string copies when splitting a
1557 # large config into separate lines.
1558 matches = re.search(r"^import ([\w.]+)\nassert .*==(.*?),", config_py)
1560 if not matches:
1561 first_line, second_line, _ = config_py.split("\n", 2)
1562 raise ValueError(
1563 "First two lines did not match expected form. Got:\n" f" - {first_line}\n" f" - {second_line}"
1564 )
1566 module_name = matches.group(1)
1567 module = importlib.import_module(module_name)
1569 # Second line
1570 full_name = matches.group(2)
1572 # Remove the module name from the full name
1573 if not full_name.startswith(module_name):
1574 raise ValueError(f"Module name ({module_name}) inconsistent with full name ({full_name})")
1576 # if module name is a.b.c and full name is a.b.c.d.E then
1577 # we need to remove a.b.c. and iterate over the remainder
1578 # The +1 is for the extra dot after a.b.c
1579 remainder = full_name[len(module_name) + 1 :]
1580 components = remainder.split(".")
1581 pytype = module
1582 for component in components:
1583 pytype = getattr(pytype, component)
1584 return pytype
1587def unreduceConfig(cls, stream):
1588 """Create a `~lsst.pex.config.Config` from a stream.
1590 Parameters
1591 ----------
1592 cls : `lsst.pex.config.Config`-type
1593 A `lsst.pex.config.Config` type (not an instance) that is instantiated
1594 with configurations in the ``stream``.
1595 stream : file-like object, `str`, or compiled string
1596 Stream containing configuration override code.
1598 Returns
1599 -------
1600 config : `lsst.pex.config.Config`
1601 Config instance.
1603 See also
1604 --------
1605 lsst.pex.config.Config.loadFromStream
1606 """
1607 config = cls()
1608 config.loadFromStream(stream)
1609 return config