LSST Applications  21.0.0+04719a4bac,21.0.0-1-ga51b5d4+f5e6047307,21.0.0-11-g2b59f77+a9c1acf22d,21.0.0-11-ga42c5b2+86977b0b17,21.0.0-12-gf4ce030+76814010d2,21.0.0-13-g1721dae+760e7a6536,21.0.0-13-g3a573fe+768d78a30a,21.0.0-15-g5a7caf0+f21cbc5713,21.0.0-16-g0fb55c1+b60e2d390c,21.0.0-19-g4cded4ca+71a93a33c0,21.0.0-2-g103fe59+bb20972958,21.0.0-2-g45278ab+04719a4bac,21.0.0-2-g5242d73+3ad5d60fb1,21.0.0-2-g7f82c8f+8babb168e8,21.0.0-2-g8f08a60+06509c8b61,21.0.0-2-g8faa9b5+616205b9df,21.0.0-2-ga326454+8babb168e8,21.0.0-2-gde069b7+5e4aea9c2f,21.0.0-2-gecfae73+1d3a86e577,21.0.0-2-gfc62afb+3ad5d60fb1,21.0.0-25-g1d57be3cd+e73869a214,21.0.0-3-g357aad2+ed88757d29,21.0.0-3-g4a4ce7f+3ad5d60fb1,21.0.0-3-g4be5c26+3ad5d60fb1,21.0.0-3-g65f322c+e0b24896a3,21.0.0-3-g7d9da8d+616205b9df,21.0.0-3-ge02ed75+a9c1acf22d,21.0.0-4-g591bb35+a9c1acf22d,21.0.0-4-g65b4814+b60e2d390c,21.0.0-4-gccdca77+0de219a2bc,21.0.0-4-ge8a399c+6c55c39e83,21.0.0-5-gd00fb1e+05fce91b99,21.0.0-6-gc675373+3ad5d60fb1,21.0.0-64-g1122c245+4fb2b8f86e,21.0.0-7-g04766d7+cd19d05db2,21.0.0-7-gdf92d54+04719a4bac,21.0.0-8-g5674e7b+d1bd76f71f,master-gac4afde19b+a9c1acf22d,w.2021.13
LSST Data Management Base Package
configChoiceField.py
Go to the documentation of this file.
1 # This file is part of pex_config.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (http://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This software is dual licensed under the GNU General Public License and also
10 # under a 3-clause BSD license. Recipients may choose which of these licenses
11 # to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12 # respectively. If you choose the GPL option then the following text applies
13 # (but note that there is still no warranty even if you opt for BSD instead):
14 #
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 
28 __all__ = ["ConfigChoiceField"]
29 
30 import copy
31 import collections.abc
32 
33 from .config import Config, Field, FieldValidationError, _typeStr, _joinNamePath
34 from .comparison import getComparisonName, compareScalars, compareConfigs
35 from .callStack import getCallStack, getStackFrame
36 
37 
38 class SelectionSet(collections.abc.MutableSet):
39  """A mutable set class that tracks the selection of multi-select
40  `~lsst.pex.config.ConfigChoiceField` objects.
41 
42  Parameters
43  ----------
44  dict_ : `ConfigInstanceDict`
45  The dictionary of instantiated configs.
46  value
47  The selected key.
48  at : `lsst.pex.config.callStack.StackFrame`, optional
49  The call stack when the selection was made.
50  label : `str`, optional
51  Label for history tracking.
52  setHistory : `bool`, optional
53  Add this even to the history, if `True`.
54 
55  Notes
56  -----
57  This class allows a user of a multi-select
58  `~lsst.pex.config.ConfigChoiceField` to add or discard items from the set
59  of active configs. Each change to the selection is tracked in the field's
60  history.
61  """
62 
63  def __init__(self, dict_, value, at=None, label="assignment", setHistory=True):
64  if at is None:
65  at = getCallStack()
66  self._dict_dict = dict_
67  self._field_field = self._dict_dict._field
68  self._config_config = self._dict_dict._config
69  self.__history__history = self._config_config._history.setdefault(self._field_field.name, [])
70  if value is not None:
71  try:
72  for v in value:
73  if v not in self._dict_dict:
74  # invoke __getitem__ to ensure it's present
75  self._dict_dict.__getitem__(v, at=at)
76  except TypeError:
77  msg = "Value %s is of incorrect type %s. Sequence type expected" % (value, _typeStr(value))
78  raise FieldValidationError(self._field_field, self._config_config, msg)
79  self._set_set = set(value)
80  else:
81  self._set_set = set()
82 
83  if setHistory:
84  self.__history__history.append(("Set selection to %s" % self, at, label))
85 
86  def add(self, value, at=None):
87  """Add a value to the selected set.
88  """
89  if self._config_config._frozen:
90  raise FieldValidationError(self._field_field, self._config_config,
91  "Cannot modify a frozen Config")
92 
93  if at is None:
94  at = getCallStack()
95 
96  if value not in self._dict_dict:
97  # invoke __getitem__ to make sure it's present
98  self._dict_dict.__getitem__(value, at=at)
99 
100  self.__history__history.append(("added %s to selection" % value, at, "selection"))
101  self._set_set.add(value)
102 
103  def discard(self, value, at=None):
104  """Discard a value from the selected set.
105  """
106  if self._config_config._frozen:
107  raise FieldValidationError(self._field_field, self._config_config,
108  "Cannot modify a frozen Config")
109 
110  if value not in self._dict_dict:
111  return
112 
113  if at is None:
114  at = getCallStack()
115 
116  self.__history__history.append(("removed %s from selection" % value, at, "selection"))
117  self._set_set.discard(value)
118 
119  def __len__(self):
120  return len(self._set_set)
121 
122  def __iter__(self):
123  return iter(self._set_set)
124 
125  def __contains__(self, value):
126  return value in self._set_set
127 
128  def __repr__(self):
129  return repr(list(self._set_set))
130 
131  def __str__(self):
132  return str(list(self._set_set))
133 
134 
135 class ConfigInstanceDict(collections.abc.Mapping):
136  """Dictionary of instantiated configs, used to populate a
137  `~lsst.pex.config.ConfigChoiceField`.
138 
139  Parameters
140  ----------
141  config : `lsst.pex.config.Config`
142  A configuration instance.
143  field : `lsst.pex.config.Field`-type
144  A configuration field. Note that the `lsst.pex.config.Field.fieldmap`
145  attribute must provide key-based access to configuration classes,
146  (that is, ``typemap[name]``).
147  """
148  def __init__(self, config, field):
149  collections.abc.Mapping.__init__(self)
150  self._dict_dict = dict()
151  self._selection_selection = None
152  self._config_config = config
153  self._field_field = field
154  self._history_history = config._history.setdefault(field.name, [])
155  self.__doc____doc__ = field.doc
156  self._typemap_typemap = None
157 
158  @property
159  def types(self):
160  return self._typemap_typemap if self._typemap_typemap is not None else self._field_field.typemap
161 
162  def __contains__(self, k):
163  return k in self.typestypes
164 
165  def __len__(self):
166  return len(self.typestypes)
167 
168  def __iter__(self):
169  return iter(self.typestypes)
170 
171  def _setSelection(self, value, at=None, label="assignment"):
172  if self._config_config._frozen:
173  raise FieldValidationError(self._field_field, self._config_config, "Cannot modify a frozen Config")
174 
175  if at is None:
176  at = getCallStack(1)
177 
178  if value is None:
179  self._selection_selection = None
180  elif self._field_field.multi:
181  self._selection_selection = SelectionSet(self, value, setHistory=False)
182  else:
183  if value not in self._dict_dict:
184  self.__getitem____getitem__(value, at=at) # just invoke __getitem__ to make sure it's present
185  self._selection_selection = value
186  self._history_history.append((value, at, label))
187 
188  def _getNames(self):
189  if not self._field_field.multi:
190  raise FieldValidationError(self._field_field, self._config_config,
191  "Single-selection field has no attribute 'names'")
192  return self._selection_selection
193 
194  def _setNames(self, value):
195  if not self._field_field.multi:
196  raise FieldValidationError(self._field_field, self._config_config,
197  "Single-selection field has no attribute 'names'")
198  self._setSelection_setSelection(value)
199 
200  def _delNames(self):
201  if not self._field_field.multi:
202  raise FieldValidationError(self._field_field, self._config_config,
203  "Single-selection field has no attribute 'names'")
204  self._selection_selection = None
205 
206  def _getName(self):
207  if self._field_field.multi:
208  raise FieldValidationError(self._field_field, self._config_config,
209  "Multi-selection field has no attribute 'name'")
210  return self._selection_selection
211 
212  def _setName(self, value):
213  if self._field_field.multi:
214  raise FieldValidationError(self._field_field, self._config_config,
215  "Multi-selection field has no attribute 'name'")
216  self._setSelection_setSelection(value)
217 
218  def _delName(self):
219  if self._field_field.multi:
220  raise FieldValidationError(self._field_field, self._config_config,
221  "Multi-selection field has no attribute 'name'")
222  self._selection_selection = None
223 
224  names = property(_getNames, _setNames, _delNames)
225  """List of names of active items in a multi-selection
226  ``ConfigInstanceDict``. Disabled in a single-selection ``_Registry``; use
227  the `name` attribute instead.
228  """
229 
230  name = property(_getName, _setName, _delName)
231  """Name of the active item in a single-selection ``ConfigInstanceDict``.
232  Disabled in a multi-selection ``_Registry``; use the ``names`` attribute
233  instead.
234  """
235 
236  def _getActive(self):
237  if self._selection_selection is None:
238  return None
239 
240  if self._field_field.multi:
241  return [self[c] for c in self._selection_selection]
242  else:
243  return self[self._selection_selection]
244 
245  active = property(_getActive)
246  """The selected items.
247 
248  For multi-selection, this is equivalent to: ``[self[name] for name in
249  self.names]``. For single-selection, this is equivalent to: ``self[name]``.
250  """
251 
252  def __getitem__(self, k, at=None, label="default"):
253  try:
254  value = self._dict_dict[k]
255  except KeyError:
256  try:
257  dtype = self.typestypes[k]
258  except Exception:
259  raise FieldValidationError(self._field_field, self._config_config,
260  "Unknown key %r in Registry/ConfigChoiceField" % k)
261  name = _joinNamePath(self._config_config._name, self._field_field.name, k)
262  if at is None:
263  at = getCallStack()
264  at.insert(0, dtype._source)
265  value = self._dict_dict.setdefault(k, dtype(__name=name, __at=at, __label=label))
266  return value
267 
268  def __setitem__(self, k, value, at=None, label="assignment"):
269  if self._config_config._frozen:
270  raise FieldValidationError(self._field_field, self._config_config, "Cannot modify a frozen Config")
271 
272  try:
273  dtype = self.typestypes[k]
274  except Exception:
275  raise FieldValidationError(self._field_field, self._config_config, "Unknown key %r" % k)
276 
277  if value != dtype and type(value) != dtype:
278  msg = "Value %s at key %s is of incorrect type %s. Expected type %s" % \
279  (value, k, _typeStr(value), _typeStr(dtype))
280  raise FieldValidationError(self._field_field, self._config_config, msg)
281 
282  if at is None:
283  at = getCallStack()
284  name = _joinNamePath(self._config_config._name, self._field_field.name, k)
285  oldValue = self._dict_dict.get(k, None)
286  if oldValue is None:
287  if value == dtype:
288  self._dict_dict[k] = value(__name=name, __at=at, __label=label)
289  else:
290  self._dict_dict[k] = dtype(__name=name, __at=at, __label=label, **value._storage)
291  else:
292  if value == dtype:
293  value = value()
294  oldValue.update(__at=at, __label=label, **value._storage)
295 
296  def _rename(self, fullname):
297  for k, v in self._dict_dict.items():
298  v._rename(_joinNamePath(name=fullname, index=k))
299 
300  def __setattr__(self, attr, value, at=None, label="assignment"):
301  if hasattr(getattr(self.__class__, attr, None), '__set__'):
302  # This allows properties to work.
303  object.__setattr__(self, attr, value)
304  elif attr in self.__dict__ or attr in ["_history", "_field", "_config", "_dict",
305  "_selection", "__doc__", "_typemap"]:
306  # This allows specific private attributes to work.
307  object.__setattr__(self, attr, value)
308  else:
309  # We throw everything else.
310  msg = "%s has no attribute %s" % (_typeStr(self._field_field), attr)
311  raise FieldValidationError(self._field_field, self._config_config, msg)
312 
313  def freeze(self):
314  """Invoking this freeze method will create a local copy of the field
315  attribute's typemap. This decouples this instance dict from the
316  underlying objects type map ensuring that and subsequent changes to the
317  typemap will not be reflected in this instance (i.e imports adding
318  additional registry entries).
319  """
320  if self._typemap_typemap is None:
321  self._typemap_typemap = copy.deepcopy(self.typestypes)
322 
323 
325  """A configuration field (`~lsst.pex.config.Field` subclass) that allows a
326  user to choose from a set of `~lsst.pex.config.Config` types.
327 
328  Parameters
329  ----------
330  doc : `str`
331  Documentation string for the field.
332  typemap : `dict`-like
333  A mapping between keys and `~lsst.pex.config.Config`-types as values.
334  See *Examples* for details.
335  default : `str`, optional
336  The default configuration name.
337  optional : `bool`, optional
338  When `False`, `lsst.pex.config.Config.validate` will fail if the
339  field's value is `None`.
340  multi : `bool`, optional
341  If `True`, the field allows multiple selections. In this case, set the
342  selections by assigning a sequence to the ``names`` attribute of the
343  field.
344 
345  If `False`, the field allows only a single selection. In this case,
346  set the active config by assigning the config's key from the
347  ``typemap`` to the field's ``name`` attribute (see *Examples*).
348  deprecated : None or `str`, optional
349  A description of why this Field is deprecated, including removal date.
350  If not None, the string is appended to the docstring for this Field.
351 
352  See also
353  --------
354  ChoiceField
355  ConfigDictField
356  ConfigField
357  ConfigurableField
358  DictField
359  Field
360  ListField
361  RangeField
362  RegistryField
363 
364  Notes
365  -----
366  ``ConfigChoiceField`` instances can allow either single selections or
367  multiple selections, depending on the ``multi`` parameter. For
368  single-selection fields, set the selection with the ``name`` attribute.
369  For multi-selection fields, set the selection though the ``names``
370  attribute.
371 
372  This field is validated only against the active selection. If the
373  ``active`` attribute is `None` and the field is not optional, validation
374  will fail.
375 
376  When saving a configuration with a ``ConfigChoiceField``, the entire set is
377  saved, as well as the active selection.
378 
379  Examples
380  --------
381  While the ``typemap`` is shared by all instances of the field, each
382  instance of the field has its own instance of a particular sub-config type.
383 
384  For example, ``AaaConfig`` is a config object
385 
386  >>> from lsst.pex.config import Config, ConfigChoiceField, Field
387  >>> class AaaConfig(Config):
388  ... somefield = Field("doc", int)
389  ...
390 
391  The ``MyConfig`` config has a ``ConfigChoiceField`` field called ``choice``
392  that maps the ``AaaConfig`` type to the ``"AAA"`` key:
393 
394  >>> TYPEMAP = {"AAA", AaaConfig}
395  >>> class MyConfig(Config):
396  ... choice = ConfigChoiceField("doc for choice", TYPEMAP)
397  ...
398 
399  Creating an instance of ``MyConfig``:
400 
401  >>> instance = MyConfig()
402 
403  Setting value of the field ``somefield`` on the "AAA" key of the ``choice``
404  field:
405 
406  >>> instance.choice['AAA'].somefield = 5
407 
408  **Selecting the active configuration**
409 
410  Make the ``"AAA"`` key the active configuration value for the ``choice``
411  field:
412 
413  >>> instance.choice = "AAA"
414 
415  Alternatively, the last line can be written:
416 
417  >>> instance.choice.name = "AAA"
418 
419  (If the config instance allows multiple selections, you'd assign a sequence
420  to the ``names`` attribute instead.)
421 
422  ``ConfigChoiceField`` instances also allow multiple values of the same
423  type:
424 
425  >>> TYPEMAP["CCC"] = AaaConfig
426  >>> TYPEMAP["BBB"] = AaaConfig
427  """
428 
429  instanceDictClass = ConfigInstanceDict
430 
431  def __init__(self, doc, typemap, default=None, optional=False, multi=False, deprecated=None):
432  source = getStackFrame()
433  self._setup_setup(doc=doc, dtype=self.instanceDictClassinstanceDictClass, default=default, check=None, optional=optional,
434  source=source, deprecated=deprecated)
435  self.typemaptypemap = typemap
436  self.multimulti = multi
437 
438  def _getOrMake(self, instance, label="default"):
439  instanceDict = instance._storage.get(self.name)
440  if instanceDict is None:
441  at = getCallStack(1)
442  instanceDict = self.dtypedtype(instance, self)
443  instanceDict.__doc__ = self.docdoc
444  instance._storage[self.name] = instanceDict
445  history = instance._history.setdefault(self.name, [])
446  history.append(("Initialized from defaults", at, label))
447 
448  return instanceDict
449 
450  def __get__(self, instance, owner=None):
451  if instance is None or not isinstance(instance, Config):
452  return self
453  else:
454  return self._getOrMake_getOrMake(instance)
455 
456  def __set__(self, instance, value, at=None, label="assignment"):
457  if instance._frozen:
458  raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
459  if at is None:
460  at = getCallStack()
461  instanceDict = self._getOrMake_getOrMake(instance)
462  if isinstance(value, self.instanceDictClassinstanceDictClass):
463  for k, v in value.items():
464  instanceDict.__setitem__(k, v, at=at, label=label)
465  instanceDict._setSelection(value._selection, at=at, label=label)
466 
467  else:
468  instanceDict._setSelection(value, at=at, label=label)
469 
470  def rename(self, instance):
471  instanceDict = self.__get____get____get__(instance)
472  fullname = _joinNamePath(instance._name, self.name)
473  instanceDict._rename(fullname)
474 
475  def validate(self, instance):
476  instanceDict = self.__get____get____get__(instance)
477  if instanceDict.active is None and not self.optionaloptional:
478  msg = "Required field cannot be None"
479  raise FieldValidationError(self, instance, msg)
480  elif instanceDict.active is not None:
481  if self.multimulti:
482  for a in instanceDict.active:
483  a.validate()
484  else:
485  instanceDict.active.validate()
486 
487  def toDict(self, instance):
488  instanceDict = self.__get____get____get__(instance)
489 
490  dict_ = {}
491  if self.multimulti:
492  dict_["names"] = instanceDict.names
493  else:
494  dict_["name"] = instanceDict.name
495 
496  values = {}
497  for k, v in instanceDict.items():
498  values[k] = v.toDict()
499  dict_["values"] = values
500 
501  return dict_
502 
503  def freeze(self, instance):
504  instanceDict = self.__get____get____get__(instance)
505  instanceDict.freeze()
506  for v in instanceDict.values():
507  v.freeze()
508 
509  def _collectImports(self, instance, imports):
510  instanceDict = self.__get____get____get__(instance)
511  for config in instanceDict.values():
512  config._collectImports()
513  imports |= config._imports
514 
515  def save(self, outfile, instance):
516  instanceDict = self.__get____get____get__(instance)
517  fullname = _joinNamePath(instance._name, self.name)
518  for v in instanceDict.values():
519  v._save(outfile)
520  if self.multimulti:
521  outfile.write(u"{}.names={!r}\n".format(fullname, instanceDict.names))
522  else:
523  outfile.write(u"{}.name={!r}\n".format(fullname, instanceDict.name))
524 
525  def __deepcopy__(self, memo):
526  """Customize deep-copying, because we always want a reference to the
527  original typemap.
528 
529  WARNING: this must be overridden by subclasses if they change the
530  constructor signature!
531  """
532  other = type(self)(doc=self.docdoc, typemap=self.typemaptypemap, default=copy.deepcopy(self.defaultdefault),
533  optional=self.optionaloptional, multi=self.multimulti)
534  other.source = self.sourcesource
535  return other
536 
537  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
538  """Compare two fields for equality.
539 
540  Used by `lsst.pex.ConfigChoiceField.compare`.
541 
542  Parameters
543  ----------
544  instance1 : `lsst.pex.config.Config`
545  Left-hand side config instance to compare.
546  instance2 : `lsst.pex.config.Config`
547  Right-hand side config instance to compare.
548  shortcut : `bool`
549  If `True`, this function returns as soon as an inequality if found.
550  rtol : `float`
551  Relative tolerance for floating point comparisons.
552  atol : `float`
553  Absolute tolerance for floating point comparisons.
554  output : callable
555  A callable that takes a string, used (possibly repeatedly) to
556  report inequalities.
557 
558  Returns
559  -------
560  isEqual : bool
561  `True` if the fields are equal, `False` otherwise.
562 
563  Notes
564  -----
565  Only the selected configurations are compared, as the parameters of any
566  others do not matter.
567 
568  Floating point comparisons are performed by `numpy.allclose`.
569  """
570  d1 = getattr(instance1, self.name)
571  d2 = getattr(instance2, self.name)
572  name = getComparisonName(
573  _joinNamePath(instance1._name, self.name),
574  _joinNamePath(instance2._name, self.name)
575  )
576  if not compareScalars("selection for %s" % name, d1._selection, d2._selection, output=output):
577  return False
578  if d1._selection is None:
579  return True
580  if self.multimulti:
581  nested = [(k, d1[k], d2[k]) for k in d1._selection]
582  else:
583  nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
584  equal = True
585  for k, c1, c2 in nested:
586  result = compareConfigs("%s[%r]" % (name, k), c1, c2, shortcut=shortcut,
587  rtol=rtol, atol=atol, output=output)
588  if not result and shortcut:
589  return False
590  equal = equal and result
591  return equal
std::vector< SchemaItem< Flag > > * items
table::Key< int > type
Definition: Detector.cc:163
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:550
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:336
def __set__(self, instance, value, at=None, label="assignment")
def __init__(self, doc, typemap, default=None, optional=False, multi=False, deprecated=None)
def _getOrMake(self, instance, label="default")
def _setSelection(self, value, at=None, label="assignment")
def __getitem__(self, k, at=None, label="default")
def __setattr__(self, attr, value, at=None, label="assignment")
def __setitem__(self, k, value, at=None, label="assignment")
def __init__(self, dict_, value, at=None, label="assignment", setHistory=True)
daf::base::PropertyList * list
Definition: fits.cc:913
daf::base::PropertySet * set
Definition: fits.cc:912
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
def getCallStack(skip=0)
Definition: callStack.py:175
def getStackFrame(relative=0)
Definition: callStack.py:58
def compareConfigs(name, c1, c2, shortcut=True, rtol=1E-8, atol=1E-8, output=None)
Definition: comparison.py:111
def compareScalars(name, v1, v2, output, rtol=1E-8, atol=1E-8, dtype=None)
Definition: comparison.py:62
def getComparisonName(name1, name2)
Definition: comparison.py:40
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174