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