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