Coverage for python/lsst/pex/config/configChoiceField.py : 16%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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/>.
28__all__ = ["ConfigChoiceField"]
30import copy
31import collections.abc
33from .config import Config, Field, FieldValidationError, _typeStr, _joinNamePath
34from .comparison import getComparisonName, compareScalars, compareConfigs
35from .callStack import getCallStack, getStackFrame
38class SelectionSet(collections.abc.MutableSet):
39 """A mutable set class that tracks the selection of multi-select
40 `~lsst.pex.config.ConfigChoiceField` objects.
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`.
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 """
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()
83 if setHistory:
84 self.__history.append(("Set selection to %s" % self, at, label))
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")
93 if at is None:
94 at = getCallStack()
96 if value not in self._dict:
97 # invoke __getitem__ to make sure it's present
98 self._dict.__getitem__(value, at=at)
100 self.__history.append(("added %s to selection" % value, at, "selection"))
101 self._set.add(value)
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")
110 if value not in self._dict:
111 return
113 if at is None:
114 at = getCallStack()
116 self.__history.append(("removed %s from selection" % value, at, "selection"))
117 self._set.discard(value)
119 def __len__(self):
120 return len(self._set)
122 def __iter__(self):
123 return iter(self._set)
125 def __contains__(self, value):
126 return value in self._set
128 def __repr__(self):
129 return repr(list(self._set))
131 def __str__(self):
132 return str(list(self._set))
135class ConfigInstanceDict(collections.abc.Mapping):
136 """Dictionary of instantiated configs, used to populate a
137 `~lsst.pex.config.ConfigChoiceField`.
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
157 types = property(lambda x: x._field.typemap) 157 ↛ exitline 157 didn't run the lambda on line 157
159 def __contains__(self, k):
160 return k in self._field.typemap
162 def __len__(self):
163 return len(self._field.typemap)
165 def __iter__(self):
166 return iter(self._field.typemap)
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")
172 if at is None:
173 at = getCallStack(1)
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))
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
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)
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
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
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)
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
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 """
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 """
233 def _getActive(self):
234 if self._selection is None:
235 return None
237 if self._field.multi:
238 return [self[c] for c in self._selection]
239 else:
240 return self[self._selection]
242 active = property(_getActive)
243 """The selected items.
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 """
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
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")
269 try:
270 dtype = self._field.typemap[k]
271 except Exception:
272 raise FieldValidationError(self._field, self._config, "Unknown key %r" % k)
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)
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)
293 def _rename(self, fullname):
294 for k, v in self._dict.items():
295 v._rename(_joinNamePath(name=fullname, index=k))
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)
311class ConfigChoiceField(Field):
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.
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.
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.
339 See also
340 --------
341 ChoiceField
342 ConfigDictField
343 ConfigField
344 ConfigurableField
345 DictField
346 Field
347 ListField
348 RangeField
349 RegistryField
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.
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.
363 When saving a configuration with a ``ConfigChoiceField``, the entire set is
364 saved, as well as the active selection.
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.
371 For example, ``AaaConfig`` is a config object
373 >>> from lsst.pex.config import Config, ConfigChoiceField, Field
374 >>> class AaaConfig(Config):
375 ... somefield = Field("doc", int)
376 ...
378 The ``MyConfig`` config has a ``ConfigChoiceField`` field called ``choice``
379 that maps the ``AaaConfig`` type to the ``"AAA"`` key:
381 >>> TYPEMAP = {"AAA", AaaConfig}
382 >>> class MyConfig(Config):
383 ... choice = ConfigChoiceField("doc for choice", TYPEMAP)
384 ...
386 Creating an instance of ``MyConfig``:
388 >>> instance = MyConfig()
390 Setting value of the field ``somefield`` on the "AAA" key of the ``choice``
391 field:
393 >>> instance.choice['AAA'].somefield = 5
395 **Selecting the active configuration**
397 Make the ``"AAA"`` key the active configuration value for the ``choice``
398 field:
400 >>> instance.choice = "AAA"
402 Alternatively, the last line can be written:
404 >>> instance.choice.name = "AAA"
406 (If the config instance allows multiple selections, you'd assign a sequence
407 to the ``names`` attribute instead.)
409 ``ConfigChoiceField`` instances also allow multiple values of the same
410 type:
412 >>> TYPEMAP["CCC"] = AaaConfig
413 >>> TYPEMAP["BBB"] = AaaConfig
414 """
416 instanceDictClass = ConfigInstanceDict
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
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))
435 return instanceDict
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)
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)
454 else:
455 instanceDict._setSelection(value, at=at, label=label)
457 def rename(self, instance):
458 instanceDict = self.__get__(instance)
459 fullname = _joinNamePath(instance._name, self.name)
460 instanceDict._rename(fullname)
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()
474 def toDict(self, instance):
475 instanceDict = self.__get__(instance)
477 dict_ = {}
478 if self.multi:
479 dict_["names"] = instanceDict.names
480 else:
481 dict_["name"] = instanceDict.name
483 values = {}
484 for k, v in instanceDict.items():
485 values[k] = v.toDict()
486 dict_["values"] = values
488 return dict_
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()
499 def _collectImports(self, instance, imports):
500 instanceDict = self.__get__(instance)
501 for config in instanceDict.values():
502 config._collectImports()
503 imports |= config._imports
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))
515 def __deepcopy__(self, memo):
516 """Customize deep-copying, because we always want a reference to the
517 original typemap.
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
527 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
528 """Compare two fields for equality.
530 Used by `lsst.pex.ConfigChoiceField.compare`.
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.
548 Returns
549 -------
550 isEqual : bool
551 `True` if the fields are equal, `False` otherwise.
553 Notes
554 -----
555 Only the selected configurations are compared, as the parameters of any
556 others do not matter.
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