LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
LSST Data Management Base Package
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, Config
33 from .comparison import getComparisonName, compareScalars
34 from .callStack import getCallStack, getStackFrame
35 
36 import weakref
37 
38 
39 class Dict(collections.abc.MutableMapping):
40  """An internal mapping container.
41 
42  This class emulates a `dict`, but adds validation and provenance.
43  """
44 
45  def __init__(self, config, field, value, at, label, setHistory=True):
46  self._field_field = field
47  self._config__config_ = weakref.ref(config)
48  self._dict_dict = {}
49  self._history_history = self._config_config._history.setdefault(self._field_field.name, [])
50  self.__doc____doc__ = field.doc
51  if value is not None:
52  try:
53  for k in value:
54  # do not set history per-item
55  self.__setitem____setitem__(k, value[k], at=at, label=label, setHistory=False)
56  except TypeError:
57  msg = "Value %s is of incorrect type %s. Mapping type expected." % \
58  (value, _typeStr(value))
59  raise FieldValidationError(self._field_field, self._config_config, msg)
60  if setHistory:
61  self._history_history.append((dict(self._dict_dict), at, label))
62 
63  @property
64  def _config(self) -> Config:
65  # Config Fields should never outlive their config class instance
66  # assert that as such here
67  assert(self._config__config_() is not None)
68  return self._config__config_()
69 
70  history = property(lambda x: x._history)
71  """History (read-only).
72  """
73 
74  def __getitem__(self, k):
75  return self._dict_dict[k]
76 
77  def __len__(self):
78  return len(self._dict_dict)
79 
80  def __iter__(self):
81  return iter(self._dict_dict)
82 
83  def __contains__(self, k):
84  return k in self._dict_dict
85 
86  def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
87  if self._config_config._frozen:
88  msg = "Cannot modify a frozen Config. "\
89  "Attempting to set item at key %r to value %s" % (k, x)
90  raise FieldValidationError(self._field_field, self._config_config, msg)
91 
92  # validate keytype
93  k = _autocast(k, self._field_field.keytype)
94  if type(k) != self._field_field.keytype:
95  msg = "Key %r is of type %s, expected type %s" % \
96  (k, _typeStr(k), _typeStr(self._field_field.keytype))
97  raise FieldValidationError(self._field_field, self._config_config, msg)
98 
99  # validate itemtype
100  x = _autocast(x, self._field_field.itemtype)
101  if self._field_field.itemtype is None:
102  if type(x) not in self._field_field.supportedTypes and x is not None:
103  msg = "Value %s at key %r is of invalid type %s" % (x, k, _typeStr(x))
104  raise FieldValidationError(self._field_field, self._config_config, msg)
105  else:
106  if type(x) != self._field_field.itemtype and x is not None:
107  msg = "Value %s at key %r is of incorrect type %s. Expected type %s" % \
108  (x, k, _typeStr(x), _typeStr(self._field_field.itemtype))
109  raise FieldValidationError(self._field_field, self._config_config, msg)
110 
111  # validate item using itemcheck
112  if self._field_field.itemCheck is not None and not self._field_field.itemCheck(x):
113  msg = "Item at key %r is not a valid value: %s" % (k, x)
114  raise FieldValidationError(self._field_field, self._config_config, msg)
115 
116  if at is None:
117  at = getCallStack()
118 
119  self._dict_dict[k] = x
120  if setHistory:
121  self._history_history.append((dict(self._dict_dict), at, label))
122 
123  def __delitem__(self, k, at=None, label="delitem", setHistory=True):
124  if self._config_config._frozen:
125  raise FieldValidationError(self._field_field, self._config_config,
126  "Cannot modify a frozen Config")
127 
128  del self._dict_dict[k]
129  if setHistory:
130  if at is None:
131  at = getCallStack()
132  self._history_history.append((dict(self._dict_dict), at, label))
133 
134  def __repr__(self):
135  return repr(self._dict_dict)
136 
137  def __str__(self):
138  return str(self._dict_dict)
139 
140  def __setattr__(self, attr, value, at=None, label="assignment"):
141  if hasattr(getattr(self.__class__, attr, None), '__set__'):
142  # This allows properties to work.
143  object.__setattr__(self, attr, value)
144  elif attr in self.__dict__ or attr in ["_field", "_config_", "_history", "_dict", "__doc__"]:
145  # This allows specific private attributes to work.
146  object.__setattr__(self, attr, value)
147  else:
148  # We throw everything else.
149  msg = "%s has no attribute %s" % (_typeStr(self._field_field), attr)
150  raise FieldValidationError(self._field_field, self._config_config, msg)
151 
152 
154  """A configuration field (`~lsst.pex.config.Field` subclass) that maps keys
155  and values.
156 
157  The types of both items and keys are restricted to these builtin types:
158  `int`, `float`, `complex`, `bool`, and `str`). All keys share the same type
159  and all values share the same type. Keys can have a different type from
160  values.
161 
162  Parameters
163  ----------
164  doc : `str`
165  A documentation string that describes the configuration field.
166  keytype : {`int`, `float`, `complex`, `bool`, `str`}
167  The type of the mapping keys. All keys must have this type.
168  itemtype : {`int`, `float`, `complex`, `bool`, `str`}
169  Type of the mapping values.
170  default : `dict`, optional
171  The default mapping.
172  optional : `bool`, optional
173  If `True`, the field doesn't need to have a set value.
174  dictCheck : callable
175  A function that validates the dictionary as a whole.
176  itemCheck : callable
177  A function that validates individual mapping values.
178  deprecated : None or `str`, optional
179  A description of why this Field is deprecated, including removal date.
180  If not None, the string is appended to the docstring for this Field.
181 
182  See also
183  --------
184  ChoiceField
185  ConfigChoiceField
186  ConfigDictField
187  ConfigField
188  ConfigurableField
189  Field
190  ListField
191  RangeField
192  RegistryField
193 
194  Examples
195  --------
196  This field maps has `str` keys and `int` values:
197 
198  >>> from lsst.pex.config import Config, DictField
199  >>> class MyConfig(Config):
200  ... field = DictField(
201  ... doc="Example string-to-int mapping field.",
202  ... keytype=str, itemtype=int,
203  ... default={})
204  ...
205  >>> config = MyConfig()
206  >>> config.field['myKey'] = 42
207  >>> print(config.field)
208  {'myKey': 42}
209  """
210 
211  DictClass = Dict
212 
213  def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None,
214  deprecated=None):
215  source = getStackFrame()
216  self._setup_setup(doc=doc, dtype=Dict, default=default, check=None,
217  optional=optional, source=source, deprecated=deprecated)
218  if keytype not in self.supportedTypessupportedTypes:
219  raise ValueError("'keytype' %s is not a supported type" %
220  _typeStr(keytype))
221  elif itemtype is not None and itemtype not in self.supportedTypessupportedTypes:
222  raise ValueError("'itemtype' %s is not a supported type" %
223  _typeStr(itemtype))
224  if dictCheck is not None and not hasattr(dictCheck, "__call__"):
225  raise ValueError("'dictCheck' must be callable")
226  if itemCheck is not None and not hasattr(itemCheck, "__call__"):
227  raise ValueError("'itemCheck' must be callable")
228 
229  self.keytypekeytype = keytype
230  self.itemtypeitemtype = itemtype
231  self.dictCheckdictCheck = dictCheck
232  self.itemCheckitemCheck = itemCheck
233 
234  def validate(self, instance):
235  """Validate the field's value (for internal use only).
236 
237  Parameters
238  ----------
239  instance : `lsst.pex.config.Config`
240  The configuration that contains this field.
241 
242  Returns
243  -------
244  isValid : `bool`
245  `True` is returned if the field passes validation criteria (see
246  *Notes*). Otherwise `False`.
247 
248  Notes
249  -----
250  This method validates values according to the following criteria:
251 
252  - A non-optional field is not `None`.
253  - If a value is not `None`, is must pass the `ConfigField.dictCheck`
254  user callback functon.
255 
256  Individual item checks by the `ConfigField.itemCheck` user callback
257  function are done immediately when the value is set on a key. Those
258  checks are not repeated by this method.
259  """
260  Field.validate(self, instance)
261  value = self.__get____get__(instance)
262  if value is not None and self.dictCheckdictCheck is not None \
263  and not self.dictCheckdictCheck(value):
264  msg = "%s is not a valid value" % str(value)
265  raise FieldValidationError(self, instance, msg)
266 
267  def __set__(self, instance, value, at=None, label="assignment"):
268  if instance._frozen:
269  msg = "Cannot modify a frozen Config. "\
270  "Attempting to set field to value %s" % value
271  raise FieldValidationError(self, instance, msg)
272 
273  if at is None:
274  at = getCallStack()
275  if value is not None:
276  value = self.DictClassDictClass(instance, self, value, at=at, label=label)
277  else:
278  history = instance._history.setdefault(self.name, [])
279  history.append((value, at, label))
280 
281  instance._storage[self.name] = value
282 
283  def toDict(self, instance):
284  """Convert this field's key-value pairs into a regular `dict`.
285 
286  Parameters
287  ----------
288  instance : `lsst.pex.config.Config`
289  The configuration that contains this field.
290 
291  Returns
292  -------
293  result : `dict` or `None`
294  If this field has a value of `None`, then this method returns
295  `None`. Otherwise, this method returns the field's value as a
296  regular Python `dict`.
297  """
298  value = self.__get____get__(instance)
299  return dict(value) if value is not None else None
300 
301  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
302  """Compare two fields for equality.
303 
304  Used by `lsst.pex.ConfigDictField.compare`.
305 
306  Parameters
307  ----------
308  instance1 : `lsst.pex.config.Config`
309  Left-hand side config instance to compare.
310  instance2 : `lsst.pex.config.Config`
311  Right-hand side config instance to compare.
312  shortcut : `bool`
313  If `True`, this function returns as soon as an inequality if found.
314  rtol : `float`
315  Relative tolerance for floating point comparisons.
316  atol : `float`
317  Absolute tolerance for floating point comparisons.
318  output : callable
319  A callable that takes a string, used (possibly repeatedly) to
320  report inequalities.
321 
322  Returns
323  -------
324  isEqual : bool
325  `True` if the fields are equal, `False` otherwise.
326 
327  Notes
328  -----
329  Floating point comparisons are performed by `numpy.allclose`.
330  """
331  d1 = getattr(instance1, self.name)
332  d2 = getattr(instance2, self.name)
333  name = getComparisonName(
334  _joinNamePath(instance1._name, self.name),
335  _joinNamePath(instance2._name, self.name)
336  )
337  if not compareScalars("isnone for %s" % name, d1 is None, d2 is None, output=output):
338  return False
339  if d1 is None and d2 is None:
340  return True
341  if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
342  return False
343  equal = True
344  for k, v1 in d1.items():
345  v2 = d2[k]
346  result = compareScalars("%s[%r]" % (name, k), v1, v2, dtype=self.itemtypeitemtype,
347  rtol=rtol, atol=atol, output=output)
348  if not result and shortcut:
349  return False
350  equal = equal and result
351  return equal
table::Key< int > type
Definition: Detector.cc:163
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:550
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:336
def __set__(self, instance, value, at=None, label="assignment")
Definition: dictField.py:267
def validate(self, instance)
Definition: dictField.py:234
def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None, deprecated=None)
Definition: dictField.py:214
def __delitem__(self, k, at=None, label="delitem", setHistory=True)
Definition: dictField.py:123
def __setattr__(self, attr, value, at=None, label="assignment")
Definition: dictField.py:140
def __init__(self, config, field, value, at, label, setHistory=True)
Definition: dictField.py:45
def __setitem__(self, k, x, at=None, label="setitem", setHistory=True)
Definition: dictField.py:86
daf::base::PropertySet * set
Definition: fits.cc:912
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 getCallStack(skip=0)
Definition: callStack.py:175
def getStackFrame(relative=0)
Definition: callStack.py:58
def compareScalars(name, v1, v2, output, rtol=1E-8, atol=1E-8, dtype=None)
Definition: comparison.py:62
def getComparisonName(name1, name2)
Definition: comparison.py:40