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