28 __all__ = [
"DictField"]
 
   30 import collections.abc
 
   32 from .config 
import Field, FieldValidationError, _typeStr, _autocast, _joinNamePath
 
   33 from .comparison 
import getComparisonName, compareScalars
 
   34 from .callStack 
import getCallStack, getStackFrame
 
   37 class Dict(collections.abc.MutableMapping):
 
   38     """An internal mapping container. 
   40     This class emulates a `dict`, but adds validation and provenance. 
   43     def __init__(self, config, field, value, at, label, setHistory=True):
 
   53                     self.
__setitem__(k, value[k], at=at, label=label, setHistory=
False)
 
   55                 msg = 
"Value %s is of incorrect type %s. Mapping type expected." % \
 
   56                     (value, _typeStr(value))
 
   61     history = property(
lambda x: x._history)
 
   62     """History (read-only). 
   69         return len(self.
_dict)
 
   75         return k 
in self.
_dict 
   77     def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
 
   79             msg = 
"Cannot modify a frozen Config. "\
 
   80                 "Attempting to set item at key %r to value %s" % (k, x)
 
   84         k = _autocast(k, self.
_field.keytype)
 
   86             msg = 
"Key %r is of type %s, expected type %s" % \
 
   87                 (k, _typeStr(k), _typeStr(self.
_field.keytype))
 
   91         x = _autocast(x, self.
_field.itemtype)
 
   92         if self.
_field.itemtype 
is None:
 
   93             if type(x) 
not in self.
_field.supportedTypes 
and x 
is not None:
 
   94                 msg = 
"Value %s at key %r is of invalid type %s" % (x, k, _typeStr(x))
 
   97             if type(x) != self.
_field.itemtype 
and x 
is not None:
 
   98                 msg = 
"Value %s at key %r is of incorrect type %s. Expected type %s" % \
 
   99                     (x, k, _typeStr(x), _typeStr(self.
_field.itemtype))
 
  103         if self.
_field.itemCheck 
is not None and not self.
_field.itemCheck(x):
 
  104             msg = 
"Item at key %r is not a valid value: %s" % (k, x)
 
  114     def __delitem__(self, k, at=None, label="delitem", setHistory=True):
 
  117                                        "Cannot modify a frozen Config")
 
  126         return repr(self.
_dict)
 
  129         return str(self.
_dict)
 
  132         if hasattr(getattr(self.__class__, attr, 
None), 
'__set__'):
 
  134             object.__setattr__(self, attr, value)
 
  135         elif attr 
in self.__dict__ 
or attr 
in [
"_field", 
"_config", 
"_history", 
"_dict", 
"__doc__"]:
 
  137             object.__setattr__(self, attr, value)
 
  140             msg = 
"%s has no attribute %s" % (_typeStr(self.
_field), attr)
 
  145     """A configuration field (`~lsst.pex.config.Field` subclass) that maps keys 
  148     The types of both items and keys are restricted to these builtin types: 
  149     `int`, `float`, `complex`, `bool`, and `str`). All keys share the same type 
  150     and all values share the same type. Keys can have a different type from 
  156         A documentation string that describes the configuration field. 
  157     keytype : {`int`, `float`, `complex`, `bool`, `str`} 
  158         The type of the mapping keys. All keys must have this type. 
  159     itemtype : {`int`, `float`, `complex`, `bool`, `str`} 
  160         Type of the mapping values. 
  161     default : `dict`, optional 
  163     optional : `bool`, optional 
  164         If `True`, the field doesn't need to have a set value. 
  166         A function that validates the dictionary as a whole. 
  168         A function that validates individual mapping values. 
  169     deprecated : None or `str`, optional 
  170         A description of why this Field is deprecated, including removal date. 
  171         If not None, the string is appended to the docstring for this Field. 
  187     This field maps has `str` keys and `int` values: 
  189     >>> from lsst.pex.config import Config, DictField 
  190     >>> class MyConfig(Config): 
  191     ...     field = DictField( 
  192     ...         doc="Example string-to-int mapping field.", 
  193     ...         keytype=str, itemtype=int, 
  196     >>> config = MyConfig() 
  197     >>> config.field['myKey'] = 42 
  198     >>> print(config.field) 
  204     def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None,
 
  207         self.
_setup(doc=doc, dtype=Dict, default=default, check=
None,
 
  208                     optional=optional, source=source, deprecated=deprecated)
 
  210             raise ValueError(
"'keytype' %s is not a supported type" %
 
  212         elif itemtype 
is not None and itemtype 
not in self.
supportedTypes:
 
  213             raise ValueError(
"'itemtype' %s is not a supported type" %
 
  215         if dictCheck 
is not None and not hasattr(dictCheck, 
"__call__"):
 
  216             raise ValueError(
"'dictCheck' must be callable")
 
  217         if itemCheck 
is not None and not hasattr(itemCheck, 
"__call__"):
 
  218             raise ValueError(
"'itemCheck' must be callable")
 
  226         """Validate the field's value (for internal use only). 
  230         instance : `lsst.pex.config.Config` 
  231             The configuration that contains this field. 
  236             `True` is returned if the field passes validation criteria (see 
  237             *Notes*). Otherwise `False`. 
  241         This method validates values according to the following criteria: 
  243         - A non-optional field is not `None`. 
  244         - If a value is not `None`, is must pass the `ConfigField.dictCheck` 
  245           user callback functon. 
  247         Individual item checks by the `ConfigField.itemCheck` user callback 
  248         function are done immediately when the value is set on a key. Those 
  249         checks are not repeated by this method. 
  251         Field.validate(self, instance)
 
  253         if value 
is not None and self.
dictCheck is not None \
 
  255             msg = 
"%s is not a valid value" % str(value)
 
  258     def __set__(self, instance, value, at=None, label="assignment"):
 
  260             msg = 
"Cannot modify a frozen Config. "\
 
  261                   "Attempting to set field to value %s" % value
 
  266         if value 
is not None:
 
  267             value = self.
DictClass(instance, self, value, at=at, label=label)
 
  269             history = instance._history.setdefault(self.name, [])
 
  270             history.append((value, at, label))
 
  272         instance._storage[self.name] = value
 
  275         """Convert this field's key-value pairs into a regular `dict`. 
  279         instance : `lsst.pex.config.Config` 
  280             The configuration that contains this field. 
  284         result : `dict` or `None` 
  285             If this field has a value of `None`, then this method returns 
  286             `None`. Otherwise, this method returns the field's value as a 
  287             regular Python `dict`. 
  290         return dict(value) 
if value 
is not None else None 
  292     def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
 
  293         """Compare two fields for equality. 
  295         Used by `lsst.pex.ConfigDictField.compare`. 
  299         instance1 : `lsst.pex.config.Config` 
  300             Left-hand side config instance to compare. 
  301         instance2 : `lsst.pex.config.Config` 
  302             Right-hand side config instance to compare. 
  304             If `True`, this function returns as soon as an inequality if found. 
  306             Relative tolerance for floating point comparisons. 
  308             Absolute tolerance for floating point comparisons. 
  310             A callable that takes a string, used (possibly repeatedly) to 
  316             `True` if the fields are equal, `False` otherwise. 
  320         Floating point comparisons are performed by `numpy.allclose`. 
  322         d1 = getattr(instance1, self.name)
 
  323         d2 = getattr(instance2, self.name)
 
  325             _joinNamePath(instance1._name, self.name),
 
  326             _joinNamePath(instance2._name, self.name)
 
  328         if not compareScalars(
"isnone for %s" % name, d1 
is None, d2 
is None, output=output):
 
  330         if d1 
is None and d2 
is None:
 
  335         for k, v1 
in d1.items():
 
  338                                     rtol=rtol, atol=atol, output=output)
 
  339             if not result 
and shortcut:
 
  341             equal = equal 
and result