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