lsst.pex.config  13.0-1-g41367f3+6
 All Classes Namespaces Files Functions Variables Properties Macros Pages
configDictField.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 __future__ import print_function
23 
24 import traceback
25 
26 from .config import Config, FieldValidationError, _autocast, _typeStr, _joinNamePath
27 from .dictField import Dict, DictField
28 from .comparison import compareConfigs, compareScalars, getComparisonName
29 
30 __all__ = ["ConfigDictField"]
31 
32 
33 class ConfigDict(Dict):
34  """
35  Config-Insternal representation of a dict of config classes
36 
37  Much like Dict, ConfigDict is a custom MutableMapper which tracks the
38  history of changes to any of its items.
39  """
40  def __init__(self, config, field, value, at, label):
41  Dict.__init__(self, config, field, value, at, label, setHistory=False)
42  self.history.append(("Dict initialized", at, label))
43 
44  def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
45  if self._config._frozen:
46  msg = "Cannot modify a frozen Config. "\
47  "Attempting to set item at key %r to value %s" % (k, x)
48  raise FieldValidationError(self._field, self._config, msg)
49 
50  # validate keytype
51  k = _autocast(k, self._field.keytype)
52  if type(k) != self._field.keytype:
53  msg = "Key %r is of type %s, expected type %s" % \
54  (k, _typeStr(k), _typeStr(self._field.keytype))
55  raise FieldValidationError(self._field, self._config, msg)
56 
57  # validate itemtype
58  dtype = self._field.itemtype
59  if type(x) != self._field.itemtype and x != self._field.itemtype:
60  msg = "Value %s at key %r is of incorrect type %s. Expected type %s" % \
61  (x, k, _typeStr(x), _typeStr(self._field.itemtype))
62  raise FieldValidationError(self._field, self._config, msg)
63 
64  if at is None:
65  at = traceback.extract_stack()[:-1]
66  name = _joinNamePath(self._config._name, self._field.name, k)
67  oldValue = self._dict.get(k, None)
68  if oldValue is None:
69  if x == dtype:
70  self._dict[k] = dtype(__name=name, __at=at, __label=label)
71  else:
72  self._dict[k] = dtype(__name=name, __at=at, __label=label, **x._storage)
73  if setHistory:
74  self.history.append(("Added item at key %s" % k, at, label))
75  else:
76  if x == dtype:
77  x = dtype()
78  oldValue.update(__at=at, __label=label, **x._storage)
79  if setHistory:
80  self.history.append(("Modified item at key %s" % k, at, label))
81 
82  def __delitem__(self, k, at=None, label="delitem"):
83  if at is None:
84  at = traceback.extract_stack()[:-1]
85  Dict.__delitem__(self, k, at, label, False)
86  self.history.append(("Removed item at key %s" % k, at, label))
87 
88 
89 class ConfigDictField(DictField):
90  """
91  Defines a field which is a mapping between a POD and a config class.
92 
93  This behaves exactly like a DictField with the slight difference that
94  itemtype must be an subclass of Config.
95 
96  This allows config writters to create name-to-config mappings. One use case
97  is for configuring mappings for dataset types in a butler. In this case,
98  the dataset type names are arbitrary and user-selected; the mapping
99  configurations are known and fixed.
100  """
101 
102  DictClass = ConfigDict
103 
104  def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None):
105  source = traceback.extract_stack(limit=2)[0]
106  self._setup(doc=doc, dtype=ConfigDict, default=default, check=None,
107  optional=optional, source=source)
108  if keytype not in self.supportedTypes:
109  raise ValueError("'keytype' %s is not a supported type" %
110  _typeStr(keytype))
111  elif not issubclass(itemtype, Config):
112  raise ValueError("'itemtype' %s is not a supported type" %
113  _typeStr(itemtype))
114  if dictCheck is not None and not hasattr(dictCheck, "__call__"):
115  raise ValueError("'dictCheck' must be callable")
116  if itemCheck is not None and not hasattr(itemCheck, "__call__"):
117  raise ValueError("'itemCheck' must be callable")
118 
119  self.keytype = keytype
120  self.itemtype = itemtype
121  self.dictCheck = dictCheck
122  self.itemCheck = itemCheck
123 
124  def rename(self, instance):
125  configDict = self.__get__(instance)
126  if configDict is not None:
127  for k in configDict:
128  fullname = _joinNamePath(instance._name, self.name, k)
129  configDict[k]._rename(fullname)
130 
131  def validate(self, instance):
132  value = self.__get__(instance)
133  if value is not None:
134  for k in value:
135  item = value[k]
136  item.validate()
137  if self.itemCheck is not None and not self.itemCheck(item):
138  msg = "Item at key %r is not a valid value: %s" % (k, item)
139  raise FieldValidationError(self, instance, msg)
140  DictField.validate(self, instance)
141 
142  def toDict(self, instance):
143  configDict = self.__get__(instance)
144  if configDict is None:
145  return None
146 
147  dict_ = {}
148  for k in configDict:
149  dict_[k] = configDict[k].toDict()
150 
151  return dict_
152 
153  def save(self, outfile, instance):
154  configDict = self.__get__(instance)
155  fullname = _joinNamePath(instance._name, self.name)
156  if configDict is None:
157  outfile.write(u"{}={!r}\n".format(fullname, configDict))
158  return
159 
160  outfile.write(u"{}={!r}\n".format(fullname, {}))
161  for v in configDict.values():
162  outfile.write(u"{}={}()\n".format(v._name, _typeStr(v)))
163  v._save(outfile)
164 
165  def freeze(self, instance):
166  configDict = self.__get__(instance)
167  if configDict is not None:
168  for k in configDict:
169  configDict[k].freeze()
170 
171  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
172  """Helper function for Config.compare; used to compare two fields for equality.
173 
174  @param[in] instance1 LHS Config instance to compare.
175  @param[in] instance2 RHS Config instance to compare.
176  @param[in] shortcut If True, return as soon as an inequality is found.
177  @param[in] rtol Relative tolerance for floating point comparisons.
178  @param[in] atol Absolute tolerance for floating point comparisons.
179  @param[in] output If not None, a callable that takes a string, used (possibly repeatedly)
180  to report inequalities.
181 
182  Floating point comparisons are performed by numpy.allclose; refer to that for details.
183  """
184  d1 = getattr(instance1, self.name)
185  d2 = getattr(instance2, self.name)
186  name = getComparisonName(
187  _joinNamePath(instance1._name, self.name),
188  _joinNamePath(instance2._name, self.name)
189  )
190  if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
191  return False
192  equal = True
193  for k, v1 in d1.items():
194  v2 = d2[k]
195  result = compareConfigs("%s[%r]" % (name, k), v1, v2, shortcut=shortcut,
196  rtol=rtol, atol=atol, output=output)
197  if not result and shortcut:
198  return False
199  equal = equal and result
200  return equal