lsst.pex.config  16.0-8-g4734f7a
configChoiceField.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 
23 __all__ = ["ConfigChoiceField"]
24 
25 import copy
26 import collections
27 
28 from .config import Config, Field, FieldValidationError, _typeStr, _joinNamePath
29 from .comparison import getComparisonName, compareScalars, compareConfigs
30 from .callStack import getCallStack, getStackFrame
31 
32 
33 class SelectionSet(collections.MutableSet):
34  """
35  Custom set class used to track the selection of multi-select
36  ConfigChoiceField.
37 
38  This class allows user a multi-select ConfigChoiceField to add/discard
39  items from the set of active configs. Each change to the selection is
40  tracked in the field's history.
41  """
42  def __init__(self, dict_, value, at=None, label="assignment", setHistory=True):
43  if at is None:
44  at = getCallStack()
45  self._dict = dict_
46  self._field = self._dict._field
47  self._config = self._dict._config
48  self.__history = self._config._history.setdefault(self._field.name, [])
49  if value is not None:
50  try:
51  for v in value:
52  if v not in self._dict:
53  # invoke __getitem__ to ensure it's present
54  self._dict.__getitem__(v, at=at)
55  except TypeError:
56  msg = "Value %s is of incorrect type %s. Sequence type expected"(value, _typeStr(value))
57  raise FieldValidationError(self._field, self._config, msg)
58  self._set = set(value)
59  else:
60  self._set = set()
61 
62  if setHistory:
63  self.__history.append(("Set selection to %s" % self, at, label))
64 
65  def add(self, value, at=None):
66  if self._config._frozen:
67  raise FieldValidationError(self._field, self._config,
68  "Cannot modify a frozen Config")
69 
70  if at is None:
71  at = getCallStack()
72 
73  if value not in self._dict:
74  # invoke __getitem__ to make sure it's present
75  self._dict.__getitem__(value, at=at)
76 
77  self.__history.append(("added %s to selection" % value, at, "selection"))
78  self._set.add(value)
79 
80  def discard(self, value, at=None):
81  if self._config._frozen:
82  raise FieldValidationError(self._field, self._config,
83  "Cannot modify a frozen Config")
84 
85  if value not in self._dict:
86  return
87 
88  if at is None:
89  at = getCallStack()
90 
91  self.__history.append(("removed %s from selection" % value, at, "selection"))
92  self._set.discard(value)
93 
94  def __len__(self):
95  return len(self._set)
96 
97  def __iter__(self):
98  return iter(self._set)
99 
100  def __contains__(self, value):
101  return value in self._set
102 
103  def __repr__(self):
104  return repr(list(self._set))
105 
106  def __str__(self):
107  return str(list(self._set))
108 
109 
110 class ConfigInstanceDict(collections.Mapping):
111  """A dict of instantiated configs, used to populate a ConfigChoiceField.
112 
113  typemap must support the following:
114  - typemap[name]: return the config class associated with the given name
115  """
116  def __init__(self, config, field):
117  collections.Mapping.__init__(self)
118  self._dict = dict()
119  self._selection = None
120  self._config = config
121  self._field = field
122  self._history = config._history.setdefault(field.name, [])
123  self.__doc__ = field.doc
124 
125  types = property(lambda x: x._field.typemap)
126 
127  def __contains__(self, k):
128  return k in self._field.typemap
129 
130  def __len__(self):
131  return len(self._field.typemap)
132 
133  def __iter__(self):
134  return iter(self._field.typemap)
135 
136  def _setSelection(self, value, at=None, label="assignment"):
137  if self._config._frozen:
138  raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
139 
140  if at is None:
141  at = getCallStack(1)
142 
143  if value is None:
144  self._selection = None
145  elif self._field.multi:
146  self._selection = SelectionSet(self, value, setHistory=False)
147  else:
148  if value not in self._dict:
149  self.__getitem__(value, at=at) # just invoke __getitem__ to make sure it's present
150  self._selection = value
151  self._history.append((value, at, label))
152 
153  def _getNames(self):
154  if not self._field.multi:
155  raise FieldValidationError(self._field, self._config,
156  "Single-selection field has no attribute 'names'")
157  return self._selection
158 
159  def _setNames(self, value):
160  if not self._field.multi:
161  raise FieldValidationError(self._field, self._config,
162  "Single-selection field has no attribute 'names'")
163  self._setSelection(value)
164 
165  def _delNames(self):
166  if not self._field.multi:
167  raise FieldValidationError(self._field, self._config,
168  "Single-selection field has no attribute 'names'")
169  self._selection = None
170 
171  def _getName(self):
172  if self._field.multi:
173  raise FieldValidationError(self._field, self._config,
174  "Multi-selection field has no attribute 'name'")
175  return self._selection
176 
177  def _setName(self, value):
178  if self._field.multi:
179  raise FieldValidationError(self._field, self._config,
180  "Multi-selection field has no attribute 'name'")
181  self._setSelection(value)
182 
183  def _delName(self):
184  if self._field.multi:
185  raise FieldValidationError(self._field, self._config,
186  "Multi-selection field has no attribute 'name'")
187  self._selection = None
188 
189  """
190  In a multi-selection ConfigInstanceDict, list of names of active items
191  Disabled In a single-selection _Regsitry)
192  """
193  names = property(_getNames, _setNames, _delNames)
194 
195  """
196  In a single-selection ConfigInstanceDict, name of the active item
197  Disabled In a multi-selection _Regsitry)
198  """
199  name = property(_getName, _setName, _delName)
200 
201  def _getActive(self):
202  if self._selection is None:
203  return None
204 
205  if self._field.multi:
206  return [self[c] for c in self._selection]
207  else:
208  return self[self._selection]
209 
210  """
211  Readonly shortcut to access the selected item(s)
212  for multi-selection, this is equivalent to: [self[name] for name in self.names]
213  for single-selection, this is equivalent to: self[name]
214  """
215  active = property(_getActive)
216 
217  def __getitem__(self, k, at=None, label="default"):
218  try:
219  value = self._dict[k]
220  except KeyError:
221  try:
222  dtype = self._field.typemap[k]
223  except Exception:
224  raise FieldValidationError(self._field, self._config,
225  "Unknown key %r in Registry/ConfigChoiceField" % k)
226  name = _joinNamePath(self._config._name, self._field.name, k)
227  if at is None:
228  at = getCallStack()
229  at.insert(0, dtype._source)
230  value = self._dict.setdefault(k, dtype(__name=name, __at=at, __label=label))
231  return value
232 
233  def __setitem__(self, k, value, at=None, label="assignment"):
234  if self._config._frozen:
235  raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
236 
237  try:
238  dtype = self._field.typemap[k]
239  except Exception:
240  raise FieldValidationError(self._field, self._config, "Unknown key %r" % k)
241 
242  if value != dtype and type(value) != dtype:
243  msg = "Value %s at key %k is of incorrect type %s. Expected type %s" % \
244  (value, k, _typeStr(value), _typeStr(dtype))
245  raise FieldValidationError(self._field, self._config, msg)
246 
247  if at is None:
248  at = getCallStack()
249  name = _joinNamePath(self._config._name, self._field.name, k)
250  oldValue = self._dict.get(k, None)
251  if oldValue is None:
252  if value == dtype:
253  self._dict[k] = value(__name=name, __at=at, __label=label)
254  else:
255  self._dict[k] = dtype(__name=name, __at=at, __label=label, **value._storage)
256  else:
257  if value == dtype:
258  value = value()
259  oldValue.update(__at=at, __label=label, **value._storage)
260 
261  def _rename(self, fullname):
262  for k, v in self._dict.items():
263  v._rename(_joinNamePath(name=fullname, index=k))
264 
265  def __setattr__(self, attr, value, at=None, label="assignment"):
266  if hasattr(getattr(self.__class__, attr, None), '__set__'):
267  # This allows properties to work.
268  object.__setattr__(self, attr, value)
269  elif attr in self.__dict__ or attr in ["_history", "_field", "_config", "_dict",
270  "_selection", "__doc__"]:
271  # This allows specific private attributes to work.
272  object.__setattr__(self, attr, value)
273  else:
274  # We throw everything else.
275  msg = "%s has no attribute %s" % (_typeStr(self._field), attr)
276  raise FieldValidationError(self._field, self._config, msg)
277 
278 
280  """
281  ConfigChoiceFields allow the config to choose from a set of possible Config types.
282  The set of allowable types is given by the typemap argument to the constructor
283 
284  The typemap object must implement typemap[name], which must return a Config subclass.
285 
286  While the typemap is shared by all instances of the field, each instance of
287  the field has its own instance of a particular sub-config type
288 
289  For example:
290 
291  class AaaConfig(Config):
292  somefield = Field(int, "...")
293  TYPEMAP = {"A", AaaConfig}
294  class MyConfig(Config):
295  choice = ConfigChoiceField("doc for choice", TYPEMAP)
296 
297  instance = MyConfig()
298  instance.choice['AAA'].somefield = 5
299  instance.choice = "AAA"
300 
301  Alternatively, the last line can be written:
302  instance.choice.name = "AAA"
303 
304  Validation of this field is performed only the "active" selection.
305  If active is None and the field is not optional, validation will fail.
306 
307  ConfigChoiceFields can allow single selections or multiple selections.
308  Single selection fields set selection through property name, and
309  multi-selection fields use the property names.
310 
311  ConfigChoiceFields also allow multiple values of the same type:
312  TYPEMAP["CCC"] = AaaConfig
313  TYPEMAP["BBB"] = AaaConfig
314 
315  When saving a config with a ConfigChoiceField, the entire set is saved, as well as the active selection
316  """
317  instanceDictClass = ConfigInstanceDict
318 
319  def __init__(self, doc, typemap, default=None, optional=False, multi=False):
320  source = getStackFrame()
321  self._setup(doc=doc, dtype=self.instanceDictClass, default=default, check=None, optional=optional,
322  source=source)
323  self.typemap = typemap
324  self.multi = multi
325 
326  def _getOrMake(self, instance, label="default"):
327  instanceDict = instance._storage.get(self.name)
328  if instanceDict is None:
329  at = getCallStack(1)
330  instanceDict = self.dtype(instance, self)
331  instanceDict.__doc__ = self.doc
332  instance._storage[self.name] = instanceDict
333  history = instance._history.setdefault(self.name, [])
334  history.append(("Initialized from defaults", at, label))
335 
336  return instanceDict
337 
338  def __get__(self, instance, owner=None):
339  if instance is None or not isinstance(instance, Config):
340  return self
341  else:
342  return self._getOrMake(instance)
343 
344  def __set__(self, instance, value, at=None, label="assignment"):
345  if instance._frozen:
346  raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
347  if at is None:
348  at = getCallStack()
349  instanceDict = self._getOrMake(instance)
350  if isinstance(value, self.instanceDictClass):
351  for k, v in value.items():
352  instanceDict.__setitem__(k, v, at=at, label=label)
353  instanceDict._setSelection(value._selection, at=at, label=label)
354 
355  else:
356  instanceDict._setSelection(value, at=at, label=label)
357 
358  def rename(self, instance):
359  instanceDict = self.__get__(instance)
360  fullname = _joinNamePath(instance._name, self.name)
361  instanceDict._rename(fullname)
362 
363  def validate(self, instance):
364  instanceDict = self.__get__(instance)
365  if instanceDict.active is None and not self.optional:
366  msg = "Required field cannot be None"
367  raise FieldValidationError(self, instance, msg)
368  elif instanceDict.active is not None:
369  if self.multi:
370  for a in instanceDict.active:
371  a.validate()
372  else:
373  instanceDict.active.validate()
374 
375  def toDict(self, instance):
376  instanceDict = self.__get__(instance)
377 
378  dict_ = {}
379  if self.multi:
380  dict_["names"] = instanceDict.names
381  else:
382  dict_["name"] = instanceDict.name
383 
384  values = {}
385  for k, v in instanceDict.items():
386  values[k] = v.toDict()
387  dict_["values"] = values
388 
389  return dict_
390 
391  def freeze(self, instance):
392  instanceDict = self.__get__(instance)
393  for v in instanceDict.values():
394  v.freeze()
395 
396  def save(self, outfile, instance):
397  instanceDict = self.__get__(instance)
398  fullname = _joinNamePath(instance._name, self.name)
399  for v in instanceDict.values():
400  v._save(outfile)
401  if self.multi:
402  outfile.write(u"{}.names={!r}\n".format(fullname, instanceDict.names))
403  else:
404  outfile.write(u"{}.name={!r}\n".format(fullname, instanceDict.name))
405 
406  def __deepcopy__(self, memo):
407  """Customize deep-copying, because we always want a reference to the original typemap.
408 
409  WARNING: this must be overridden by subclasses if they change the constructor signature!
410  """
411  other = type(self)(doc=self.doc, typemap=self.typemap, default=copy.deepcopy(self.default),
412  optional=self.optional, multi=self.multi)
413  other.source = self.source
414  return other
415 
416  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
417  """Helper function for Config.compare; used to compare two fields for equality.
418 
419  Only the selected config(s) are compared, as the parameters of any others do not matter.
420 
421  @param[in] instance1 LHS Config instance to compare.
422  @param[in] instance2 RHS Config instance to compare.
423  @param[in] shortcut If True, return as soon as an inequality is found.
424  @param[in] rtol Relative tolerance for floating point comparisons.
425  @param[in] atol Absolute tolerance for floating point comparisons.
426  @param[in] output If not None, a callable that takes a string, used (possibly repeatedly)
427  to report inequalities.
428 
429  Floating point comparisons are performed by numpy.allclose; refer to that for details.
430  """
431  d1 = getattr(instance1, self.name)
432  d2 = getattr(instance2, self.name)
433  name = getComparisonName(
434  _joinNamePath(instance1._name, self.name),
435  _joinNamePath(instance2._name, self.name)
436  )
437  if not compareScalars("selection for %s" % name, d1._selection, d2._selection, output=output):
438  return False
439  if d1._selection is None:
440  return True
441  if self.multi:
442  nested = [(k, d1[k], d2[k]) for k in d1._selection]
443  else:
444  nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
445  equal = True
446  for k, c1, c2 in nested:
447  result = compareConfigs("%s[%r]" % (name, k), c1, c2, shortcut=shortcut,
448  rtol=rtol, atol=atol, output=output)
449  if not result and shortcut:
450  return False
451  equal = equal and result
452  return equal
def __init__(self, doc, typemap, default=None, optional=False, multi=False)
def compareConfigs(name, c1, c2, shortcut=True, rtol=1E-8, atol=1E-8, output=None)
Definition: comparison.py:67
def __set__(self, instance, value, at=None, label="assignment")
def getCallStack(skip=0)
Definition: callStack.py:153
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:275
def __setitem__(self, k, value, at=None, label="assignment")
def _setup(self, doc, dtype, default, check, optional, source)
Definition: config.py:173
def getStackFrame(relative=0)
Definition: callStack.py:50
def _getOrMake(self, instance, label="default")
def __getitem__(self, k, at=None, label="default")
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:129
def compareScalars(name, v1, v2, output, rtol=1E-8, atol=1E-8, dtype=None)
Definition: comparison.py:41
def _setSelection(self, value, at=None, label="assignment")
def __init__(self, dict_, value, at=None, label="assignment", setHistory=True)
def getComparisonName(name1, name2)
Definition: comparison.py:35
def __setattr__(self, attr, value, at=None, label="assignment")