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