LSSTApplications  18.0.0+106,18.0.0+50,19.0.0,19.0.0+1,19.0.0+10,19.0.0+11,19.0.0+13,19.0.0+17,19.0.0+2,19.0.0-1-g20d9b18+6,19.0.0-1-g425ff20,19.0.0-1-g5549ca4,19.0.0-1-g580fafe+6,19.0.0-1-g6fe20d0+1,19.0.0-1-g7011481+9,19.0.0-1-g8c57eb9+6,19.0.0-1-gb5175dc+11,19.0.0-1-gdc0e4a7+9,19.0.0-1-ge272bc4+6,19.0.0-1-ge3aa853,19.0.0-10-g448f008b,19.0.0-12-g6990b2c,19.0.0-2-g0d9f9cd+11,19.0.0-2-g3d9e4fb2+11,19.0.0-2-g5037de4,19.0.0-2-gb96a1c4+3,19.0.0-2-gd955cfd+15,19.0.0-3-g2d13df8,19.0.0-3-g6f3c7dc,19.0.0-4-g725f80e+11,19.0.0-4-ga671dab3b+1,19.0.0-4-gad373c5+3,19.0.0-5-ga2acb9c+2,19.0.0-5-gfe96e6c+2,w.2020.01
LSSTDataManagementBasePackage
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 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/>.
27 
28 __all__ = ["DictField"]
29 
30 import collections.abc
31 
32 from .config import Field, FieldValidationError, _typeStr, _autocast, _joinNamePath
33 from .comparison import getComparisonName, compareScalars
34 from .callStack import getCallStack, getStackFrame
35 
36 
37 class Dict(collections.abc.MutableMapping):
38  """An internal mapping container.
39 
40  This class emulates a `dict`, but adds validation and provenance.
41  """
42 
43  def __init__(self, config, field, value, at, label, setHistory=True):
44  self._field = field
45  self._config = config
46  self._dict = {}
47  self._history = self._config._history.setdefault(self._field.name, [])
48  self.__doc__ = field.doc
49  if value is not None:
50  try:
51  for k in value:
52  # do not set history per-item
53  self.__setitem__(k, value[k], at=at, label=label, setHistory=False)
54  except TypeError:
55  msg = "Value %s is of incorrect type %s. Mapping type expected." % \
56  (value, _typeStr(value))
57  raise FieldValidationError(self._field, self._config, msg)
58  if setHistory:
59  self._history.append((dict(self._dict), at, label))
60 
61  history = property(lambda x: x._history)
62  """History (read-only).
63  """
64 
65  def __getitem__(self, k):
66  return self._dict[k]
67 
68  def __len__(self):
69  return len(self._dict)
70 
71  def __iter__(self):
72  return iter(self._dict)
73 
74  def __contains__(self, k):
75  return k in self._dict
76 
77  def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
78  if self._config._frozen:
79  msg = "Cannot modify a frozen Config. "\
80  "Attempting to set item at key %r to value %s" % (k, x)
81  raise FieldValidationError(self._field, self._config, msg)
82 
83  # validate keytype
84  k = _autocast(k, self._field.keytype)
85  if type(k) != self._field.keytype:
86  msg = "Key %r is of type %s, expected type %s" % \
87  (k, _typeStr(k), _typeStr(self._field.keytype))
88  raise FieldValidationError(self._field, self._config, msg)
89 
90  # validate itemtype
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))
95  raise FieldValidationError(self._field, self._config, msg)
96  else:
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))
100  raise FieldValidationError(self._field, self._config, msg)
101 
102  # validate item using itemcheck
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)
105  raise FieldValidationError(self._field, self._config, msg)
106 
107  if at is None:
108  at = getCallStack()
109 
110  self._dict[k] = x
111  if setHistory:
112  self._history.append((dict(self._dict), at, label))
113 
114  def __delitem__(self, k, at=None, label="delitem", setHistory=True):
115  if self._config._frozen:
116  raise FieldValidationError(self._field, self._config,
117  "Cannot modify a frozen Config")
118 
119  del self._dict[k]
120  if setHistory:
121  if at is None:
122  at = getCallStack()
123  self._history.append((dict(self._dict), at, label))
124 
125  def __repr__(self):
126  return repr(self._dict)
127 
128  def __str__(self):
129  return str(self._dict)
130 
131  def __setattr__(self, attr, value, at=None, label="assignment"):
132  if hasattr(getattr(self.__class__, attr, None), '__set__'):
133  # This allows properties to work.
134  object.__setattr__(self, attr, value)
135  elif attr in self.__dict__ or attr in ["_field", "_config", "_history", "_dict", "__doc__"]:
136  # This allows specific private attributes to work.
137  object.__setattr__(self, attr, value)
138  else:
139  # We throw everything else.
140  msg = "%s has no attribute %s" % (_typeStr(self._field), attr)
141  raise FieldValidationError(self._field, self._config, msg)
142 
143 
145  """A configuration field (`~lsst.pex.config.Field` subclass) that maps keys
146  and values.
147 
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
151  values.
152 
153  Parameters
154  ----------
155  doc : `str`
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
162  The default mapping.
163  optional : `bool`, optional
164  If `True`, the field doesn't need to have a set value.
165  dictCheck : callable
166  A function that validates the dictionary as a whole.
167  itemCheck : callable
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.
172 
173  See also
174  --------
175  ChoiceField
176  ConfigChoiceField
177  ConfigDictField
178  ConfigField
179  ConfigurableField
180  Field
181  ListField
182  RangeField
183  RegistryField
184 
185  Examples
186  --------
187  This field maps has `str` keys and `int` values:
188 
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,
194  ... default={})
195  ...
196  >>> config = MyConfig()
197  >>> config.field['myKey'] = 42
198  >>> print(config.field)
199  {'myKey': 42}
200  """
201 
202  DictClass = Dict
203 
204  def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None,
205  deprecated=None):
206  source = getStackFrame()
207  self._setup(doc=doc, dtype=Dict, default=default, check=None,
208  optional=optional, source=source, deprecated=deprecated)
209  if keytype not in self.supportedTypes:
210  raise ValueError("'keytype' %s is not a supported type" %
211  _typeStr(keytype))
212  elif itemtype is not None and itemtype not in self.supportedTypes:
213  raise ValueError("'itemtype' %s is not a supported type" %
214  _typeStr(itemtype))
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")
219 
220  self.keytype = keytype
221  self.itemtype = itemtype
222  self.dictCheck = dictCheck
223  self.itemCheck = itemCheck
224 
225  def validate(self, instance):
226  """Validate the field's value (for internal use only).
227 
228  Parameters
229  ----------
230  instance : `lsst.pex.config.Config`
231  The configuration that contains this field.
232 
233  Returns
234  -------
235  isValid : `bool`
236  `True` is returned if the field passes validation criteria (see
237  *Notes*). Otherwise `False`.
238 
239  Notes
240  -----
241  This method validates values according to the following criteria:
242 
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.
246 
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.
250  """
251  Field.validate(self, instance)
252  value = self.__get__(instance)
253  if value is not None and self.dictCheck is not None \
254  and not self.dictCheck(value):
255  msg = "%s is not a valid value" % str(value)
256  raise FieldValidationError(self, instance, msg)
257 
258  def __set__(self, instance, value, at=None, label="assignment"):
259  if instance._frozen:
260  msg = "Cannot modify a frozen Config. "\
261  "Attempting to set field to value %s" % value
262  raise FieldValidationError(self, instance, msg)
263 
264  if at is None:
265  at = getCallStack()
266  if value is not None:
267  value = self.DictClass(instance, self, value, at=at, label=label)
268  else:
269  history = instance._history.setdefault(self.name, [])
270  history.append((value, at, label))
271 
272  instance._storage[self.name] = value
273 
274  def toDict(self, instance):
275  """Convert this field's key-value pairs into a regular `dict`.
276 
277  Parameters
278  ----------
279  instance : `lsst.pex.config.Config`
280  The configuration that contains this field.
281 
282  Returns
283  -------
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`.
288  """
289  value = self.__get__(instance)
290  return dict(value) if value is not None else None
291 
292  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
293  """Compare two fields for equality.
294 
295  Used by `lsst.pex.ConfigDictField.compare`.
296 
297  Parameters
298  ----------
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.
303  shortcut : `bool`
304  If `True`, this function returns as soon as an inequality if found.
305  rtol : `float`
306  Relative tolerance for floating point comparisons.
307  atol : `float`
308  Absolute tolerance for floating point comparisons.
309  output : callable
310  A callable that takes a string, used (possibly repeatedly) to
311  report inequalities.
312 
313  Returns
314  -------
315  isEqual : bool
316  `True` if the fields are equal, `False` otherwise.
317 
318  Notes
319  -----
320  Floating point comparisons are performed by `numpy.allclose`.
321  """
322  d1 = getattr(instance1, self.name)
323  d2 = getattr(instance2, self.name)
324  name = getComparisonName(
325  _joinNamePath(instance1._name, self.name),
326  _joinNamePath(instance2._name, self.name)
327  )
328  if not compareScalars("isnone for %s" % name, d1 is None, d2 is None, output=output):
329  return False
330  if d1 is None and d2 is None:
331  return True
332  if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
333  return False
334  equal = True
335  for k, v1 in d1.items():
336  v2 = d2[k]
337  result = compareScalars("%s[%r]" % (name, k), v1, v2, dtype=self.itemtype,
338  rtol=rtol, atol=atol, output=output)
339  if not result and shortcut:
340  return False
341  equal = equal and result
342  return equal
def getCallStack(skip=0)
Definition: callStack.py:175
def __getitem__(self, k)
Definition: dictField.py:65
def __setattr__(self, attr, value, at=None, label="assignment")
Definition: dictField.py:131
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
def toDict(self, instance)
Definition: dictField.py:274
daf::base::PropertySet * set
Definition: fits.cc:902
def __init__(self, config, field, value, at, label, setHistory=True)
Definition: dictField.py:43
def validate(self, instance)
Definition: dictField.py:225
def __contains__(self, k)
Definition: dictField.py:74
def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None, deprecated=None)
Definition: dictField.py:205
def getStackFrame(relative=0)
Definition: callStack.py:58
table::Key< int > type
Definition: Detector.cc:163
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:498
def __set__(self, instance, value, at=None, label="assignment")
Definition: dictField.py:258
def __setitem__(self, k, x, at=None, label="setitem", setHistory=True)
Definition: dictField.py:77
def getComparisonName(name1, name2)
Definition: comparison.py:40
def __delitem__(self, k, at=None, label="delitem", setHistory=True)
Definition: dictField.py:114
def compareScalars(name, v1, v2, output, rtol=1E-8, atol=1E-8, dtype=None)
Definition: comparison.py:62
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:284