lsst.pex.config  18.1.0-1-gc037db8+2
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  deprecated : None or `str`, optional
331  A description of why this Field is deprecated, including removal date.
332  If not None, the string is appended to the docstring for this Field.
333 
334  See also
335  --------
336  ChoiceField
337  ConfigDictField
338  ConfigField
339  ConfigurableField
340  DictField
341  Field
342  ListField
343  RangeField
344  RegistryField
345 
346  Notes
347  -----
348  ``ConfigChoiceField`` instances can allow either single selections or
349  multiple selections, depending on the ``multi`` parameter. For
350  single-selection fields, set the selection with the ``name`` attribute.
351  For multi-selection fields, set the selection though the ``names``
352  attribute.
353 
354  This field is validated only against the active selection. If the
355  ``active`` attribute is `None` and the field is not optional, validation
356  will fail.
357 
358  When saving a configuration with a ``ConfigChoiceField``, the entire set is
359  saved, as well as the active selection.
360 
361  Examples
362  --------
363  While the ``typemap`` is shared by all instances of the field, each
364  instance of the field has its own instance of a particular sub-config type.
365 
366  For example, ``AaaConfig`` is a config object
367 
368  >>> from lsst.pex.config import Config, ConfigChoiceField, Field
369  >>> class AaaConfig(Config):
370  ... somefield = Field("doc", int)
371  ...
372 
373  The ``MyConfig`` config has a ``ConfigChoiceField`` field called ``choice``
374  that maps the ``AaaConfig`` type to the ``"AAA"`` key:
375 
376  >>> TYPEMAP = {"AAA", AaaConfig}
377  >>> class MyConfig(Config):
378  ... choice = ConfigChoiceField("doc for choice", TYPEMAP)
379  ...
380 
381  Creating an instance of ``MyConfig``:
382 
383  >>> instance = MyConfig()
384 
385  Setting value of the field ``somefield`` on the "AAA" key of the ``choice``
386  field:
387 
388  >>> instance.choice['AAA'].somefield = 5
389 
390  **Selecting the active configuration**
391 
392  Make the ``"AAA"`` key the active configuration value for the ``choice``
393  field:
394 
395  >>> instance.choice = "AAA"
396 
397  Alternatively, the last line can be written:
398 
399  >>> instance.choice.name = "AAA"
400 
401  (If the config instance allows multiple selections, you'd assign a sequence
402  to the ``names`` attribute instead.)
403 
404  ``ConfigChoiceField`` instances also allow multiple values of the same type:
405 
406  >>> TYPEMAP["CCC"] = AaaConfig
407  >>> TYPEMAP["BBB"] = AaaConfig
408  """
409 
410  instanceDictClass = ConfigInstanceDict
411 
412  def __init__(self, doc, typemap, default=None, optional=False, multi=False, deprecated=None):
413  source = getStackFrame()
414  self._setup(doc=doc, dtype=self.instanceDictClass, default=default, check=None, optional=optional,
415  source=source, deprecated=deprecated)
416  self.typemap = typemap
417  self.multi = multi
418 
419  def _getOrMake(self, instance, label="default"):
420  instanceDict = instance._storage.get(self.name)
421  if instanceDict is None:
422  at = getCallStack(1)
423  instanceDict = self.dtype(instance, self)
424  instanceDict.__doc__ = self.doc
425  instance._storage[self.name] = instanceDict
426  history = instance._history.setdefault(self.name, [])
427  history.append(("Initialized from defaults", at, label))
428 
429  return instanceDict
430 
431  def __get__(self, instance, owner=None):
432  if instance is None or not isinstance(instance, Config):
433  return self
434  else:
435  return self._getOrMake(instance)
436 
437  def __set__(self, instance, value, at=None, label="assignment"):
438  if instance._frozen:
439  raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
440  if at is None:
441  at = getCallStack()
442  instanceDict = self._getOrMake(instance)
443  if isinstance(value, self.instanceDictClass):
444  for k, v in value.items():
445  instanceDict.__setitem__(k, v, at=at, label=label)
446  instanceDict._setSelection(value._selection, at=at, label=label)
447 
448  else:
449  instanceDict._setSelection(value, at=at, label=label)
450 
451  def rename(self, instance):
452  instanceDict = self.__get__(instance)
453  fullname = _joinNamePath(instance._name, self.name)
454  instanceDict._rename(fullname)
455 
456  def validate(self, instance):
457  instanceDict = self.__get__(instance)
458  if instanceDict.active is None and not self.optional:
459  msg = "Required field cannot be None"
460  raise FieldValidationError(self, instance, msg)
461  elif instanceDict.active is not None:
462  if self.multi:
463  for a in instanceDict.active:
464  a.validate()
465  else:
466  instanceDict.active.validate()
467 
468  def toDict(self, instance):
469  instanceDict = self.__get__(instance)
470 
471  dict_ = {}
472  if self.multi:
473  dict_["names"] = instanceDict.names
474  else:
475  dict_["name"] = instanceDict.name
476 
477  values = {}
478  for k, v in instanceDict.items():
479  values[k] = v.toDict()
480  dict_["values"] = values
481 
482  return dict_
483 
484  def freeze(self, instance):
485  # When a config is frozen it should not be affected by anything further
486  # being added to a registry, so create a deep copy of the registry
487  # typemap
488  self.typemap = copy.deepcopy(self.typemap)
489  instanceDict = self.__get__(instance)
490  for v in instanceDict.values():
491  v.freeze()
492 
493  def _collectImports(self, instance, imports):
494  instanceDict = self.__get__(instance)
495  for config in instanceDict.values():
496  config._collectImports()
497  imports |= config._imports
498 
499  def save(self, outfile, instance):
500  instanceDict = self.__get__(instance)
501  fullname = _joinNamePath(instance._name, self.name)
502  for v in instanceDict.values():
503  v._save(outfile)
504  if self.multi:
505  outfile.write(u"{}.names={!r}\n".format(fullname, instanceDict.names))
506  else:
507  outfile.write(u"{}.name={!r}\n".format(fullname, instanceDict.name))
508 
509  def __deepcopy__(self, memo):
510  """Customize deep-copying, because we always want a reference to the
511  original typemap.
512 
513  WARNING: this must be overridden by subclasses if they change the
514  constructor signature!
515  """
516  other = type(self)(doc=self.doc, typemap=self.typemap, default=copy.deepcopy(self.default),
517  optional=self.optional, multi=self.multi)
518  other.source = self.source
519  return other
520 
521  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
522  """Compare two fields for equality.
523 
524  Used by `lsst.pex.ConfigChoiceField.compare`.
525 
526  Parameters
527  ----------
528  instance1 : `lsst.pex.config.Config`
529  Left-hand side config instance to compare.
530  instance2 : `lsst.pex.config.Config`
531  Right-hand side config instance to compare.
532  shortcut : `bool`
533  If `True`, this function returns as soon as an inequality if found.
534  rtol : `float`
535  Relative tolerance for floating point comparisons.
536  atol : `float`
537  Absolute tolerance for floating point comparisons.
538  output : callable
539  A callable that takes a string, used (possibly repeatedly) to
540  report inequalities.
541 
542  Returns
543  -------
544  isEqual : bool
545  `True` if the fields are equal, `False` otherwise.
546 
547  Notes
548  -----
549  Only the selected configurations are compared, as the parameters of any
550  others do not matter.
551 
552  Floating point comparisons are performed by `numpy.allclose`.
553  """
554  d1 = getattr(instance1, self.name)
555  d2 = getattr(instance2, self.name)
556  name = getComparisonName(
557  _joinNamePath(instance1._name, self.name),
558  _joinNamePath(instance2._name, self.name)
559  )
560  if not compareScalars("selection for %s" % name, d1._selection, d2._selection, output=output):
561  return False
562  if d1._selection is None:
563  return True
564  if self.multi:
565  nested = [(k, d1[k], d2[k]) for k in d1._selection]
566  else:
567  nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
568  equal = True
569  for k, c1, c2 in nested:
570  result = compareConfigs("%s[%r]" % (name, k), c1, c2, shortcut=shortcut,
571  rtol=rtol, atol=atol, output=output)
572  if not result and shortcut:
573  return False
574  equal = equal and result
575  return equal
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")
def getCallStack(skip=0)
Definition: callStack.py:169
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:488
def __init__(self, doc, typemap, default=None, optional=False, multi=False, deprecated=None)
def __setitem__(self, k, value, at=None, label="assignment")
def getStackFrame(relative=0)
Definition: callStack.py:52
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:168
def compareScalars(name, v1, v2, output, rtol=1E-8, atol=1E-8, dtype=None)
Definition: comparison.py:56
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:278
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:34
def __setattr__(self, attr, value, at=None, label="assignment")