Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+05816baf74,g1ec0fe41b4+f536777771,g1fd858c14a+a9301854fb,g35bb328faa+fcb1d3bbc8,g4af146b050+a5c07d5b1d,g4d2262a081+6e5fcc2a4e,g53246c7159+fcb1d3bbc8,g56a49b3a55+9c12191793,g5a012ec0e7+3632fc3ff3,g60b5630c4e+ded28b650d,g67b6fd64d1+ed4b5058f4,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g8352419a5c+fcb1d3bbc8,g87b7deb4dc+7b42cf88bf,g8852436030+e5453db6e6,g89139ef638+ed4b5058f4,g8e3bb8577d+d38d73bdbd,g9125e01d80+fcb1d3bbc8,g94187f82dc+ded28b650d,g989de1cb63+ed4b5058f4,g9d31334357+ded28b650d,g9f33ca652e+50a8019d8c,gabe3b4be73+1e0a283bba,gabf8522325+fa80ff7197,gb1101e3267+d9fb1f8026,gb58c049af0+f03b321e39,gb665e3612d+2a0c9e9e84,gb89ab40317+ed4b5058f4,gcf25f946ba+e5453db6e6,gd6cbbdb0b4+bb83cc51f8,gdd1046aedd+ded28b650d,gde0f65d7ad+941d412827,ge278dab8ac+d65b3c2b70,ge410e46f29+ed4b5058f4,gf23fb2af72+b7cae620c0,gf5e32f922b+fcb1d3bbc8,gf67bdafdda+ed4b5058f4,w.2025.16
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
configDictField.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/>.
27
28__all__ = ["ConfigDictField"]
29
30from .callStack import getCallStack, getStackFrame
31from .comparison import compareConfigs, compareScalars, getComparisonName
32from .config import Config, FieldValidationError, _autocast, _joinNamePath, _typeStr
33from .dictField import Dict, DictField
34
35
36class ConfigDict(Dict[str, Config]):
37 """Internal representation of a dictionary of configuration classes.
38
39 Much like `Dict`, `ConfigDict` is a custom `MutableMapper` which tracks
40 the history of changes to any of its items.
41
42 Parameters
43 ----------
44 config : `~lsst.pex.config.Config`
45 Config to use.
46 field : `~lsst.pex.config.ConfigDictField`
47 Field to use.
48 value : `~typing.Any`
49 Value to store in dict.
50 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`, optional
51 Stack frame for history recording. Will be calculated if `None`.
52 label : `str`, optional
53 Label to use for history recording.
54 """
55
56 def __init__(self, config, field, value, at, label):
57 Dict.__init__(self, config, field, value, at, label, setHistory=False)
58 self.history.append(("Dict initialized", at, label))
59
60 def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
61 if self._config._frozen:
62 msg = f"Cannot modify a frozen Config. Attempting to set item at key {k!r} to value {x}"
63 raise FieldValidationError(self._field, self._config, msg)
64
65 # validate keytype
66 k = _autocast(k, self._field.keytype)
67 if type(k) is not self._field.keytype:
68 msg = f"Key {k!r} is of type {_typeStr(k)}, expected type {_typeStr(self._field.keytype)}"
69 raise FieldValidationError(self._field, self._config, msg)
70
71 # validate itemtype
72 dtype = self._field.itemtype
73 if type(x) is not self._field.itemtype and x != self._field.itemtype:
74 msg = (
75 f"Value {x} at key {k!r} is of incorrect type {_typeStr(x)}. "
76 f"Expected type {_typeStr(self._field.itemtype)}"
77 )
78 raise FieldValidationError(self._field, self._config, msg)
79
80 # validate key using keycheck
81 if self._field.keyCheck is not None and not self._field.keyCheck(k):
82 msg = f"Key {k!r} is not a valid key"
83 raise FieldValidationError(self._field, self._config, msg)
84
85 if at is None:
86 at = getCallStack()
87 name = _joinNamePath(self._config._name, self._field.name, k)
88 oldValue = self._dict.get(k, None)
89 if oldValue is None:
90 if x == dtype:
91 self._dict[k] = dtype(__name=name, __at=at, __label=label)
92 else:
93 self._dict[k] = dtype(__name=name, __at=at, __label=label, **x._storage)
94 if setHistory:
95 self.history.append((f"Added item at key {k}", at, label))
96 else:
97 if x == dtype:
98 x = dtype()
99 oldValue.update(__at=at, __label=label, **x._storage)
100 if setHistory:
101 self.history.append((f"Modified item at key {k}", at, label))
102
103 def __delitem__(self, k, at=None, label="delitem"):
104 if at is None:
105 at = getCallStack()
106 Dict.__delitem__(self, k, at, label, False)
107 self.history.append((f"Removed item at key {k}", at, label))
108
109
111 """A configuration field (`~lsst.pex.config.Field` subclass) that is a
112 mapping of keys to `~lsst.pex.config.Config` instances.
113
114 ``ConfigDictField`` behaves like `DictField` except that the
115 ``itemtype`` must be a `~lsst.pex.config.Config` subclass.
116
117 Parameters
118 ----------
119 doc : `str`
120 A description of the configuration field.
121 keytype : {`int`, `float`, `complex`, `bool`, `str`}
122 The type of the mapping keys. All keys must have this type.
123 itemtype : `lsst.pex.config.Config`-type
124 The type of the values in the mapping. This must be
125 `~lsst.pex.config.Config` or a subclass.
126 default : optional
127 Unknown.
128 default : ``itemtype``-dtype, optional
129 Default value of this field.
130 optional : `bool`, optional
131 If `True`, this configuration `~lsst.pex.config.Field` is *optional*.
132 Default is `True`.
133 dictCheck : `~collections.abc.Callable` or `None`, optional
134 Callable to check a dict.
135 keyCheck : `~collections.abc.Callable` or `None`, optional
136 Callable to check a key.
137 itemCheck : `~collections.abc.Callable` or `None`, optional
138 Callable to check an item.
139 deprecated : None or `str`, optional
140 A description of why this Field is deprecated, including removal date.
141 If not None, the string is appended to the docstring for this Field.
142
143 Raises
144 ------
145 ValueError
146 Raised if the inputs are invalid:
147
148 - ``keytype`` or ``itemtype`` arguments are not supported types
149 (members of `ConfigDictField.supportedTypes`.
150 - ``dictCheck``, ``keyCheck`` or ``itemCheck`` is not a callable
151 function.
152
153 See Also
154 --------
155 ChoiceField
156 ConfigChoiceField
157 ConfigField
158 ConfigurableField
159 DictField
160 Field
161 ListField
162 RangeField
163 RegistryField
164
165 Notes
166 -----
167 You can use ``ConfigDictField`` to create name-to-config mappings. One use
168 case is for configuring mappings for dataset types in a Butler. In this
169 case, the dataset type names are arbitrary and user-selected while the
170 mapping configurations are known and fixed.
171 """
172
173 DictClass = ConfigDict
174
176 self,
177 doc,
178 keytype,
179 itemtype,
180 default=None,
181 optional=False,
182 dictCheck=None,
183 keyCheck=None,
184 itemCheck=None,
185 deprecated=None,
186 ):
187 source = getStackFrame()
188 self._setup(
189 doc=doc,
190 dtype=ConfigDict,
191 default=default,
192 check=None,
193 optional=optional,
194 source=source,
195 deprecated=deprecated,
196 )
197 if keytype not in self.supportedTypes:
198 raise ValueError(f"'keytype' {_typeStr(keytype)} is not a supported type")
199 elif not issubclass(itemtype, Config):
200 raise ValueError(f"'itemtype' {_typeStr(itemtype)} is not a supported type")
201
202 check_errors = []
203 for name, check in (("dictCheck", dictCheck), ("keyCheck", keyCheck), ("itemCheck", itemCheck)):
204 if check is not None and not callable(check):
205 check_errors.append(name)
206 if check_errors:
207 raise ValueError(f"{', '.join(check_errors)} must be callable")
208
209 self.keytype = keytype
210 self.itemtype = itemtype
211 self.dictCheck = dictCheck
212 self.keyCheck = keyCheck
213 self.itemCheck = itemCheck
214
215 def rename(self, instance):
216 configDict = self.__get__(instance)
217 if configDict is not None:
218 for k in configDict:
219 fullname = _joinNamePath(instance._name, self.name, k)
220 configDict[k]._rename(fullname)
221
222 def validate(self, instance):
223 """Validate the field.
224
225 Parameters
226 ----------
227 instance : `lsst.pex.config.Config`
228 The config instance that contains this field.
229
230 Raises
231 ------
232 lsst.pex.config.FieldValidationError
233 Raised if validation fails for this field.
234
235 Notes
236 -----
237 Individual key checks (``keyCheck``) are applied when each key is added
238 and are not re-checked by this method.
239 """
240 value = self.__get__(instance)
241 if value is not None:
242 for k in value:
243 item = value[k]
244 item.validate()
245 if self.itemCheck is not None and not self.itemCheck(item):
246 msg = f"Item at key {k!r} is not a valid value: {item}"
247 raise FieldValidationError(self, instance, msg)
248 DictField.validate(self, instance)
249
250 def toDict(self, instance):
251 configDict = self.__get__(instance)
252 if configDict is None:
253 return None
254
255 dict_ = {}
256 for k in configDict:
257 dict_[k] = configDict[k].toDict()
258
259 return dict_
260
261 def _collectImports(self, instance, imports):
262 # docstring inherited from Field
263 configDict = self.__get__(instance)
264 if configDict is not None:
265 for v in configDict.values():
266 v._collectImports()
267 imports |= v._imports
268
269 def save(self, outfile, instance):
270 configDict = self.__get__(instance)
271 fullname = _joinNamePath(instance._name, self.name)
272 if configDict is None:
273 outfile.write(f"{fullname}={configDict!r}\n")
274 return
275
276 outfile.write(f"{fullname}={{}}\n")
277 for v in configDict.values():
278 outfile.write(f"{v._name}={_typeStr(v)}()\n")
279 v._save(outfile)
280
281 def freeze(self, instance):
282 configDict = self.__get__(instance)
283 if configDict is not None:
284 for k in configDict:
285 configDict[k].freeze()
286
287 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
288 """Compare two fields for equality.
289
290 Used by `lsst.pex.ConfigDictField.compare`.
291
292 Parameters
293 ----------
294 instance1 : `lsst.pex.config.Config`
295 Left-hand side config instance to compare.
296 instance2 : `lsst.pex.config.Config`
297 Right-hand side config instance to compare.
298 shortcut : `bool`
299 If `True`, this function returns as soon as an inequality if found.
300 rtol : `float`
301 Relative tolerance for floating point comparisons.
302 atol : `float`
303 Absolute tolerance for floating point comparisons.
304 output : callable
305 A callable that takes a string, used (possibly repeatedly) to
306 report inequalities.
307
308 Returns
309 -------
310 isEqual : bool
311 `True` if the fields are equal, `False` otherwise.
312
313 Notes
314 -----
315 Floating point comparisons are performed by `numpy.allclose`.
316 """
317 d1 = getattr(instance1, self.name)
318 d2 = getattr(instance2, self.name)
319 name = getComparisonName(
320 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
321 )
322 if not compareScalars(f"{name} (keys)", set(d1.keys()), set(d2.keys()), output=output):
323 return False
324 equal = True
325 for k, v1 in d1.items():
326 v2 = d2[k]
327 result = compareConfigs(
328 f"{name}[{k!r}]", v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output
329 )
330 if not result and shortcut:
331 return False
332 equal = equal and result
333 return equal
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:706
_setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition config.py:486
_compare(self, instance1, instance2, shortcut, rtol, atol, output)
__init__(self, doc, keytype, itemtype, default=None, optional=False, dictCheck=None, keyCheck=None, itemCheck=None, deprecated=None)
__init__(self, config, field, value, at, label)
__setitem__(self, k, x, at=None, label="setitem", setHistory=True)
__delitem__(self, k, at=None, label="delitem")
compareConfigs(name, c1, c2, shortcut=True, rtol=1e-8, atol=1e-8, output=None)
getComparisonName(name1, name2)
Definition comparison.py:40
compareScalars(name, v1, v2, output, rtol=1e-8, atol=1e-8, dtype=None)
Definition comparison.py:62
_autocast(x, dtype)
Definition config.py:122
_joinNamePath(prefix=None, name=None, index=None)
Definition config.py:107