lsst.pex.config  18.1.0-3-g6b74884
configDictField.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 program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 
22 __all__ = ["ConfigDictField"]
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 
31  """Internal representation of a dictionary of configuration classes.
32 
33  Much like `Dict`, `ConfigDict` is a custom `MutableMapper` which tracks
34  the history of changes to any of its items.
35  """
36 
37  def __init__(self, config, field, value, at, label):
38  Dict.__init__(self, config, field, value, at, label, setHistory=False)
39  self.history.append(("Dict initialized", at, label))
40 
41  def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
42  if self._config._frozen:
43  msg = "Cannot modify a frozen Config. "\
44  "Attempting to set item at key %r to value %s" % (k, x)
45  raise FieldValidationError(self._field, self._config, msg)
46 
47  # validate keytype
48  k = _autocast(k, self._field.keytype)
49  if type(k) != self._field.keytype:
50  msg = "Key %r is of type %s, expected type %s" % \
51  (k, _typeStr(k), _typeStr(self._field.keytype))
52  raise FieldValidationError(self._field, self._config, msg)
53 
54  # validate itemtype
55  dtype = self._field.itemtype
56  if type(x) != self._field.itemtype and x != self._field.itemtype:
57  msg = "Value %s at key %r is of incorrect type %s. Expected type %s" % \
58  (x, k, _typeStr(x), _typeStr(self._field.itemtype))
59  raise FieldValidationError(self._field, self._config, msg)
60 
61  if at is None:
62  at = getCallStack()
63  name = _joinNamePath(self._config._name, self._field.name, k)
64  oldValue = self._dict.get(k, None)
65  if oldValue is None:
66  if x == dtype:
67  self._dict[k] = dtype(__name=name, __at=at, __label=label)
68  else:
69  self._dict[k] = dtype(__name=name, __at=at, __label=label, **x._storage)
70  if setHistory:
71  self.history.append(("Added item at key %s" % k, at, label))
72  else:
73  if x == dtype:
74  x = dtype()
75  oldValue.update(__at=at, __label=label, **x._storage)
76  if setHistory:
77  self.history.append(("Modified item at key %s" % k, at, label))
78 
79  def __delitem__(self, k, at=None, label="delitem"):
80  if at is None:
81  at = getCallStack()
82  Dict.__delitem__(self, k, at, label, False)
83  self.history.append(("Removed item at key %s" % k, at, label))
84 
85 
87  """A configuration field (`~lsst.pex.config.Field` subclass) that is a
88  mapping of keys to `~lsst.pex.config.Config` instances.
89 
90  ``ConfigDictField`` behaves like `DictField` except that the
91  ``itemtype`` must be a `~lsst.pex.config.Config` subclass.
92 
93  Parameters
94  ----------
95  doc : `str`
96  A description of the configuration field.
97  keytype : {`int`, `float`, `complex`, `bool`, `str`}
98  The type of the mapping keys. All keys must have this type.
99  itemtype : `lsst.pex.config.Config`-type
100  The type of the values in the mapping. This must be
101  `~lsst.pex.config.Config` or a subclass.
102  default : optional
103  Unknown.
104  default : ``itemtype``-dtype, optional
105  Default value of this field.
106  optional : `bool`, optional
107  If `True`, this configuration `~lsst.pex.config.Field` is *optional*.
108  Default is `True`.
109  deprecated : None or `str`, optional
110  A description of why this Field is deprecated, including removal date.
111  If not None, the string is appended to the docstring for this Field.
112 
113  Raises
114  ------
115  ValueError
116  Raised if the inputs are invalid:
117 
118  - ``keytype`` or ``itemtype`` arguments are not supported types
119  (members of `ConfigDictField.supportedTypes`.
120  - ``dictCheck`` or ``itemCheck`` is not a callable function.
121 
122  See also
123  --------
124  ChoiceField
125  ConfigChoiceField
126  ConfigField
127  ConfigurableField
128  DictField
129  Field
130  ListField
131  RangeField
132  RegistryField
133 
134  Notes
135  -----
136  You can use ``ConfigDictField`` to create name-to-config mappings. One use
137  case is for configuring mappings for dataset types in a Butler. In this
138  case, the dataset type names are arbitrary and user-selected while the
139  mapping configurations are known and fixed.
140  """
141 
142  DictClass = ConfigDict
143 
144  def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None,
145  deprecated=None):
146  source = getStackFrame()
147  self._setup(doc=doc, dtype=ConfigDict, default=default, check=None,
148  optional=optional, source=source, deprecated=deprecated)
149  if keytype not in self.supportedTypes:
150  raise ValueError("'keytype' %s is not a supported type" %
151  _typeStr(keytype))
152  elif not issubclass(itemtype, Config):
153  raise ValueError("'itemtype' %s is not a supported type" %
154  _typeStr(itemtype))
155  if dictCheck is not None and not hasattr(dictCheck, "__call__"):
156  raise ValueError("'dictCheck' must be callable")
157  if itemCheck is not None and not hasattr(itemCheck, "__call__"):
158  raise ValueError("'itemCheck' must be callable")
159 
160  self.keytype = keytype
161  self.itemtype = itemtype
162  self.dictCheck = dictCheck
163  self.itemCheck = itemCheck
164 
165  def rename(self, instance):
166  configDict = self.__get__(instance)
167  if configDict is not None:
168  for k in configDict:
169  fullname = _joinNamePath(instance._name, self.name, k)
170  configDict[k]._rename(fullname)
171 
172  def validate(self, instance):
173  value = self.__get__(instance)
174  if value is not None:
175  for k in value:
176  item = value[k]
177  item.validate()
178  if self.itemCheck is not None and not self.itemCheck(item):
179  msg = "Item at key %r is not a valid value: %s" % (k, item)
180  raise FieldValidationError(self, instance, msg)
181  DictField.validate(self, instance)
182 
183  def toDict(self, instance):
184  configDict = self.__get__(instance)
185  if configDict is None:
186  return None
187 
188  dict_ = {}
189  for k in configDict:
190  dict_[k] = configDict[k].toDict()
191 
192  return dict_
193 
194  def save(self, outfile, instance):
195  configDict = self.__get__(instance)
196  fullname = _joinNamePath(instance._name, self.name)
197  if configDict is None:
198  outfile.write(u"{}={!r}\n".format(fullname, configDict))
199  return
200 
201  outfile.write(u"{}={!r}\n".format(fullname, {}))
202  for v in configDict.values():
203  outfile.write(u"{}={}()\n".format(v._name, _typeStr(v)))
204  v._save(outfile)
205 
206  def freeze(self, instance):
207  configDict = self.__get__(instance)
208  if configDict is not None:
209  for k in configDict:
210  configDict[k].freeze()
211 
212  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
213  """Compare two fields for equality.
214 
215  Used by `lsst.pex.ConfigDictField.compare`.
216 
217  Parameters
218  ----------
219  instance1 : `lsst.pex.config.Config`
220  Left-hand side config instance to compare.
221  instance2 : `lsst.pex.config.Config`
222  Right-hand side config instance to compare.
223  shortcut : `bool`
224  If `True`, this function returns as soon as an inequality if found.
225  rtol : `float`
226  Relative tolerance for floating point comparisons.
227  atol : `float`
228  Absolute tolerance for floating point comparisons.
229  output : callable
230  A callable that takes a string, used (possibly repeatedly) to report inequalities.
231 
232  Returns
233  -------
234  isEqual : bool
235  `True` if the fields are equal, `False` otherwise.
236 
237  Notes
238  -----
239  Floating point comparisons are performed by `numpy.allclose`.
240  """
241  d1 = getattr(instance1, self.name)
242  d2 = getattr(instance2, self.name)
243  name = getComparisonName(
244  _joinNamePath(instance1._name, self.name),
245  _joinNamePath(instance2._name, self.name)
246  )
247  if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
248  return False
249  equal = True
250  for k, v1 in d1.items():
251  v2 = d2[k]
252  result = compareConfigs("%s[%r]" % (name, k), v1, v2, shortcut=shortcut,
253  rtol=rtol, atol=atol, output=output)
254  if not result and shortcut:
255  return False
256  equal = equal and result
257  return equal
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:167
def getCallStack(skip=0)
Definition: callStack.py:168
def compareConfigs(name, c1, c2, shortcut=True, rtol=1E-8, atol=1E-8, output=None)
Definition: comparison.py:105
def __init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, itemCheck=None, deprecated=None)
def getStackFrame(relative=0)
Definition: callStack.py:51
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:488
def __delitem__(self, k, at=None, label="delitem")
def __init__(self, config, field, value, at, label)
def getComparisonName(name1, name2)
Definition: comparison.py:34
def compareScalars(name, v1, v2, output, rtol=1E-8, atol=1E-8, dtype=None)
Definition: comparison.py:56
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:278
def __setitem__(self, k, x, at=None, label="setitem", setHistory=True)