lsst.pex.config  18.1.0-3-g6b74884
dictField.py
Go to the documentation of this file.
1 # This file is part of pex_config.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (http://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 
22 __all__ = ["DictField"]
23 
24 import collections.abc
25 
26 from .config import Field, FieldValidationError, _typeStr, _autocast, _joinNamePath
27 from .comparison import getComparisonName, compareScalars
28 from .callStack import getCallStack, getStackFrame
29 
30 
31 class Dict(collections.abc.MutableMapping):
32  """An internal mapping container.
33 
34  This class emulates a `dict`, but adds validation and provenance.
35  """
36 
37  def __init__(self, config, field, value, at, label, setHistory=True):
38  self._field = field
39  self._config = config
40  self._dict = {}
41  self._history = self._config._history.setdefault(self._field.name, [])
42  self.__doc__ = field.doc
43  if value is not None:
44  try:
45  for k in value:
46  # do not set history per-item
47  self.__setitem__(k, value[k], at=at, label=label, setHistory=False)
48  except TypeError:
49  msg = "Value %s is of incorrect type %s. Mapping type expected." % \
50  (value, _typeStr(value))
51  raise FieldValidationError(self._field, self._config, msg)
52  if setHistory:
53  self._history.append((dict(self._dict), at, label))
54 
55  history = property(lambda x: x._history)
56  """History (read-only).
57  """
58 
59  def __getitem__(self, k):
60  return self._dict[k]
61 
62  def __len__(self):
63  return len(self._dict)
64 
65  def __iter__(self):
66  return iter(self._dict)
67 
68  def __contains__(self, k):
69  return k in self._dict
70 
71  def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
72  if self._config._frozen:
73  msg = "Cannot modify a frozen Config. "\
74  "Attempting to set item at key %r to value %s" % (k, x)
75  raise FieldValidationError(self._field, self._config, msg)
76 
77  # validate keytype
78  k = _autocast(k, self._field.keytype)
79  if type(k) != self._field.keytype:
80  msg = "Key %r is of type %s, expected type %s" % \
81  (k, _typeStr(k), _typeStr(self._field.keytype))
82  raise FieldValidationError(self._field, self._config, msg)
83 
84  # validate itemtype
85  x = _autocast(x, self._field.itemtype)
86  if self._field.itemtype is None:
87  if type(x) not in self._field.supportedTypes and x is not None:
88  msg = "Value %s at key %r is of invalid type %s" % (x, k, _typeStr(x))
89  raise FieldValidationError(self._field, self._config, msg)
90  else:
91  if type(x) != self._field.itemtype and x is not None:
92  msg = "Value %s at key %r is of incorrect type %s. Expected type %s" % \
93  (x, k, _typeStr(x), _typeStr(self._field.itemtype))
94  raise FieldValidationError(self._field, self._config, msg)
95 
96  # validate item using itemcheck
97  if self._field.itemCheck is not None and not self._field.itemCheck(x):
98  msg = "Item at key %r is not a valid value: %s" % (k, x)
99  raise FieldValidationError(self._field, self._config, msg)
100 
101  if at is None:
102  at = getCallStack()
103 
104  self._dict[k] = x
105  if setHistory:
106  self._history.append((dict(self._dict), at, label))
107 
108  def __delitem__(self, k, at=None, label="delitem", setHistory=True):
109  if self._config._frozen:
110  raise FieldValidationError(self._field, self._config,
111  "Cannot modify a frozen Config")
112 
113  del self._dict[k]
114  if setHistory:
115  if at is None:
116  at = getCallStack()
117  self._history.append((dict(self._dict), at, label))
118 
119  def __repr__(self):
120  return repr(self._dict)
121 
122  def __str__(self):
123  return str(self._dict)
124 
125  def __setattr__(self, attr, value, at=None, label="assignment"):
126  if hasattr(getattr(self.__class__, attr, None), '__set__'):
127  # This allows properties to work.
128  object.__setattr__(self, attr, value)
129  elif attr in self.__dict__ or attr in ["_field", "_config", "_history", "_dict", "__doc__"]:
130  # This allows specific private attributes to work.
131  object.__setattr__(self, attr, value)
132  else:
133  # We throw everything else.
134  msg = "%s has no attribute %s" % (_typeStr(self._field), attr)
135  raise FieldValidationError(self._field, self._config, msg)
136 
137 
139  """A configuration field (`~lsst.pex.config.Field` subclass) that maps keys
140  and values.
141 
142  The types of both items and keys are restricted to these builtin types:
143  `int`, `float`, `complex`, `bool`, and `str`). All keys share the same type
144  and all values share the same type. Keys can have a different type from
145  values.
146 
147  Parameters
148  ----------
149  doc : `str`
150  A documentation string that describes the configuration field.
151  keytype : {`int`, `float`, `complex`, `bool`, `str`}
152  The type of the mapping keys. All keys must have this type.
153  itemtype : {`int`, `float`, `complex`, `bool`, `str`}
154  Type of the mapping values.
155  default : `dict`, optional
156  The default mapping.
157  optional : `bool`, optional
158  If `True`, the field doesn't need to have a set value.
159  dictCheck : callable
160  A function that validates the dictionary as a whole.
161  itemCheck : callable
162  A function that validates individual mapping values.
163  deprecated : None or `str`, optional
164  A description of why this Field is deprecated, including removal date.
165  If not None, the string is appended to the docstring for this Field.
166 
167  See also
168  --------
169  ChoiceField
170  ConfigChoiceField
171  ConfigDictField
172  ConfigField
173  ConfigurableField
174  Field
175  ListField
176  RangeField
177  RegistryField
178 
179  Examples
180  --------
181  This field maps has `str` keys and `int` values:
182 
183  >>> from lsst.pex.config import Config, DictField
184  >>> class MyConfig(Config):
185  ... field = DictField(
186  ... doc="Example string-to-int mapping field.",
187  ... keytype=str, itemtype=int,
188  ... default={})
189  ...
190  >>> config = MyConfig()
191  >>> config.field['myKey'] = 42
192  >>> print(config.field)
193  {'myKey': 42}
194  """
195 
196  DictClass = Dict
197 
198  def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None,
199  deprecated=None):
200  source = getStackFrame()
201  self._setup(doc=doc, dtype=Dict, default=default, check=None,
202  optional=optional, source=source, deprecated=deprecated)
203  if keytype not in self.supportedTypes:
204  raise ValueError("'keytype' %s is not a supported type" %
205  _typeStr(keytype))
206  elif itemtype is not None and itemtype not in self.supportedTypes:
207  raise ValueError("'itemtype' %s is not a supported type" %
208  _typeStr(itemtype))
209  if dictCheck is not None and not hasattr(dictCheck, "__call__"):
210  raise ValueError("'dictCheck' must be callable")
211  if itemCheck is not None and not hasattr(itemCheck, "__call__"):
212  raise ValueError("'itemCheck' must be callable")
213 
214  self.keytype = keytype
215  self.itemtype = itemtype
216  self.dictCheck = dictCheck
217  self.itemCheck = itemCheck
218 
219  def validate(self, instance):
220  """Validate the field's value (for internal use only).
221 
222  Parameters
223  ----------
224  instance : `lsst.pex.config.Config`
225  The configuration that contains this field.
226 
227  Returns
228  -------
229  isValid : `bool`
230  `True` is returned if the field passes validation criteria (see
231  *Notes*). Otherwise `False`.
232 
233  Notes
234  -----
235  This method validates values according to the following criteria:
236 
237  - A non-optional field is not `None`.
238  - If a value is not `None`, is must pass the `ConfigField.dictCheck`
239  user callback functon.
240 
241  Individual item checks by the `ConfigField.itemCheck` user callback
242  function are done immediately when the value is set on a key. Those
243  checks are not repeated by this method.
244  """
245  Field.validate(self, instance)
246  value = self.__get__(instance)
247  if value is not None and self.dictCheck is not None \
248  and not self.dictCheck(value):
249  msg = "%s is not a valid value" % str(value)
250  raise FieldValidationError(self, instance, msg)
251 
252  def __set__(self, instance, value, at=None, label="assignment"):
253  if instance._frozen:
254  msg = "Cannot modify a frozen Config. "\
255  "Attempting to set field to value %s" % value
256  raise FieldValidationError(self, instance, msg)
257 
258  if at is None:
259  at = getCallStack()
260  if value is not None:
261  value = self.DictClass(instance, self, value, at=at, label=label)
262  else:
263  history = instance._history.setdefault(self.name, [])
264  history.append((value, at, label))
265 
266  instance._storage[self.name] = value
267 
268  def toDict(self, instance):
269  """Convert this field's key-value pairs into a regular `dict`.
270 
271  Parameters
272  ----------
273  instance : `lsst.pex.config.Config`
274  The configuration that contains this field.
275 
276  Returns
277  -------
278  result : `dict` or `None`
279  If this field has a value of `None`, then this method returns
280  `None`. Otherwise, this method returns the field's value as a
281  regular Python `dict`.
282  """
283  value = self.__get__(instance)
284  return dict(value) if value is not None else None
285 
286  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
287  """Compare two fields for equality.
288 
289  Used by `lsst.pex.ConfigDictField.compare`.
290 
291  Parameters
292  ----------
293  instance1 : `lsst.pex.config.Config`
294  Left-hand side config instance to compare.
295  instance2 : `lsst.pex.config.Config`
296  Right-hand side config instance to compare.
297  shortcut : `bool`
298  If `True`, this function returns as soon as an inequality if found.
299  rtol : `float`
300  Relative tolerance for floating point comparisons.
301  atol : `float`
302  Absolute tolerance for floating point comparisons.
303  output : callable
304  A callable that takes a string, used (possibly repeatedly) to
305  report inequalities.
306 
307  Returns
308  -------
309  isEqual : bool
310  `True` if the fields are equal, `False` otherwise.
311 
312  Notes
313  -----
314  Floating point comparisons are performed by `numpy.allclose`.
315  """
316  d1 = getattr(instance1, self.name)
317  d2 = getattr(instance2, self.name)
318  name = getComparisonName(
319  _joinNamePath(instance1._name, self.name),
320  _joinNamePath(instance2._name, self.name)
321  )
322  if not compareScalars("isnone for %s" % name, d1 is None, d2 is None, output=output):
323  return False
324  if d1 is None and d2 is None:
325  return True
326  if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
327  return False
328  equal = True
329  for k, v1 in d1.items():
330  v2 = d2[k]
331  result = compareScalars("%s[%r]" % (name, k), v1, v2, dtype=self.itemtype,
332  rtol=rtol, atol=atol, output=output)
333  if not result and shortcut:
334  return False
335  equal = equal and result
336  return equal
def getCallStack(skip=0)
Definition: callStack.py:168
def __getitem__(self, k)
Definition: dictField.py:59
def __setattr__(self, attr, value, at=None, label="assignment")
Definition: dictField.py:125
def toDict(self, instance)
Definition: dictField.py:268
def __init__(self, config, field, value, at, label, setHistory=True)
Definition: dictField.py:37
def validate(self, instance)
Definition: dictField.py:219
def __contains__(self, k)
Definition: dictField.py:68
def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None, deprecated=None)
Definition: dictField.py:199
def getStackFrame(relative=0)
Definition: callStack.py:51
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:488
def __set__(self, instance, value, at=None, label="assignment")
Definition: dictField.py:252
def __setitem__(self, k, x, at=None, label="setitem", setHistory=True)
Definition: dictField.py:71
def getComparisonName(name1, name2)
Definition: comparison.py:34
def __delitem__(self, k, at=None, label="delitem", setHistory=True)
Definition: dictField.py:108
def compareScalars(name, v1, v2, output, rtol=1E-8, atol=1E-8, dtype=None)
Definition: comparison.py:56
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:278