lsst.pex.config  14.0-2-g319577b+12
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 from builtins import str
23 
24 import collections
25 
26 from .config import Field, FieldValidationError, _typeStr, _autocast, _joinNamePath
27 from .comparison import getComparisonName, compareScalars
28 from .callStack import getCallStack, getStackFrame
29 
30 __all__ = ["DictField"]
31 
32 
33 class Dict(collections.MutableMapping):
34  """
35  Config-Internal mapping container
36  Emulates a dict, but adds validation and provenance.
37  """
38 
39  def __init__(self, config, field, value, at, label, setHistory=True):
40  self._field = field
41  self._config = config
42  self._dict = {}
43  self._history = self._config._history.setdefault(self._field.name, [])
44  self.__doc__ = field.doc
45  if value is not None:
46  try:
47  for k in value:
48  # do not set history per-item
49  self.__setitem__(k, value[k], at=at, label=label, setHistory=False)
50  except TypeError:
51  msg = "Value %s is of incorrect type %s. Mapping type expected." % \
52  (value, _typeStr(value))
53  raise FieldValidationError(self._field, self._config, msg)
54  if setHistory:
55  self._history.append((dict(self._dict), at, label))
56 
57  """
58  Read-only history
59  """
60  history = property(lambda x: x._history)
61 
62  def __getitem__(self, k):
63  return self._dict[k]
64 
65  def __len__(self):
66  return len(self._dict)
67 
68  def __iter__(self):
69  return iter(self._dict)
70 
71  def __contains__(self, k):
72  return k in self._dict
73 
74  def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
75  if self._config._frozen:
76  msg = "Cannot modify a frozen Config. "\
77  "Attempting to set item at key %r to value %s" % (k, x)
78  raise FieldValidationError(self._field, self._config, msg)
79 
80  # validate keytype
81  k = _autocast(k, self._field.keytype)
82  if type(k) != self._field.keytype:
83  msg = "Key %r is of type %s, expected type %s" % \
84  (k, _typeStr(k), _typeStr(self._field.keytype))
85  raise FieldValidationError(self._field, self._config, msg)
86 
87  # validate itemtype
88  x = _autocast(x, self._field.itemtype)
89  if self._field.itemtype is None:
90  if type(x) not in self._field.supportedTypes and x is not None:
91  msg = "Value %s at key %r is of invalid type %s" % (x, k, _typeStr(x))
92  raise FieldValidationError(self._field, self._config, msg)
93  else:
94  if type(x) != self._field.itemtype and x is not None:
95  msg = "Value %s at key %r is of incorrect type %s. Expected type %s" % \
96  (x, k, _typeStr(x), _typeStr(self._field.itemtype))
97  raise FieldValidationError(self._field, self._config, msg)
98 
99  # validate item using itemcheck
100  if self._field.itemCheck is not None and not self._field.itemCheck(x):
101  msg = "Item at key %r is not a valid value: %s" % (k, x)
102  raise FieldValidationError(self._field, self._config, msg)
103 
104  if at is None:
105  at = getCallStack()
106 
107  self._dict[k] = x
108  if setHistory:
109  self._history.append((dict(self._dict), at, label))
110 
111  def __delitem__(self, k, at=None, label="delitem", setHistory=True):
112  if self._config._frozen:
113  raise FieldValidationError(self._field, self._config,
114  "Cannot modify a frozen Config")
115 
116  del self._dict[k]
117  if setHistory:
118  if at is None:
119  at = getCallStack()
120  self._history.append((dict(self._dict), at, label))
121 
122  def __repr__(self):
123  return repr(self._dict)
124 
125  def __str__(self):
126  return str(self._dict)
127 
128  def __setattr__(self, attr, value, at=None, label="assignment"):
129  if hasattr(getattr(self.__class__, attr, None), '__set__'):
130  # This allows properties to work.
131  object.__setattr__(self, attr, value)
132  elif attr in self.__dict__ or attr in ["_field", "_config", "_history", "_dict", "__doc__"]:
133  # This allows specific private attributes to work.
134  object.__setattr__(self, attr, value)
135  else:
136  # We throw everything else.
137  msg = "%s has no attribute %s" % (_typeStr(self._field), attr)
138  raise FieldValidationError(self._field, self._config, msg)
139 
140 
142  """
143  Defines a field which is a mapping of values
144 
145  Both key and item types are restricted to builtin POD types:
146  (int, float, complex, bool, str)
147 
148  Users can provide two check functions:
149  dictCheck: used to validate the dict as a whole, and
150  itemCheck: used to validate each item individually
151 
152  For example to define a field which is a mapping from names to int values:
153 
154  class MyConfig(Config):
155  field = DictField(
156  doc="example string-to-int mapping field",
157  keytype=str, itemtype=int,
158  default= {})
159  """
160  DictClass = Dict
161 
162  def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None):
163  source = getStackFrame()
164  self._setup(doc=doc, dtype=Dict, default=default, check=None,
165  optional=optional, source=source)
166  if keytype not in self.supportedTypes:
167  raise ValueError("'keytype' %s is not a supported type" %
168  _typeStr(keytype))
169  elif itemtype is not None and itemtype not in self.supportedTypes:
170  raise ValueError("'itemtype' %s is not a supported type" %
171  _typeStr(itemtype))
172  if dictCheck is not None and not hasattr(dictCheck, "__call__"):
173  raise ValueError("'dictCheck' must be callable")
174  if itemCheck is not None and not hasattr(itemCheck, "__call__"):
175  raise ValueError("'itemCheck' must be callable")
176 
177  self.keytype = keytype
178  self.itemtype = itemtype
179  self.dictCheck = dictCheck
180  self.itemCheck = itemCheck
181 
182  def validate(self, instance):
183  """
184  DictField validation ensures that non-optional fields are not None,
185  and that non-None values comply with dictCheck.
186  Individual Item checks are applied at set time and are not re-checked.
187  """
188  Field.validate(self, instance)
189  value = self.__get__(instance)
190  if value is not None and self.dictCheck is not None \
191  and not self.dictCheck(value):
192  msg = "%s is not a valid value" % str(value)
193  raise FieldValidationError(self, instance, msg)
194 
195  def __set__(self, instance, value, at=None, label="assignment"):
196  if instance._frozen:
197  msg = "Cannot modify a frozen Config. "\
198  "Attempting to set field to value %s" % value
199  raise FieldValidationError(self, instance, msg)
200 
201  if at is None:
202  at = getCallStack()
203  if value is not None:
204  value = self.DictClass(instance, self, value, at=at, label=label)
205  else:
206  history = instance._history.setdefault(self.name, [])
207  history.append((value, at, label))
208 
209  instance._storage[self.name] = value
210 
211  def toDict(self, instance):
212  value = self.__get__(instance)
213  return dict(value) if value is not None else None
214 
215  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
216  """Helper function for Config.compare; used to compare two fields for equality.
217 
218  @param[in] instance1 LHS Config instance to compare.
219  @param[in] instance2 RHS Config instance to compare.
220  @param[in] shortcut If True, return as soon as an inequality is found.
221  @param[in] rtol Relative tolerance for floating point comparisons.
222  @param[in] atol Absolute tolerance for floating point comparisons.
223  @param[in] output If not None, a callable that takes a string, used (possibly repeatedly)
224  to report inequalities.
225 
226  Floating point comparisons are performed by numpy.allclose; refer to that for details.
227  """
228  d1 = getattr(instance1, self.name)
229  d2 = getattr(instance2, self.name)
230  name = getComparisonName(
231  _joinNamePath(instance1._name, self.name),
232  _joinNamePath(instance2._name, self.name)
233  )
234  if not compareScalars("isnone for %s" % name, d1 is None, d2 is None, output=output):
235  return False
236  if d1 is None and d2 is None:
237  return True
238  if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
239  return False
240  equal = True
241  for k, v1 in d1.items():
242  v2 = d2[k]
243  result = compareScalars("%s[%r]" % (name, k), v1, v2, dtype=self.itemtype,
244  rtol=rtol, atol=atol, output=output)
245  if not result and shortcut:
246  return False
247  equal = equal and result
248  return equal
def __init__(self, config, field, value, at, label, setHistory=True)
Definition: dictField.py:39
def __setitem__(self, k, x, at=None, label="setitem", setHistory=True)
Definition: dictField.py:74
def getCallStack(skip=0)
Definition: callStack.py:157
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:287
def _setup(self, doc, dtype, default, check, optional, source)
Definition: config.py:188
def getStackFrame(relative=0)
Definition: callStack.py:54
def __delitem__(self, k, at=None, label="delitem", setHistory=True)
Definition: dictField.py:111
def validate(self, instance)
Definition: dictField.py:182
def __setattr__(self, attr, value, at=None, label="assignment")
Definition: dictField.py:128
def compareScalars(name, v1, v2, output, rtol=1E-8, atol=1E-8, dtype=None)
Definition: comparison.py:41
def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None)
Definition: dictField.py:162
def getComparisonName(name1, name2)
Definition: comparison.py:35
def __set__(self, instance, value, at=None, label="assignment")
Definition: dictField.py:195