LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
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
43 `~lsst.pex.config.ConfigChoiceField` objects.
44
45 Parameters
46 ----------
47 dict_ : `ConfigInstanceDict`
48 The dictionary of instantiated configs.
49 value : `~typing.Any`
50 The selected key.
51 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`, optional
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((f"Set selection to {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
99 Parameters
100 ----------
101 value : `~typing.Any`
102 The selected key.
103 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`,\
104 optional
105 Stack frames for history recording.
106 """
107 if self._config_config._frozen:
108 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
109
110 if at is None:
111 at = getCallStack()
112
113 if value not in self._dict:
114 # invoke __getitem__ to make sure it's present
115 self._dict.__getitem__(value, at=at)
116
117 self.__history.append((f"added {value} to selection", at, "selection"))
118 self._set.add(value)
119
120 def discard(self, value, at=None):
121 """Discard a value from the selected set.
122
123 Parameters
124 ----------
125 value : `~typing.Any`
126 The selected key.
127 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`,\
128 optional
129 Stack frames for history recording.
130 """
131 if self._config_config._frozen:
132 raise FieldValidationError(self._field, self._config_config, "Cannot modify a frozen Config")
133
134 if value not in self._dict:
135 return
136
137 if at is None:
138 at = getCallStack()
139
140 self.__history.append((f"removed {value} from selection", at, "selection"))
141 self._set.discard(value)
142
143 def __len__(self):
144 return len(self._set)
145
146 def __iter__(self):
147 return iter(self._set)
148
149 def __contains__(self, value):
150 return value in self._set
151
152 def __repr__(self):
153 return repr(list(self._set))
154
155 def __str__(self):
156 return str(list(self._set))
157
158 def __reduce__(self):
160 f"Proxy container for config field {self._field.name} cannot "
161 "be pickled; it should be converted to a built-in container before "
162 "being assigned to other objects or variables."
163 )
164
165
166class ConfigInstanceDict(collections.abc.Mapping[str, Config]):
167 """Dictionary of instantiated configs, used to populate a
168 `~lsst.pex.config.ConfigChoiceField`.
169
170 Parameters
171 ----------
172 config : `lsst.pex.config.Config`
173 A configuration instance.
174 field : `lsst.pex.config.Field`-type
175 A configuration field. Note that the `lsst.pex.config.Field.fieldmap`
176 attribute must provide key-based access to configuration classes,
177 (that is, ``typemap[name]``).
178 """
179
180 def __init__(self, config, field):
181 collections.abc.Mapping.__init__(self)
182 self._dict = {}
183 self._selection = None
184 self._config = config
185 self._field = field
186 self._history_history = config._history.setdefault(field.name, [])
187 self.__doc__ = field.doc
188 self._typemap = None
189
190 @property
191 def types(self):
192 return self._typemap if self._typemap is not None else self._field.typemap
193
194 def __contains__(self, k):
195 return k in self.typestypes
196
197 def __len__(self):
198 return len(self.typestypes)
199
200 def __iter__(self):
201 return iter(self.typestypes)
202
203 def _setSelection(self, value, at=None, label="assignment"):
204 if self._config._frozen:
205 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
206
207 if at is None:
208 at = getCallStack(1)
209
210 if value is None:
211 self._selection = None
212 elif self._field.multi:
213 self._selection = SelectionSet(self, value, setHistory=False)
214 else:
215 if value not in self._dict:
216 self.__getitem__(value, at=at) # just invoke __getitem__ to make sure it's present
217 self._selection = value
218 self._history_history.append((value, at, label))
219
220 def _getNames(self):
221 if not self._field.multi:
223 self._field, self._config, "Single-selection field has no attribute 'names'"
224 )
225 return self._selection
226
227 def _setNames(self, value):
228 if not self._field.multi:
230 self._field, self._config, "Single-selection field has no attribute 'names'"
231 )
232 self._setSelection(value)
233
234 def _delNames(self):
235 if not self._field.multi:
237 self._field, self._config, "Single-selection field has no attribute 'names'"
238 )
239 self._selection = None
240
241 def _getName(self):
242 if self._field.multi:
244 self._field, self._config, "Multi-selection field has no attribute 'name'"
245 )
246 return self._selection
247
248 def _setName(self, value):
249 if self._field.multi:
251 self._field, self._config, "Multi-selection field has no attribute 'name'"
252 )
253 self._setSelection(value)
254
255 def _delName(self):
256 if self._field.multi:
258 self._field, self._config, "Multi-selection field has no attribute 'name'"
259 )
260 self._selection = None
261
262 names = property(_getNames, _setNames, _delNames)
263 """List of names of active items in a multi-selection
264 ``ConfigInstanceDict``. Disabled in a single-selection ``_Registry``; use
265 the `name` attribute instead.
266 """
267
268 name = property(_getName, _setName, _delName)
269 """Name of the active item in a single-selection ``ConfigInstanceDict``.
270 Disabled in a multi-selection ``_Registry``; use the ``names`` attribute
271 instead.
272 """
273
274 def _getActive(self):
275 if self._selection is None:
276 return None
277
278 if self._field.multi:
279 return [self[c] for c in self._selection]
280 else:
281 return self[self._selection]
282
283 active = property(_getActive)
284 """The selected items.
285
286 For multi-selection, this is equivalent to: ``[self[name] for name in
287 self.names]``. For single-selection, this is equivalent to: ``self[name]``.
288 """
289
290 def __getitem__(self, k, at=None, label="default"):
291 try:
292 value = self._dict[k]
293 except KeyError:
294 try:
295 dtype = self.typestypes[k]
296 except Exception:
298 self._field, self._config, f"Unknown key {k!r} in Registry/ConfigChoiceField"
299 )
300 name = _joinNamePath(self._config._name, self._field.name, k)
301 if at is None:
302 at = getCallStack()
303 at.insert(0, dtype._source)
304 value = self._dict.setdefault(k, dtype(__name=name, __at=at, __label=label))
305 return value
306
307 def __setitem__(self, k, value, at=None, label="assignment"):
308 if self._config._frozen:
309 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
310
311 try:
312 dtype = self.typestypes[k]
313 except Exception:
314 raise FieldValidationError(self._field, self._config, f"Unknown key {k!r}")
315
316 if value != dtype and type(value) is not dtype:
317 msg = (
318 f"Value {value} at key {k} is of incorrect type {_typeStr(value)}. "
319 f"Expected type {_typeStr(dtype)}"
320 )
321 raise FieldValidationError(self._field, self._config, msg)
322
323 if at is None:
324 at = getCallStack()
325 name = _joinNamePath(self._config._name, self._field.name, k)
326 oldValue = self._dict.get(k, None)
327 if oldValue is None:
328 if value == dtype:
329 self._dict[k] = value(__name=name, __at=at, __label=label)
330 else:
331 self._dict[k] = dtype(__name=name, __at=at, __label=label, **value._storage)
332 else:
333 if value == dtype:
334 value = value()
335 oldValue.update(__at=at, __label=label, **value._storage)
336
337 def _rename(self, fullname):
338 for k, v in self._dict.items():
339 v._rename(_joinNamePath(name=fullname, index=k))
340
341 def __setattr__(self, attr, value, at=None, label="assignment"):
342 if hasattr(getattr(self.__class____class__, attr, None), "__set__"):
343 # This allows properties to work.
344 object.__setattr__(self, attr, value)
345 elif attr in self.__dict__ or attr in [
346 "_history",
347 "_field",
348 "_config",
349 "_dict",
350 "_selection",
351 "__doc__",
352 "_typemap",
353 ]:
354 # This allows specific private attributes to work.
355 object.__setattr__(self, attr, value)
356 else:
357 # We throw everything else.
358 msg = f"{_typeStr(self._field)} has no attribute {attr}"
359 raise FieldValidationError(self._field, self._config, msg)
360
361 def freeze(self):
362 """Freeze the config.
363
364 Invoking this freeze method will create a local copy of the field
365 attribute's typemap. This decouples this instance dict from the
366 underlying objects type map ensuring that and subsequent changes to the
367 typemap will not be reflected in this instance (i.e imports adding
368 additional registry entries).
369 """
370 if self._typemap is None:
371 self._typemap = copy.deepcopy(self.typestypes)
372
373 def __reduce__(self):
375 f"Proxy container for config field {self._field.name} cannot "
376 "be pickled; it should be converted to a built-in container before "
377 "being assigned to other objects or variables."
378 )
379
380
381class ConfigChoiceField(Field[ConfigInstanceDict]):
382 """A configuration field (`~lsst.pex.config.Field` subclass) that allows a
383 user to choose from a set of `~lsst.pex.config.Config` types.
384
385 Parameters
386 ----------
387 doc : `str`
388 Documentation string for the field.
389 typemap : `dict`-like
390 A mapping between keys and `~lsst.pex.config.Config`-types as values.
391 See *Examples* for details.
392 default : `str`, optional
393 The default configuration name.
394 optional : `bool`, optional
395 When `False`, `lsst.pex.config.Config.validate` will fail if the
396 field's value is `None`.
397 multi : `bool`, optional
398 If `True`, the field allows multiple selections. In this case, set the
399 selections by assigning a sequence to the ``names`` attribute of the
400 field.
401
402 If `False`, the field allows only a single selection. In this case,
403 set the active config by assigning the config's key from the
404 ``typemap`` to the field's ``name`` attribute (see *Examples*).
405 deprecated : None or `str`, optional
406 A description of why this Field is deprecated, including removal date.
407 If not None, the string is appended to the docstring for this Field.
408
409 See Also
410 --------
411 ChoiceField
412 ConfigDictField
413 ConfigField
414 ConfigurableField
415 DictField
416 Field
417 ListField
418 RangeField
419 RegistryField
420
421 Notes
422 -----
423 ``ConfigChoiceField`` instances can allow either single selections or
424 multiple selections, depending on the ``multi`` parameter. For
425 single-selection fields, set the selection with the ``name`` attribute.
426 For multi-selection fields, set the selection though the ``names``
427 attribute.
428
429 This field is validated only against the active selection. If the
430 ``active`` attribute is `None` and the field is not optional, validation
431 will fail.
432
433 When saving a configuration with a ``ConfigChoiceField``, the entire set is
434 saved, as well as the active selection.
435
436 Examples
437 --------
438 While the ``typemap`` is shared by all instances of the field, each
439 instance of the field has its own instance of a particular sub-config type.
440
441 For example, ``AaaConfig`` is a config object
442
443 >>> from lsst.pex.config import Config, ConfigChoiceField, Field
444 >>> class AaaConfig(Config):
445 ... somefield = Field("doc", int)
446 ...
447
448 The ``MyConfig`` config has a ``ConfigChoiceField`` field called ``choice``
449 that maps the ``AaaConfig`` type to the ``"AAA"`` key:
450
451 >>> TYPEMAP = {"AAA", AaaConfig}
452 >>> class MyConfig(Config):
453 ... choice = ConfigChoiceField("doc for choice", TYPEMAP)
454 ...
455
456 Creating an instance of ``MyConfig``:
457
458 >>> instance = MyConfig()
459
460 Setting value of the field ``somefield`` on the "AAA" key of the ``choice``
461 field:
462
463 >>> instance.choice['AAA'].somefield = 5
464
465 **Selecting the active configuration**
466
467 Make the ``"AAA"`` key the active configuration value for the ``choice``
468 field:
469
470 >>> instance.choice = "AAA"
471
472 Alternatively, the last line can be written:
473
474 >>> instance.choice.name = "AAA"
475
476 (If the config instance allows multiple selections, you'd assign a sequence
477 to the ``names`` attribute instead.)
478
479 ``ConfigChoiceField`` instances also allow multiple values of the same
480 type:
481
482 >>> TYPEMAP["CCC"] = AaaConfig
483 >>> TYPEMAP["BBB"] = AaaConfig
484 """
485
486 instanceDictClass = ConfigInstanceDict
487
488 def __init__(self, doc, typemap, default=None, optional=False, multi=False, deprecated=None):
489 source = getStackFrame()
490 self._setup(
491 doc=doc,
492 dtype=self.instanceDictClass,
493 default=default,
494 check=None,
495 optional=optional,
496 source=source,
497 deprecated=deprecated,
498 )
499 self.typemap = typemap
500 self.multi = multi
501
502 def __class_getitem__(cls, params: tuple[type, ...] | type | ForwardRef):
503 raise ValueError("ConfigChoiceField does not support typing argument")
504
505 def _getOrMake(self, instance, label="default"):
506 instanceDict = instance._storage.get(self.namenamenamename)
507 if instanceDict is None:
508 at = getCallStack(1)
509 instanceDict = self.dtype(instance, self)
510 instanceDict.__doc__ = self.doc
511 instance._storage[self.namenamenamename] = instanceDict
512 history = instance._history.setdefault(self.namenamenamename, [])
513 history.append(("Initialized from defaults", at, label))
514
515 return instanceDict
516
517 @overload
519 self, instance: None, owner: Any = None, at: Any = None, label: str = "default"
520 ) -> ConfigChoiceField: ...
521
522 @overload
524 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default"
525 ) -> ConfigInstanceDict: ...
526
527 def __get__(self, instance, owner=None, at=None, label="default"):
528 if instance is None or not isinstance(instance, Config):
529 return self
530 else:
531 return self._getOrMake(instance)
532
534 self, instance: Config, value: ConfigInstanceDict | None, at: Any = None, label: str = "assignment"
535 ) -> None:
536 if instance._frozen:
537 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
538 if at is None:
539 at = getCallStack()
540 instanceDict = self._getOrMake(instance)
541 if isinstance(value, self.instanceDictClass):
542 for k, v in value.items():
543 instanceDict.__setitem__(k, v, at=at, label=label)
544 instanceDict._setSelection(value._selection, at=at, label=label)
545
546 else:
547 instanceDict._setSelection(value, at=at, label=label)
548
549 def rename(self, instance):
550 instanceDict = self.__get____get____get____get____get____get__(instance)
551 fullname = _joinNamePath(instance._name, self.namenamenamename)
552 instanceDict._rename(fullname)
553
554 def validate(self, instance):
555 instanceDict = self.__get____get____get____get____get____get__(instance)
556 if instanceDict.active is None and not self.optional:
557 msg = "Required field cannot be None"
558 raise FieldValidationError(self, instance, msg)
559 elif instanceDict.active is not None:
560 if self.multi:
561 for a in instanceDict.active:
562 a.validate()
563 else:
564 instanceDict.active.validate()
565
566 def toDict(self, instance):
567 instanceDict = self.__get____get____get____get____get____get__(instance)
568
569 dict_ = {}
570 if self.multi:
571 dict_["names"] = instanceDict.names
572 else:
573 dict_["name"] = instanceDict.name
574
575 values = {}
576 for k, v in instanceDict.items():
577 values[k] = v.toDict()
578 dict_["values"] = values
579
580 return dict_
581
582 def freeze(self, instance):
583 instanceDict = self.__get____get____get____get____get____get__(instance)
584 instanceDict.freeze()
585 for v in instanceDict.values():
586 v.freeze()
587
588 def _collectImports(self, instance, imports):
589 instanceDict = self.__get____get____get____get____get____get__(instance)
590 for config in instanceDict.values():
591 config._collectImports()
592 imports |= config._imports
593
594 def save(self, outfile, instance):
595 instanceDict = self.__get____get____get____get____get____get__(instance)
596 fullname = _joinNamePath(instance._name, self.namenamenamename)
597 for v in instanceDict.values():
598 v._save(outfile)
599 if self.multi:
600 outfile.write(f"{fullname}.names={sorted(instanceDict.names)!r}\n")
601 else:
602 outfile.write(f"{fullname}.name={instanceDict.name!r}\n")
603
604 def __deepcopy__(self, memo):
605 """Customize deep-copying, because we always want a reference to the
606 original typemap.
607
608 WARNING: this must be overridden by subclasses if they change the
609 constructor signature!
610 """
611 other = type(self)(
612 doc=self.doc,
613 typemap=self.typemap,
614 default=copy.deepcopy(self.default),
615 optional=self.optional,
616 multi=self.multi,
617 )
618 other.source = self.source
619 return other
620
621 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
622 """Compare two fields for equality.
623
624 Used by `lsst.pex.ConfigChoiceField.compare`.
625
626 Parameters
627 ----------
628 instance1 : `lsst.pex.config.Config`
629 Left-hand side config instance to compare.
630 instance2 : `lsst.pex.config.Config`
631 Right-hand side config instance to compare.
632 shortcut : `bool`
633 If `True`, this function returns as soon as an inequality if found.
634 rtol : `float`
635 Relative tolerance for floating point comparisons.
636 atol : `float`
637 Absolute tolerance for floating point comparisons.
638 output : callable
639 A callable that takes a string, used (possibly repeatedly) to
640 report inequalities.
641
642 Returns
643 -------
644 isEqual : bool
645 `True` if the fields are equal, `False` otherwise.
646
647 Notes
648 -----
649 Only the selected configurations are compared, as the parameters of any
650 others do not matter.
651
652 Floating point comparisons are performed by `numpy.allclose`.
653 """
654 d1 = getattr(instance1, self.namenamenamename)
655 d2 = getattr(instance2, self.namenamenamename)
656 name = getComparisonName(
657 _joinNamePath(instance1._name, self.namenamenamename), _joinNamePath(instance2._name, self.namenamenamename)
658 )
659 if not compareScalars(f"selection for {name}", d1._selection, d2._selection, output=output):
660 return False
661 if d1._selection is None:
662 return True
663 if self.multi:
664 nested = [(k, d1[k], d2[k]) for k in d1._selection]
665 else:
666 nested = [(d1._selection, d1[d1._selection], d2[d1._selection])]
667 equal = True
668 for k, c1, c2 in nested:
669 result = compareConfigs(
670 f"{name}[{k!r}]", c1, c2, shortcut=shortcut, rtol=rtol, atol=atol, output=output
671 )
672 if not result and shortcut:
673 return False
674 equal = equal and result
675 return equal
std::vector< SchemaItem< Flag > > * items
__get__(self, instance, owner=None, at=None, label="default")
Definition config.py:707
FieldTypeVar __get__(self, Config instance, Any owner=None, Any at=None, str label="default")
Definition config.py:705
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:700
_setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition config.py:480
__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)