LSSTApplications  8.0.0.0+107,8.0.0.1+13,9.1+18,9.2,master-g084aeec0a4,master-g0aced2eed8+6,master-g15627eb03c,master-g28afc54ef9,master-g3391ba5ea0,master-g3d0fb8ae5f,master-g4432ae2e89+36,master-g5c3c32f3ec+17,master-g60f1e072bb+1,master-g6a3ac32d1b,master-g76a88a4307+1,master-g7bce1f4e06+57,master-g8ff4092549+31,master-g98e65bf68e,master-ga6b77976b1+53,master-gae20e2b580+3,master-gb584cd3397+53,master-gc5448b162b+1,master-gc54cf9771d,master-gc69578ece6+1,master-gcbf758c456+22,master-gcec1da163f+63,master-gcf15f11bcc,master-gd167108223,master-gf44c96c709
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 import sys
24 
25 from .config import Config, Field, FieldValidationError, _typeStr, _joinNamePath
26 from .comparison import *
27 
28 __all__ = ["ConfigChoiceField"]
29 
30 class SelectionSet(collections.MutableSet):
31  """
32  Custom set class used to track the selection of multi-select
33  ConfigChoiceField.
34 
35  This class allows user a multi-select ConfigChoiceField to add/discard
36  items from the set of active configs. Each change to the selection is
37  tracked in the field's history.
38  """
39  def __init__(self, dict_, value, at=None, label="assignment", setHistory=True):
40  if at is None:
41  at = traceback.extract_stack()[:-1]
42  self._dict = dict_;
43  self._field = self._dict._field
44  self._config = self._dict._config
45  self.__history = self._config._history.setdefault(self._field.name, [])
46  if value is not None:
47  try:
48  for v in value:
49  if v not in self._dict:
50  #invoke __getitem__ to ensure it's present
51  r = self._dict.__getitem__(v, at=at)
52  except TypeError:
53  msg = "Value %s is of incorrect type %s. Sequence type expected"(value, _typeStr(value))
54  raise FieldValidationError(self._field, self._config, msg)
55  self._set=set(value)
56  else:
57  self._set=set()
58 
59  if setHistory:
60  self.__history.append(("Set selection to %s"%self, at, label))
61 
62  def add(self, value, at= None):
63  if self._config._frozen:
64  raise FieldValidationError(self._field, self._config,
65  "Cannot modify a frozen Config")
66 
67  if at is None:
68  at = traceback.extract_stack()[:-1]
69 
70  if value not in self._dict:
71  #invoke __getitem__ to make sure it's present
72  r = self.__getitem__(value, at=at)
73 
74  self.__history.append(("added %s to selection"%value, at, "selection"))
75  self._set.add(value)
76 
77  def discard(self, value, at=None):
78  if self._config._frozen:
79  raise FieldValidationError(self._field, self._config,
80  "Cannot modify a frozen Config")
81 
82  if value not in self._dict:
83  return
84 
85  if at is None:
86  at = traceback.extract_stack()[:-1]
87 
88  self.__history.append(("removed %s from selection"%value, at, "selection"))
89  self._set.discard(value)
90 
91  def __len__(self): return len(self._set)
92  def __iter__(self): return iter(self._set)
93  def __contains__(self, value): return value in self._set
94  def __repr__(self): return repr(list(self._set))
95  def __str__(self): return str(list(self._set))
96 
97 
98 class ConfigInstanceDict(collections.Mapping):
99  """A dict of instantiated configs, used to populate a ConfigChoiceField.
100 
101  typemap must support the following:
102  - typemap[name]: return the config class associated with the given name
103  """
104  def __init__(self, config, field):
105  collections.Mapping.__init__(self)
106  self._dict = dict()
107  self._selection = None
108  self._config = config
109  self._field = field
110  self._history = config._history.setdefault(field.name, [])
111  self.__doc__ = field.doc
112 
113  types = property(lambda x: x._field.typemap)
114 
115  def __contains__(self, k): return k in self._field.typemap
116 
117  def __len__(self): return len(self._field.typemap)
118 
119  def __iter__(self): return iter(self._field.typemap)
120 
121  def _setSelection(self,value, at=None, label="assignment"):
122  if self._config._frozen:
123  raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
124 
125  if at is None:
126  at = traceback.extract_stack()[:-2]
127 
128  if value is None:
129  self._selection=None
130  elif self._field.multi:
131  self._selection=SelectionSet(self, value, setHistory=False)
132  else:
133  if value not in self._dict:
134  r = self.__getitem__(value, at=at) # just invoke __getitem__ to make sure it's present
135  self._selection = value
136  self._history.append((value, at, label))
137 
138  def _getNames(self):
139  if not self._field.multi:
140  raise FieldValidationError(self._field, self._config,
141  "Single-selection field has no attribute 'names'")
142  return self._selection
143  def _setNames(self, value):
144  if not self._field.multi:
145  raise FieldValidationError(self._field, self._config,
146  "Single-selection field has no attribute 'names'")
147  self._setSelection(value)
148  def _delNames(self):
149  if not self._field.multi:
150  raise FieldValidationError(self._field, self._config,
151  "Single-selection field has no attribute 'names'")
152  self._selection = None
153 
154  def _getName(self):
155  if self._field.multi:
156  raise FieldValidationError(self._field, self._config,
157  "Multi-selection field has no attribute 'name'")
158  return self._selection
159  def _setName(self, value):
160  if self._field.multi:
161  raise FieldValidationError(self._field, self._config,
162  "Multi-selection field has no attribute 'name'")
163  self._setSelection(value)
164  def _delName(self):
165  if self._field.multi:
166  raise FieldValidationError(self._field, self._config,
167  "Multi-selection field has no attribute 'name'")
168  self._selection=None
169 
170  """
171  In a multi-selection ConfigInstanceDict, list of names of active items
172  Disabled In a single-selection _Regsitry)
173  """
174  names = property(_getNames, _setNames, _delNames)
175 
176  """
177  In a single-selection ConfigInstanceDict, name of the active item
178  Disabled In a multi-selection _Regsitry)
179  """
180  name = property(_getName, _setName, _delName)
181 
182  def _getActive(self):
183  if self._selection is None:
184  return None
185 
186  if self._field.multi:
187  return [self[c] for c in self._selection]
188  else:
189  return self[self._selection]
190 
191  """
192  Readonly shortcut to access the selected item(s)
193  for multi-selection, this is equivalent to: [self[name] for name in self.names]
194  for single-selection, this is equivalent to: self[name]
195  """
196  active = property(_getActive)
197 
198  def __getitem__(self, k, at=None, label="default"):
199  try:
200  value = self._dict[k]
201  except KeyError:
202  try:
203  dtype = self._field.typemap[k]
204  except:
205  raise FieldValidationError(self._field, self._config, "Unknown key %r"%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", "_selection", "__doc__"]:
249  # This allows specific private attributes to work.
250  object.__setattr__(self, attr, value)
251  else:
252  # We throw everything else.
253  msg = "%s has no attribute %s"%(_typeStr(self._field), attr)
254  raise FieldValidationError(self._field, self._config, msg)
255 
256 
257 
258 
259 class ConfigChoiceField(Field):
260  """
261  ConfigChoiceFields allow the config to choose from a set of possible Config types.
262  The set of allowable types is given by the typemap argument to the constructor
263 
264  The typemap object must implement typemap[name], which must return a Config subclass.
265 
266  While the typemap is shared by all instances of the field, each instance of
267  the field has its own instance of a particular sub-config type
268 
269  For example:
270 
271  class AaaConfig(Config):
272  somefield = Field(int, "...")
273  TYPEMAP = {"A", AaaConfig}
274  class MyConfig(Config):
275  choice = ConfigChoiceField("doc for choice", TYPEMAP)
276 
277  instance = MyConfig()
278  instance.choice['AAA'].somefield = 5
279  instance.choice = "AAA"
280 
281  Alternatively, the last line can be written:
282  instance.choice.name = "AAA"
283 
284  Validation of this field is performed only the "active" selection.
285  If active is None and the field is not optional, validation will fail.
286 
287  ConfigChoiceFields can allow single selections or multiple selections.
288  Single selection fields set selection through property name, and
289  multi-selection fields use the property names.
290 
291  ConfigChoiceFields also allow multiple values of the same type:
292  TYPEMAP["CCC"] = AaaConfig
293  TYPEMAP["BBB"] = AaaConfig
294 
295  When saving a config with a ConfigChoiceField, the entire set is saved, as well as the active selection
296  """
297  instanceDictClass = ConfigInstanceDict
298  def __init__(self, doc, typemap, default=None, optional=False, multi=False):
299  source = traceback.extract_stack(limit=2)[0]
300  self._setup( doc=doc, dtype=self.instanceDictClass, default=default, check=None, optional=optional, source=source)
301  self.typemap = typemap
302  self.multi = multi
303 
304  def _getOrMake(self, instance, label="default"):
305  instanceDict = instance._storage.get(self.name)
306  if instanceDict is None:
307  at = traceback.extract_stack()[:-2]
308  name = _joinNamePath(instance._name, self.name)
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