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