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