LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
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  instanceDict = self.__get__(instance)
483  for v in instanceDict.values():
484  v.freeze()
485 
486  def save(self, outfile, instance):
487  instanceDict = self.__get__(instance)
488  fullname = _joinNamePath(instance._name, self.name)
489  for v in instanceDict.values():
490  v._save(outfile)
491  if self.multi:
492  outfile.write(u"{}.names={!r}\n".format(fullname, instanceDict.names))
493  else:
494  outfile.write(u"{}.name={!r}\n".format(fullname, instanceDict.name))
495 
496  def __deepcopy__(self, memo):
497  """Customize deep-copying, because we always want a reference to the
498  original typemap.
499 
500  WARNING: this must be overridden by subclasses if they change the
501  constructor signature!
502  """
503  other = type(self)(doc=self.doc, typemap=self.typemap, default=copy.deepcopy(self.default),
504  optional=self.optional, multi=self.multi)
505  other.source = self.source
506  return other
507 
508  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
509  """Compare two fields for equality.
510 
511  Used by `lsst.pex.ConfigChoiceField.compare`.
512 
513  Parameters
514  ----------
515  instance1 : `lsst.pex.config.Config`
516  Left-hand side config instance to compare.
517  instance2 : `lsst.pex.config.Config`
518  Right-hand side config instance to compare.
519  shortcut : `bool`
520  If `True`, this function returns as soon as an inequality if found.
521  rtol : `float`
522  Relative tolerance for floating point comparisons.
523  atol : `float`
524  Absolute tolerance for floating point comparisons.
525  output : callable
526  A callable that takes a string, used (possibly repeatedly) to
527  report inequalities.
528 
529  Returns
530  -------
531  isEqual : bool
532  `True` if the fields are equal, `False` otherwise.
533 
534  Notes
535  -----
536  Only the selected configurations are compared, as the parameters of any
537  others do not matter.
538 
539  Floating point comparisons are performed by `numpy.allclose`.
540  """
541  d1 = getattr(instance1, self.name)
542  d2 = getattr(instance2, self.name)
543  name = getComparisonName(
544  _joinNamePath(instance1._name, self.name),
545  _joinNamePath(instance2._name, self.name)
546  )
547  if not compareScalars("selection for %s" % name, d1._selection, d2._selection, output=output):
548  return False
549  if d1._selection is None:
550  return True
551  if self.multi:
552  nested = [(k, d1[k], d2[k]) for k in d1._selection]
553  else:
554  nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
555  equal = True
556  for k, c1, c2 in nested:
557  result = compareConfigs("%s[%r]" % (name, k), c1, c2, shortcut=shortcut,
558  rtol=rtol, atol=atol, output=output)
559  if not result and shortcut:
560  return False
561  equal = equal and result
562  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:832
def getCallStack(skip=0)
Definition: callStack.py:169
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:453
def __setitem__(self, k, value, at=None, label="assignment")
def _setup(self, doc, dtype, default, check, optional, source)
Definition: config.py:264
def getStackFrame(relative=0)
Definition: callStack.py:52
def _getOrMake(self, instance, label="default")
table::Key< int > type
Definition: Detector.cc:164
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:833
def getComparisonName(name1, name2)
Definition: comparison.py:34
def __setattr__(self, attr, value, at=None, label="assignment")