LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
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/>.
27from __future__ import annotations
28
29__all__ = ["ConfigChoiceField"]
30
31import collections.abc
32import copy
33import sys
34import weakref
35from typing import Any, ForwardRef, Optional, Union, overload
36
37from .callStack import getCallStack, getStackFrame
38from .comparison import compareConfigs, compareScalars, getComparisonName
39from .config import Config, Field, FieldValidationError, UnexpectedProxyUsageError, _joinNamePath, _typeStr
40
41
42class SelectionSet(collections.abc.MutableSet):
43 """A mutable set class that tracks the selection of multi-select
45
46 Parameters
47 ----------
48 dict_ : `ConfigInstanceDict`
49 The dictionary of instantiated configs.
50 value
51 The selected key.
53 The call stack when the selection was made.
54 label : `str`, optional
55 Label for history tracking.
56 setHistory : `bool`, optional
57 Add this even to the history, if `True`.
58
59 Notes
60 -----
61 This class allows a user of a multi-select
62 `~lsst.pex.config.ConfigChoiceField` to add or discard items from the set
63 of active configs. Each change to the selection is tracked in the field's
64 history.
65 """
66
67 def __init__(self, dict_, value, at=None, label="assignment", setHistory=True):
68 if at is None:
69 at = getCallStack()
70 self._dict = dict_
71 self._field = self._dict._field
72 self._config_ = weakref.ref(self._dict._config)
73 self.__history = self._config._history.setdefault(self._field.name, [])
74 if value is not None:
75 try:
76 for v in value:
77 if v not in self._dict:
78 # invoke __getitem__ to ensure it's present
79 self._dict.__getitem__(v, at=at)
80 except TypeError:
81 msg = "Value %s is of incorrect type %s. Sequence type expected" % (value, _typeStr(value))
82 raise FieldValidationError(self._field, self._config, msg)
83 self._set = set(value)
84 else:
85 self._set = set()
86
87 if setHistory:
88 self.__history.append(("Set selection to %s" % self, at, label))
89
90 @property
91 def _config(self) -> Config:
92 # Config Fields should never outlive their config class instance
93 # assert that as such here
94 assert self._config_() is not None
95 return self._config_()
96
97 def add(self, value, at=None):
98 """Add a value to the selected set."""
99 if self._config._frozen:
100 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
101
102 if at is None:
103 at = getCallStack()
104
105 if value not in self._dict:
106 # invoke __getitem__ to make sure it's present
107 self._dict.__getitem__(value, at=at)
108
109 self.__history.append(("added %s to selection" % value, at, "selection"))
110 self._set.add(value)
111
112 def discard(self, value, at=None):
113 """Discard a value from the selected set."""
114 if self._config._frozen:
115 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
116
117 if value not in self._dict:
118 return
119
120 if at is None:
121 at = getCallStack()
122
123 self.__history.append(("removed %s from selection" % value, at, "selection"))
124 self._set.discard(value)
125
126 def __len__(self):
127 return len(self._set)
128
129 def __iter__(self):
130 return iter(self._set)
131
132 def __contains__(self, value):
133 return value in self._set
134
135 def __repr__(self):
136 return repr(list(self._set))
137
138 def __str__(self):
139 return str(list(self._set))
140
141 def __reduce__(self):
143 f"Proxy container for config field {self._field.name} cannot "
144 "be pickled; it should be converted to a built-in container before "
145 "being assigned to other objects or variables."
146 )
147
148
149if int(sys.version_info.minor) < 9:
150 _bases = (collections.abc.Mapping,)
151else:
152 _bases = (collections.abc.Mapping[str, Config],)
153
154
155class ConfigInstanceDict(*_bases):
156 """Dictionary of instantiated configs, used to populate a
158
159 Parameters
160 ----------
161 config : `lsst.pex.config.Config`
162 A configuration instance.
163 field : `lsst.pex.config.Field`-type
164 A configuration field. Note that the `lsst.pex.config.Field.fieldmap`
165 attribute must provide key-based access to configuration classes,
166 (that is, ``typemap[name]``).
167 """
168
169 def __init__(self, config, field):
170 collections.abc.Mapping.__init__(self)
171 self._dict = dict()
172 self._selection = None
173 self._config = config
174 self._field = field
175 self._history = config._history.setdefault(field.name, [])
176 self.__doc__ = field.doc
177 self._typemap = None
178
179 @property
180 def types(self):
181 return self._typemap if self._typemap is not None else self._field.typemap
182
183 def __contains__(self, k):
184 return k in self.types
185
186 def __len__(self):
187 return len(self.types)
188
189 def __iter__(self):
190 return iter(self.types)
191
192 def _setSelection(self, value, at=None, label="assignment"):
193 if self._config._frozen:
194 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
195
196 if at is None:
197 at = getCallStack(1)
198
199 if value is None:
200 self._selection = None
201 elif self._field.multi:
202 self._selection = SelectionSet(self, value, setHistory=False)
203 else:
204 if value not in self._dict:
205 self.__getitem__(value, at=at) # just invoke __getitem__ to make sure it's present
206 self._selection = value
207 self._history.append((value, at, label))
208
209 def _getNames(self):
210 if not self._field.multi:
212 self._field, self._config, "Single-selection field has no attribute 'names'"
213 )
214 return self._selection
215
216 def _setNames(self, value):
217 if not self._field.multi:
219 self._field, self._config, "Single-selection field has no attribute 'names'"
220 )
221 self._setSelection(value)
222
223 def _delNames(self):
224 if not self._field.multi:
226 self._field, self._config, "Single-selection field has no attribute 'names'"
227 )
228 self._selection = None
229
230 def _getName(self):
231 if self._field.multi:
233 self._field, self._config, "Multi-selection field has no attribute 'name'"
234 )
235 return self._selection
236
237 def _setName(self, value):
238 if self._field.multi:
240 self._field, self._config, "Multi-selection field has no attribute 'name'"
241 )
242 self._setSelection(value)
243
244 def _delName(self):
245 if self._field.multi:
247 self._field, self._config, "Multi-selection field has no attribute 'name'"
248 )
249 self._selection = None
250
251 names = property(_getNames, _setNames, _delNames)
252 """List of names of active items in a multi-selection
253 ``ConfigInstanceDict``. Disabled in a single-selection ``_Registry``; use
254 the `name` attribute instead.
255 """
256
257 name = property(_getName, _setName, _delName)
258 """Name of the active item in a single-selection ``ConfigInstanceDict``.
259 Disabled in a multi-selection ``_Registry``; use the ``names`` attribute
260 instead.
261 """
262
263 def _getActive(self):
264 if self._selection is None:
265 return None
266
267 if self._field.multi:
268 return [self[c] for c in self._selection]
269 else:
270 return self[self._selection]
271
272 active = property(_getActive)
273 """The selected items.
274
275 For multi-selection, this is equivalent to: ``[self[name] for name in
276 self.names]``. For single-selection, this is equivalent to: ``self[name]``.
277 """
278
279 def __getitem__(self, k, at=None, label="default"):
280 try:
281 value = self._dict[k]
282 except KeyError:
283 try:
284 dtype = self.types[k]
285 except Exception:
287 self._field, self._config, "Unknown key %r in Registry/ConfigChoiceField" % k
288 )
289 name = _joinNamePath(self._config._name, self._field.name, k)
290 if at is None:
291 at = getCallStack()
292 at.insert(0, dtype._source)
293 value = self._dict.setdefault(k, dtype(__name=name, __at=at, __label=label))
294 return value
295
296 def __setitem__(self, k, value, at=None, label="assignment"):
297 if self._config._frozen:
298 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
299
300 try:
301 dtype = self.types[k]
302 except Exception:
303 raise FieldValidationError(self._field, self._config, "Unknown key %r" % k)
304
305 if value != dtype and type(value) != dtype:
306 msg = "Value %s at key %s is of incorrect type %s. Expected type %s" % (
307 value,
308 k,
309 _typeStr(value),
310 _typeStr(dtype),
311 )
312 raise FieldValidationError(self._field, self._config, msg)
313
314 if at is None:
315 at = getCallStack()
316 name = _joinNamePath(self._config._name, self._field.name, k)
317 oldValue = self._dict.get(k, None)
318 if oldValue is None:
319 if value == dtype:
320 self._dict[k] = value(__name=name, __at=at, __label=label)
321 else:
322 self._dict[k] = dtype(__name=name, __at=at, __label=label, **value._storage)
323 else:
324 if value == dtype:
325 value = value()
326 oldValue.update(__at=at, __label=label, **value._storage)
327
328 def _rename(self, fullname):
329 for k, v in self._dict.items():
330 v._rename(_joinNamePath(name=fullname, index=k))
331
332 def __setattr__(self, attr, value, at=None, label="assignment"):
333 if hasattr(getattr(self.__class__, attr, None), "__set__"):
334 # This allows properties to work.
335 object.__setattr__(self, attr, value)
336 elif attr in self.__dict__ or attr in [
337 "_history",
338 "_field",
339 "_config",
340 "_dict",
341 "_selection",
342 "__doc__",
343 "_typemap",
344 ]:
345 # This allows specific private attributes to work.
346 object.__setattr__(self, attr, value)
347 else:
348 # We throw everything else.
349 msg = "%s has no attribute %s" % (_typeStr(self._field), attr)
350 raise FieldValidationError(self._field, self._config, msg)
351
352 def freeze(self):
353 """Invoking this freeze method will create a local copy of the field
354 attribute's typemap. This decouples this instance dict from the
355 underlying objects type map ensuring that and subsequent changes to the
356 typemap will not be reflected in this instance (i.e imports adding
357 additional registry entries).
358 """
359 if self._typemap is None:
360 self._typemap = copy.deepcopy(self.types)
361
362 def __reduce__(self):
364 f"Proxy container for config field {self._field.name} cannot "
365 "be pickled; it should be converted to a built-in container before "
366 "being assigned to other objects or variables."
367 )
368
369
370class ConfigChoiceField(Field[ConfigInstanceDict]):
371 """A configuration field (`~lsst.pex.config.Field` subclass) that allows a
372 user to choose from a set of `~lsst.pex.config.Config` types.
373
374 Parameters
375 ----------
376 doc : `str`
377 Documentation string for the field.
378 typemap : `dict`-like
379 A mapping between keys and `~lsst.pex.config.Config`-types as values.
380 See *Examples* for details.
381 default : `str`, optional
382 The default configuration name.
383 optional : `bool`, optional
384 When `False`, `lsst.pex.config.Config.validate` will fail if the
385 field's value is `None`.
386 multi : `bool`, optional
387 If `True`, the field allows multiple selections. In this case, set the
388 selections by assigning a sequence to the ``names`` attribute of the
389 field.
390
391 If `False`, the field allows only a single selection. In this case,
392 set the active config by assigning the config's key from the
393 ``typemap`` to the field's ``name`` attribute (see *Examples*).
394 deprecated : None or `str`, optional
395 A description of why this Field is deprecated, including removal date.
396 If not None, the string is appended to the docstring for this Field.
397
398 See also
399 --------
400 ChoiceField
401 ConfigDictField
402 ConfigField
403 ConfigurableField
404 DictField
405 Field
406 ListField
407 RangeField
408 RegistryField
409
410 Notes
411 -----
412 ``ConfigChoiceField`` instances can allow either single selections or
413 multiple selections, depending on the ``multi`` parameter. For
414 single-selection fields, set the selection with the ``name`` attribute.
415 For multi-selection fields, set the selection though the ``names``
416 attribute.
417
418 This field is validated only against the active selection. If the
419 ``active`` attribute is `None` and the field is not optional, validation
420 will fail.
421
422 When saving a configuration with a ``ConfigChoiceField``, the entire set is
423 saved, as well as the active selection.
424
425 Examples
426 --------
427 While the ``typemap`` is shared by all instances of the field, each
428 instance of the field has its own instance of a particular sub-config type.
429
430 For example, ``AaaConfig`` is a config object
431
432 >>> from lsst.pex.config import Config, ConfigChoiceField, Field
433 >>> class AaaConfig(Config):
434 ... somefield = Field("doc", int)
435 ...
436
437 The ``MyConfig`` config has a ``ConfigChoiceField`` field called ``choice``
438 that maps the ``AaaConfig`` type to the ``"AAA"`` key:
439
440 >>> TYPEMAP = {"AAA", AaaConfig}
441 >>> class MyConfig(Config):
442 ... choice = ConfigChoiceField("doc for choice", TYPEMAP)
443 ...
444
445 Creating an instance of ``MyConfig``:
446
447 >>> instance = MyConfig()
448
449 Setting value of the field ``somefield`` on the "AAA" key of the ``choice``
450 field:
451
452 >>> instance.choice['AAA'].somefield = 5
453
454 **Selecting the active configuration**
455
456 Make the ``"AAA"`` key the active configuration value for the ``choice``
457 field:
458
459 >>> instance.choice = "AAA"
460
461 Alternatively, the last line can be written:
462
463 >>> instance.choice.name = "AAA"
464
465 (If the config instance allows multiple selections, you'd assign a sequence
466 to the ``names`` attribute instead.)
467
468 ``ConfigChoiceField`` instances also allow multiple values of the same
469 type:
470
471 >>> TYPEMAP["CCC"] = AaaConfig
472 >>> TYPEMAP["BBB"] = AaaConfig
473 """
474
475 instanceDictClass = ConfigInstanceDict
476
477 def __init__(self, doc, typemap, default=None, optional=False, multi=False, deprecated=None):
478 source = getStackFrame()
479 self._setup(
480 doc=doc,
481 dtype=self.instanceDictClass,
482 default=default,
483 check=None,
484 optional=optional,
485 source=source,
486 deprecated=deprecated,
487 )
488 self.typemap = typemap
489 self.multi = multi
490
491 def __class_getitem__(cls, params: Union[tuple[type, ...], type, ForwardRef]):
492 raise ValueError("ConfigChoiceField does not support typing argument")
493
494 def _getOrMake(self, instance, label="default"):
495 instanceDict = instance._storage.get(self.name)
496 if instanceDict is None:
497 at = getCallStack(1)
498 instanceDict = self.dtype(instance, self)
499 instanceDict.__doc__ = self.doc
500 instance._storage[self.name] = instanceDict
501 history = instance._history.setdefault(self.name, [])
502 history.append(("Initialized from defaults", at, label))
503
504 return instanceDict
505
506 @overload
508 self, instance: None, owner: Any = None, at: Any = None, label: str = "default"
509 ) -> "ConfigChoiceField":
510 ...
511
512 @overload
514 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
515 ) -> ConfigInstanceDict:
516 ...
517
518 def __get__(self, instance, owner=None, at=None, label="default"):
519 if instance is None or not isinstance(instance, Config):
520 return self
521 else:
522 return self._getOrMake(instance)
523
525 self, instance: Config, value: Optional[ConfigInstanceDict], at: Any = None, label: str = "assignment"
526 ) -> None:
527 if instance._frozen:
528 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
529 if at is None:
530 at = getCallStack()
531 instanceDict = self._getOrMake(instance)
532 if isinstance(value, self.instanceDictClass):
533 for k, v in value.items():
534 instanceDict.__setitem__(k, v, at=at, label=label)
535 instanceDict._setSelection(value._selection, at=at, label=label)
536
537 else:
538 instanceDict._setSelection(value, at=at, label=label)
539
540 def rename(self, instance):
541 instanceDict = self.__get____get____get____get____get____get__(instance)
542 fullname = _joinNamePath(instance._name, self.name)
543 instanceDict._rename(fullname)
544
545 def validate(self, instance):
546 instanceDict = self.__get____get____get____get____get____get__(instance)
547 if instanceDict.active is None and not self.optional:
548 msg = "Required field cannot be None"
549 raise FieldValidationError(self, instance, msg)
550 elif instanceDict.active is not None:
551 if self.multi:
552 for a in instanceDict.active:
553 a.validate()
554 else:
555 instanceDict.active.validate()
556
557 def toDict(self, instance):
558 instanceDict = self.__get____get____get____get____get____get__(instance)
559
560 dict_ = {}
561 if self.multi:
562 dict_["names"] = instanceDict.names
563 else:
564 dict_["name"] = instanceDict.name
565
566 values = {}
567 for k, v in instanceDict.items():
568 values[k] = v.toDict()
569 dict_["values"] = values
570
571 return dict_
572
573 def freeze(self, instance):
574 instanceDict = self.__get____get____get____get____get____get__(instance)
575 instanceDict.freeze()
576 for v in instanceDict.values():
577 v.freeze()
578
579 def _collectImports(self, instance, imports):
580 instanceDict = self.__get____get____get____get____get____get__(instance)
581 for config in instanceDict.values():
582 config._collectImports()
583 imports |= config._imports
584
585 def save(self, outfile, instance):
586 instanceDict = self.__get____get____get____get____get____get__(instance)
587 fullname = _joinNamePath(instance._name, self.name)
588 for v in instanceDict.values():
589 v._save(outfile)
590 if self.multi:
591 outfile.write("{}.names={!r}\n".format(fullname, sorted(instanceDict.names)))
592 else:
593 outfile.write("{}.name={!r}\n".format(fullname, instanceDict.name))
594
595 def __deepcopy__(self, memo):
596 """Customize deep-copying, because we always want a reference to the
597 original typemap.
598
599 WARNING: this must be overridden by subclasses if they change the
600 constructor signature!
601 """
602 other = type(self)(
603 doc=self.doc,
604 typemap=self.typemap,
605 default=copy.deepcopy(self.default),
606 optional=self.optional,
607 multi=self.multi,
608 )
609 other.source = self.source
610 return other
611
612 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
613 """Compare two fields for equality.
614
615 Used by `lsst.pex.ConfigChoiceField.compare`.
616
617 Parameters
618 ----------
619 instance1 : `lsst.pex.config.Config`
620 Left-hand side config instance to compare.
621 instance2 : `lsst.pex.config.Config`
622 Right-hand side config instance to compare.
623 shortcut : `bool`
624 If `True`, this function returns as soon as an inequality if found.
625 rtol : `float`
626 Relative tolerance for floating point comparisons.
627 atol : `float`
628 Absolute tolerance for floating point comparisons.
629 output : callable
630 A callable that takes a string, used (possibly repeatedly) to
631 report inequalities.
632
633 Returns
634 -------
635 isEqual : bool
636 `True` if the fields are equal, `False` otherwise.
637
638 Notes
639 -----
640 Only the selected configurations are compared, as the parameters of any
641 others do not matter.
642
643 Floating point comparisons are performed by `numpy.allclose`.
644 """
645 d1 = getattr(instance1, self.name)
646 d2 = getattr(instance2, self.name)
647 name = getComparisonName(
648 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
649 )
650 if not compareScalars("selection for %s" % name, d1._selection, d2._selection, output=output):
651 return False
652 if d1._selection is None:
653 return True
654 if self.multi:
655 nested = [(k, d1[k], d2[k]) for k in d1._selection]
656 else:
657 nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
658 equal = True
659 for k, c1, c2 in nested:
660 result = compareConfigs(
661 "%s[%r]" % (name, k), c1, c2, shortcut=shortcut, rtol=rtol, atol=atol, output=output
662 )
663 if not result and shortcut:
664 return False
665 equal = equal and result
666 return equal
std::vector< SchemaItem< Flag > > * items
table::Key< int > type
Definition: Detector.cc:163
table::Key< int > a
"Field[FieldTypeVar]" __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition: config.py:713
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:722
FieldTypeVar __get__(self, "Config" instance, Any owner=None, Any at=None, str label="default")
Definition: config.py:719
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:494
ConfigInstanceDict __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
def __init__(self, doc, typemap, default=None, optional=False, multi=False, deprecated=None)
def _getOrMake(self, instance, label="default")
def __get__(self, instance, owner=None, at=None, label="default")
def __class_getitem__(cls, Union[tuple[type,...], type, ForwardRef] params)
None __set__(self, Config instance, Optional[ConfigInstanceDict] value, Any at=None, str label="assignment")
"ConfigChoiceField" __get__(self, None instance, Any owner=None, Any at=None, str 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:928
daf::base::PropertySet * set
Definition: fits.cc:927