lsst.pex.config  14.0-4-gae1598d+3
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 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 __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 = getCallStack()
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 = getCallStack()
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 = getCallStack()
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 = getCallStack(1)
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 Exception:
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 = getCallStack()
231  at.insert(0, dtype._source)
232  value = self._dict.setdefault(k, dtype(__name=name, __at=at, __label=label))
233  return value
234 
235  def __setitem__(self, k, value, at=None, label="assignment"):
236  if self._config._frozen:
237  raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
238 
239  try:
240  dtype = self._field.typemap[k]
241  except Exception:
242  raise FieldValidationError(self._field, self._config, "Unknown key %r" % k)
243 
244  if value != dtype and type(value) != dtype:
245  msg = "Value %s at key %k is of incorrect type %s. Expected type %s" % \
246  (value, k, _typeStr(value), _typeStr(dtype))
247  raise FieldValidationError(self._field, self._config, msg)
248 
249  if at is None:
250  at = getCallStack()
251  name = _joinNamePath(self._config._name, self._field.name, k)
252  oldValue = self._dict.get(k, None)
253  if oldValue is None:
254  if value == dtype:
255  self._dict[k] = value(__name=name, __at=at, __label=label)
256  else:
257  self._dict[k] = dtype(__name=name, __at=at, __label=label, **value._storage)
258  else:
259  if value == dtype:
260  value = value()
261  oldValue.update(__at=at, __label=label, **value._storage)
262 
263  def _rename(self, fullname):
264  for k, v in self._dict.items():
265  v._rename(_joinNamePath(name=fullname, index=k))
266 
267  def __setattr__(self, attr, value, at=None, label="assignment"):
268  if hasattr(getattr(self.__class__, attr, None), '__set__'):
269  # This allows properties to work.
270  object.__setattr__(self, attr, value)
271  elif attr in self.__dict__ or attr in ["_history", "_field", "_config", "_dict",
272  "_selection", "__doc__"]:
273  # This allows specific private attributes to work.
274  object.__setattr__(self, attr, value)
275  else:
276  # We throw everything else.
277  msg = "%s has no attribute %s" % (_typeStr(self._field), attr)
278  raise FieldValidationError(self._field, self._config, msg)
279 
280 
282  """
283  ConfigChoiceFields allow the config to choose from a set of possible Config types.
284  The set of allowable types is given by the typemap argument to the constructor
285 
286  The typemap object must implement typemap[name], which must return a Config subclass.
287 
288  While the typemap is shared by all instances of the field, each instance of
289  the field has its own instance of a particular sub-config type
290 
291  For example:
292 
293  class AaaConfig(Config):
294  somefield = Field(int, "...")
295  TYPEMAP = {"A", AaaConfig}
296  class MyConfig(Config):
297  choice = ConfigChoiceField("doc for choice", TYPEMAP)
298 
299  instance = MyConfig()
300  instance.choice['AAA'].somefield = 5
301  instance.choice = "AAA"
302 
303  Alternatively, the last line can be written:
304  instance.choice.name = "AAA"
305 
306  Validation of this field is performed only the "active" selection.
307  If active is None and the field is not optional, validation will fail.
308 
309  ConfigChoiceFields can allow single selections or multiple selections.
310  Single selection fields set selection through property name, and
311  multi-selection fields use the property names.
312 
313  ConfigChoiceFields also allow multiple values of the same type:
314  TYPEMAP["CCC"] = AaaConfig
315  TYPEMAP["BBB"] = AaaConfig
316 
317  When saving a config with a ConfigChoiceField, the entire set is saved, as well as the active selection
318  """
319  instanceDictClass = ConfigInstanceDict
320 
321  def __init__(self, doc, typemap, default=None, optional=False, multi=False):
322  source = getStackFrame()
323  self._setup(doc=doc, dtype=self.instanceDictClass, default=default, check=None, optional=optional,
324  source=source)
325  self.typemap = typemap
326  self.multi = multi
327 
328  def _getOrMake(self, instance, label="default"):
329  instanceDict = instance._storage.get(self.name)
330  if instanceDict is None:
331  at = getCallStack(1)
332  instanceDict = self.dtype(instance, self)
333  instanceDict.__doc__ = self.doc
334  instance._storage[self.name] = instanceDict
335  history = instance._history.setdefault(self.name, [])
336  history.append(("Initialized from defaults", at, label))
337 
338  return instanceDict
339 
340  def __get__(self, instance, owner=None):
341  if instance is None or not isinstance(instance, Config):
342  return self
343  else:
344  return self._getOrMake(instance)
345 
346  def __set__(self, instance, value, at=None, label="assignment"):
347  if instance._frozen:
348  raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
349  if at is None:
350  at = getCallStack()
351  instanceDict = self._getOrMake(instance)
352  if isinstance(value, self.instanceDictClass):
353  for k, v in value.items():
354  instanceDict.__setitem__(k, v, at=at, label=label)
355  instanceDict._setSelection(value._selection, at=at, label=label)
356 
357  else:
358  instanceDict._setSelection(value, at=at, label=label)
359 
360  def rename(self, instance):
361  instanceDict = self.__get__(instance)
362  fullname = _joinNamePath(instance._name, self.name)
363  instanceDict._rename(fullname)
364 
365  def validate(self, instance):
366  instanceDict = self.__get__(instance)
367  if instanceDict.active is None and not self.optional:
368  msg = "Required field cannot be None"
369  raise FieldValidationError(self, instance, msg)
370  elif instanceDict.active is not None:
371  if self.multi:
372  for a in instanceDict.active:
373  a.validate()
374  else:
375  instanceDict.active.validate()
376 
377  def toDict(self, instance):
378  instanceDict = self.__get__(instance)
379 
380  dict_ = {}
381  if self.multi:
382  dict_["names"] = instanceDict.names
383  else:
384  dict_["name"] = instanceDict.name
385 
386  values = {}
387  for k, v in instanceDict.items():
388  values[k] = v.toDict()
389  dict_["values"] = values
390 
391  return dict_
392 
393  def freeze(self, instance):
394  instanceDict = self.__get__(instance)
395  for v in instanceDict.values():
396  v.freeze()
397 
398  def save(self, outfile, instance):
399  instanceDict = self.__get__(instance)
400  fullname = _joinNamePath(instance._name, self.name)
401  for v in instanceDict.values():
402  v._save(outfile)
403  if self.multi:
404  outfile.write(u"{}.names={!r}\n".format(fullname, instanceDict.names))
405  else:
406  outfile.write(u"{}.name={!r}\n".format(fullname, instanceDict.name))
407 
408  def __deepcopy__(self, memo):
409  """Customize deep-copying, because we always want a reference to the original typemap.
410 
411  WARNING: this must be overridden by subclasses if they change the constructor signature!
412  """
413  other = type(self)(doc=self.doc, typemap=self.typemap, default=copy.deepcopy(self.default),
414  optional=self.optional, multi=self.multi)
415  other.source = self.source
416  return other
417 
418  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
419  """Helper function for Config.compare; used to compare two fields for equality.
420 
421  Only the selected config(s) are compared, as the parameters of any others do not matter.
422 
423  @param[in] instance1 LHS Config instance to compare.
424  @param[in] instance2 RHS Config instance to compare.
425  @param[in] shortcut If True, return as soon as an inequality is found.
426  @param[in] rtol Relative tolerance for floating point comparisons.
427  @param[in] atol Absolute tolerance for floating point comparisons.
428  @param[in] output If not None, a callable that takes a string, used (possibly repeatedly)
429  to report inequalities.
430 
431  Floating point comparisons are performed by numpy.allclose; refer to that for details.
432  """
433  d1 = getattr(instance1, self.name)
434  d2 = getattr(instance2, self.name)
435  name = getComparisonName(
436  _joinNamePath(instance1._name, self.name),
437  _joinNamePath(instance2._name, self.name)
438  )
439  if not compareScalars("selection for %s" % name, d1._selection, d2._selection, output=output):
440  return False
441  if d1._selection is None:
442  return True
443  if self.multi:
444  nested = [(k, d1[k], d2[k]) for k in d1._selection]
445  else:
446  nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
447  equal = True
448  for k, c1, c2 in nested:
449  result = compareConfigs("%s[%r]" % (name, k), c1, c2, shortcut=shortcut,
450  rtol=rtol, atol=atol, output=output)
451  if not result and shortcut:
452  return False
453  equal = equal and result
454  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:157
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:287
def __setitem__(self, k, value, at=None, label="assignment")
def _setup(self, doc, dtype, default, check, optional, source)
Definition: config.py:188
def getStackFrame(relative=0)
Definition: callStack.py:54
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:134
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")