LSSTApplications  18.1.0
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 
23 __all__ = ["ConfigChoiceField"]
24 
25 import copy
26 import collections.abc
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.abc.MutableSet):
34  """A mutable set class that tracks the selection of multi-select
35  `~lsst.pex.config.ConfigChoiceField` objects.
36 
37  Parameters
38  ----------
39  dict_ : `ConfigInstanceDict`
40  The dictionary of instantiated configs.
41  value
42  The selected key.
43  at : `lsst.pex.config.callStack.StackFrame`, optional
44  The call stack when the selection was made.
45  label : `str`, optional
46  Label for history tracking.
47  setHistory : `bool`, optional
48  Add this even to the history, if `True`.
49 
50  Notes
51  -----
52  This class allows a user of a multi-select
53  `~lsst.pex.config.ConfigChoiceField` to add or discard items from the set
54  of active configs. Each change to the selection is tracked in the field's
55  history.
56  """
57 
58  def __init__(self, dict_, value, at=None, label="assignment", setHistory=True):
59  if at is None:
60  at = getCallStack()
61  self._dict = dict_
62  self._field = self._dict._field
63  self._config = self._dict._config
64  self.__history = self._config._history.setdefault(self._field.name, [])
65  if value is not None:
66  try:
67  for v in value:
68  if v not in self._dict:
69  # invoke __getitem__ to ensure it's present
70  self._dict.__getitem__(v, at=at)
71  except TypeError:
72  msg = "Value %s is of incorrect type %s. Sequence type expected"(value, _typeStr(value))
73  raise FieldValidationError(self._field, self._config, msg)
74  self._set = set(value)
75  else:
76  self._set = set()
77 
78  if setHistory:
79  self.__history.append(("Set selection to %s" % self, at, label))
80 
81  def add(self, value, at=None):
82  """Add a value to the selected set.
83  """
84  if self._config._frozen:
85  raise FieldValidationError(self._field, self._config,
86  "Cannot modify a frozen Config")
87 
88  if at is None:
89  at = getCallStack()
90 
91  if value not in self._dict:
92  # invoke __getitem__ to make sure it's present
93  self._dict.__getitem__(value, at=at)
94 
95  self.__history.append(("added %s to selection" % value, at, "selection"))
96  self._set.add(value)
97 
98  def discard(self, value, at=None):
99  """Discard a value from the selected set.
100  """
101  if self._config._frozen:
102  raise FieldValidationError(self._field, self._config,
103  "Cannot modify a frozen Config")
104 
105  if value not in self._dict:
106  return
107 
108  if at is None:
109  at = getCallStack()
110 
111  self.__history.append(("removed %s from selection" % value, at, "selection"))
112  self._set.discard(value)
113 
114  def __len__(self):
115  return len(self._set)
116 
117  def __iter__(self):
118  return iter(self._set)
119 
120  def __contains__(self, value):
121  return value in self._set
122 
123  def __repr__(self):
124  return repr(list(self._set))
125 
126  def __str__(self):
127  return str(list(self._set))
128 
129 
130 class ConfigInstanceDict(collections.abc.Mapping):
131  """Dictionary of instantiated configs, used to populate a
132  `~lsst.pex.config.ConfigChoiceField`.
133 
134  Parameters
135  ----------
136  config : `lsst.pex.config.Config`
137  A configuration instance.
138  field : `lsst.pex.config.Field`-type
139  A configuration field. Note that the `lsst.pex.config.Field.fieldmap`
140  attribute must provide key-based access to configuration classes,
141  (that is, ``typemap[name]``).
142  """
143  def __init__(self, config, field):
144  collections.abc.Mapping.__init__(self)
145  self._dict = dict()
146  self._selection = None
147  self._config = config
148  self._field = field
149  self._history = config._history.setdefault(field.name, [])
150  self.__doc__ = field.doc
151 
152  types = property(lambda x: x._field.typemap)
153 
154  def __contains__(self, k):
155  return k in self._field.typemap
156 
157  def __len__(self):
158  return len(self._field.typemap)
159 
160  def __iter__(self):
161  return iter(self._field.typemap)
162 
163  def _setSelection(self, value, at=None, label="assignment"):
164  if self._config._frozen:
165  raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
166 
167  if at is None:
168  at = getCallStack(1)
169 
170  if value is None:
171  self._selection = None
172  elif self._field.multi:
173  self._selection = SelectionSet(self, value, setHistory=False)
174  else:
175  if value not in self._dict:
176  self.__getitem__(value, at=at) # just invoke __getitem__ to make sure it's present
177  self._selection = value
178  self._history.append((value, at, label))
179 
180  def _getNames(self):
181  if not self._field.multi:
182  raise FieldValidationError(self._field, self._config,
183  "Single-selection field has no attribute 'names'")
184  return self._selection
185 
186  def _setNames(self, value):
187  if not self._field.multi:
188  raise FieldValidationError(self._field, self._config,
189  "Single-selection field has no attribute 'names'")
190  self._setSelection(value)
191 
192  def _delNames(self):
193  if not self._field.multi:
194  raise FieldValidationError(self._field, self._config,
195  "Single-selection field has no attribute 'names'")
196  self._selection = None
197 
198  def _getName(self):
199  if self._field.multi:
200  raise FieldValidationError(self._field, self._config,
201  "Multi-selection field has no attribute 'name'")
202  return self._selection
203 
204  def _setName(self, value):
205  if self._field.multi:
206  raise FieldValidationError(self._field, self._config,
207  "Multi-selection field has no attribute 'name'")
208  self._setSelection(value)
209 
210  def _delName(self):
211  if self._field.multi:
212  raise FieldValidationError(self._field, self._config,
213  "Multi-selection field has no attribute 'name'")
214  self._selection = None
215 
216  names = property(_getNames, _setNames, _delNames)
217  """List of names of active items in a multi-selection
218  ``ConfigInstanceDict``. Disabled in a single-selection ``_Registry``; use
219  the `name` attribute instead.
220  """
221 
222  name = property(_getName, _setName, _delName)
223  """Name of the active item in a single-selection ``ConfigInstanceDict``.
224  Disabled in a multi-selection ``_Registry``; use the ``names`` attribute
225  instead.
226  """
227 
228  def _getActive(self):
229  if self._selection is None:
230  return None
231 
232  if self._field.multi:
233  return [self[c] for c in self._selection]
234  else:
235  return self[self._selection]
236 
237  active = property(_getActive)
238  """The selected items.
239 
240  For multi-selection, this is equivalent to: ``[self[name] for name in
241  self.names]``. For single-selection, this is equivalent to: ``self[name]``.
242  """
243 
244  def __getitem__(self, k, at=None, label="default"):
245  try:
246  value = self._dict[k]
247  except KeyError:
248  try:
249  dtype = self._field.typemap[k]
250  except Exception:
251  raise FieldValidationError(self._field, self._config,
252  "Unknown key %r in Registry/ConfigChoiceField" % k)
253  name = _joinNamePath(self._config._name, self._field.name, k)
254  if at is None:
255  at = getCallStack()
256  at.insert(0, dtype._source)
257  value = self._dict.setdefault(k, dtype(__name=name, __at=at, __label=label))
258  return value
259 
260  def __setitem__(self, k, value, at=None, label="assignment"):
261  if self._config._frozen:
262  raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
263 
264  try:
265  dtype = self._field.typemap[k]
266  except Exception:
267  raise FieldValidationError(self._field, self._config, "Unknown key %r" % k)
268 
269  if value != dtype and type(value) != dtype:
270  msg = "Value %s at key %k is of incorrect type %s. Expected type %s" % \
271  (value, k, _typeStr(value), _typeStr(dtype))
272  raise FieldValidationError(self._field, self._config, msg)
273 
274  if at is None:
275  at = getCallStack()
276  name = _joinNamePath(self._config._name, self._field.name, k)
277  oldValue = self._dict.get(k, None)
278  if oldValue is None:
279  if value == dtype:
280  self._dict[k] = value(__name=name, __at=at, __label=label)
281  else:
282  self._dict[k] = dtype(__name=name, __at=at, __label=label, **value._storage)
283  else:
284  if value == dtype:
285  value = value()
286  oldValue.update(__at=at, __label=label, **value._storage)
287 
288  def _rename(self, fullname):
289  for k, v in self._dict.items():
290  v._rename(_joinNamePath(name=fullname, index=k))
291 
292  def __setattr__(self, attr, value, at=None, label="assignment"):
293  if hasattr(getattr(self.__class__, attr, None), '__set__'):
294  # This allows properties to work.
295  object.__setattr__(self, attr, value)
296  elif attr in self.__dict__ or attr in ["_history", "_field", "_config", "_dict",
297  "_selection", "__doc__"]:
298  # This allows specific private attributes to work.
299  object.__setattr__(self, attr, value)
300  else:
301  # We throw everything else.
302  msg = "%s has no attribute %s" % (_typeStr(self._field), attr)
303  raise FieldValidationError(self._field, self._config, msg)
304 
305 
307  """A configuration field (`~lsst.pex.config.Field` subclass) that allows a
308  user to choose from a set of `~lsst.pex.config.Config` types.
309 
310  Parameters
311  ----------
312  doc : `str`
313  Documentation string for the field.
314  typemap : `dict`-like
315  A mapping between keys and `~lsst.pex.config.Config`-types as values.
316  See *Examples* for details.
317  default : `str`, optional
318  The default configuration name.
319  optional : `bool`, optional
320  When `False`, `lsst.pex.config.Config.validate` will fail if the
321  field's value is `None`.
322  multi : `bool`, optional
323  If `True`, the field allows multiple selections. In this case, set the
324  selections by assigning a sequence to the ``names`` attribute of the
325  field.
326 
327  If `False`, the field allows only a single selection. In this case,
328  set the active config by assigning the config's key from the
329  ``typemap`` to the field's ``name`` attribute (see *Examples*).
330 
331  See also
332  --------
333  ChoiceField
334  ConfigDictField
335  ConfigField
336  ConfigurableField
337  DictField
338  Field
339  ListField
340  RangeField
341  RegistryField
342 
343  Notes
344  -----
345  ``ConfigChoiceField`` instances can allow either single selections or
346  multiple selections, depending on the ``multi`` parameter. For
347  single-selection fields, set the selection with the ``name`` attribute.
348  For multi-selection fields, set the selection though the ``names``
349  attribute.
350 
351  This field is validated only against the active selection. If the
352  ``active`` attribute is `None` and the field is not optional, validation
353  will fail.
354 
355  When saving a configuration with a ``ConfigChoiceField``, the entire set is
356  saved, as well as the active selection.
357 
358  Examples
359  --------
360  While the ``typemap`` is shared by all instances of the field, each
361  instance of the field has its own instance of a particular sub-config type.
362 
363  For example, ``AaaConfig`` is a config object
364 
365  >>> from lsst.pex.config import Config, ConfigChoiceField, Field
366  >>> class AaaConfig(Config):
367  ... somefield = Field("doc", int)
368  ...
369 
370  The ``MyConfig`` config has a ``ConfigChoiceField`` field called ``choice``
371  that maps the ``AaaConfig`` type to the ``"AAA"`` key:
372 
373  >>> TYPEMAP = {"AAA", AaaConfig}
374  >>> class MyConfig(Config):
375  ... choice = ConfigChoiceField("doc for choice", TYPEMAP)
376  ...
377 
378  Creating an instance of ``MyConfig``:
379 
380  >>> instance = MyConfig()
381 
382  Setting value of the field ``somefield`` on the "AAA" key of the ``choice``
383  field:
384 
385  >>> instance.choice['AAA'].somefield = 5
386 
387  **Selecting the active configuration**
388 
389  Make the ``"AAA"`` key the active configuration value for the ``choice``
390  field:
391 
392  >>> instance.choice = "AAA"
393 
394  Alternatively, the last line can be written:
395 
396  >>> instance.choice.name = "AAA"
397 
398  (If the config instance allows multiple selections, you'd assign a sequence
399  to the ``names`` attribute instead.)
400 
401  ``ConfigChoiceField`` instances also allow multiple values of the same type:
402 
403  >>> TYPEMAP["CCC"] = AaaConfig
404  >>> TYPEMAP["BBB"] = AaaConfig
405  """
406 
407  instanceDictClass = ConfigInstanceDict
408 
409  def __init__(self, doc, typemap, default=None, optional=False, multi=False):
410  source = getStackFrame()
411  self._setup(doc=doc, dtype=self.instanceDictClass, default=default, check=None, optional=optional,
412  source=source)
413  self.typemap = typemap
414  self.multi = multi
415 
416  def _getOrMake(self, instance, label="default"):
417  instanceDict = instance._storage.get(self.name)
418  if instanceDict is None:
419  at = getCallStack(1)
420  instanceDict = self.dtype(instance, self)
421  instanceDict.__doc__ = self.doc
422  instance._storage[self.name] = instanceDict
423  history = instance._history.setdefault(self.name, [])
424  history.append(("Initialized from defaults", at, label))
425 
426  return instanceDict
427 
428  def __get__(self, instance, owner=None):
429  if instance is None or not isinstance(instance, Config):
430  return self
431  else:
432  return self._getOrMake(instance)
433 
434  def __set__(self, instance, value, at=None, label="assignment"):
435  if instance._frozen:
436  raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
437  if at is None:
438  at = getCallStack()
439  instanceDict = self._getOrMake(instance)
440  if isinstance(value, self.instanceDictClass):
441  for k, v in value.items():
442  instanceDict.__setitem__(k, v, at=at, label=label)
443  instanceDict._setSelection(value._selection, at=at, label=label)
444 
445  else:
446  instanceDict._setSelection(value, at=at, label=label)
447 
448  def rename(self, instance):
449  instanceDict = self.__get__(instance)
450  fullname = _joinNamePath(instance._name, self.name)
451  instanceDict._rename(fullname)
452 
453  def validate(self, instance):
454  instanceDict = self.__get__(instance)
455  if instanceDict.active is None and not self.optional:
456  msg = "Required field cannot be None"
457  raise FieldValidationError(self, instance, msg)
458  elif instanceDict.active is not None:
459  if self.multi:
460  for a in instanceDict.active:
461  a.validate()
462  else:
463  instanceDict.active.validate()
464 
465  def toDict(self, instance):
466  instanceDict = self.__get__(instance)
467 
468  dict_ = {}
469  if self.multi:
470  dict_["names"] = instanceDict.names
471  else:
472  dict_["name"] = instanceDict.name
473 
474  values = {}
475  for k, v in instanceDict.items():
476  values[k] = v.toDict()
477  dict_["values"] = values
478 
479  return dict_
480 
481  def freeze(self, instance):
482  # When a config is frozen it should not be affected by anything further
483  # being added to a registry, so create a deep copy of the registry
484  # typemap
485  self.typemap = copy.deepcopy(self.typemap)
486  instanceDict = self.__get__(instance)
487  for v in instanceDict.values():
488  v.freeze()
489 
490  def _collectImports(self, instance, imports):
491  instanceDict = self.__get__(instance)
492  for config in instanceDict.values():
493  config._collectImports()
494  imports |= config._imports
495 
496  def save(self, outfile, instance):
497  instanceDict = self.__get__(instance)
498  fullname = _joinNamePath(instance._name, self.name)
499  for v in instanceDict.values():
500  v._save(outfile)
501  if self.multi:
502  outfile.write(u"{}.names={!r}\n".format(fullname, instanceDict.names))
503  else:
504  outfile.write(u"{}.name={!r}\n".format(fullname, instanceDict.name))
505 
506  def __deepcopy__(self, memo):
507  """Customize deep-copying, because we always want a reference to the
508  original typemap.
509 
510  WARNING: this must be overridden by subclasses if they change the
511  constructor signature!
512  """
513  other = type(self)(doc=self.doc, typemap=self.typemap, default=copy.deepcopy(self.default),
514  optional=self.optional, multi=self.multi)
515  other.source = self.source
516  return other
517 
518  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
519  """Compare two fields for equality.
520 
521  Used by `lsst.pex.ConfigChoiceField.compare`.
522 
523  Parameters
524  ----------
525  instance1 : `lsst.pex.config.Config`
526  Left-hand side config instance to compare.
527  instance2 : `lsst.pex.config.Config`
528  Right-hand side config instance to compare.
529  shortcut : `bool`
530  If `True`, this function returns as soon as an inequality if found.
531  rtol : `float`
532  Relative tolerance for floating point comparisons.
533  atol : `float`
534  Absolute tolerance for floating point comparisons.
535  output : callable
536  A callable that takes a string, used (possibly repeatedly) to
537  report inequalities.
538 
539  Returns
540  -------
541  isEqual : bool
542  `True` if the fields are equal, `False` otherwise.
543 
544  Notes
545  -----
546  Only the selected configurations are compared, as the parameters of any
547  others do not matter.
548 
549  Floating point comparisons are performed by `numpy.allclose`.
550  """
551  d1 = getattr(instance1, self.name)
552  d2 = getattr(instance2, self.name)
553  name = getComparisonName(
554  _joinNamePath(instance1._name, self.name),
555  _joinNamePath(instance2._name, self.name)
556  )
557  if not compareScalars("selection for %s" % name, d1._selection, d2._selection, output=output):
558  return False
559  if d1._selection is None:
560  return True
561  if self.multi:
562  nested = [(k, d1[k], d2[k]) for k in d1._selection]
563  else:
564  nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
565  equal = True
566  for k, c1, c2 in nested:
567  result = compareConfigs("%s[%r]" % (name, k), c1, c2, shortcut=shortcut,
568  rtol=rtol, atol=atol, output=output)
569  if not result and shortcut:
570  return False
571  equal = equal and result
572  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:105
def __set__(self, instance, value, at=None, label="assignment")
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
daf::base::PropertySet * set
Definition: fits.cc:884
def getCallStack(skip=0)
Definition: callStack.py:169
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:476
def __setitem__(self, k, value, at=None, label="assignment")
def _setup(self, doc, dtype, default, check, optional, source)
Definition: config.py:273
def getStackFrame(relative=0)
Definition: callStack.py:52
def _getOrMake(self, instance, label="default")
table::Key< int > type
Definition: Detector.cc:167
def __getitem__(self, k, at=None, label="default")
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:168
def compareScalars(name, v1, v2, output, rtol=1E-8, atol=1E-8, dtype=None)
Definition: comparison.py:56
std::vector< SchemaItem< Flag > > * items
def _setSelection(self, value, at=None, label="assignment")
def __init__(self, dict_, value, at=None, label="assignment", setHistory=True)
daf::base::PropertyList * list
Definition: fits.cc:885
def getComparisonName(name1, name2)
Definition: comparison.py:34
def __setattr__(self, attr, value, at=None, label="assignment")