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