LSST Applications g00d0e8bbd7+8c5ae1fdc5,g013ef56533+603670b062,g083dd6704c+2e189452a7,g199a45376c+0ba108daf9,g1c5cce2383+bc9f6103a4,g1fd858c14a+cd69ed4fc1,g210f2d0738+c4742f2e9e,g262e1987ae+612fa42d85,g29ae962dfc+83d129e820,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+5eaa884f2a,g47891489e3+e32160a944,g53246c7159+8c5ae1fdc5,g5b326b94bb+dcc56af22d,g64539dfbff+c4742f2e9e,g67b6fd64d1+e32160a944,g74acd417e5+c122e1277d,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g88cb488625+47d24e4084,g89139ef638+e32160a944,g8d7436a09f+d14b4ff40a,g8ea07a8fe4+b212507b11,g90f42f885a+e1755607f3,g97be763408+34be90ab8c,g98df359435+ec1fa61bf1,ga2180abaac+8c5ae1fdc5,ga9e74d7ce9+43ac651df0,gbf99507273+8c5ae1fdc5,gc2a301910b+c4742f2e9e,gca7fc764a6+e32160a944,gd7ef33dd92+e32160a944,gdab6d2f7ff+c122e1277d,gdb1e2cdc75+1b18322db8,ge410e46f29+e32160a944,ge41e95a9f2+c4742f2e9e,geaed405ab2+0d91c11c6d,w.2025.44
LSST Data Management Base Package
Loading...
Searching...
No Matches
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/>.
27from __future__ import annotations
28
29__all__ = ["ConfigDictField"]
30
31from collections.abc import Mapping
32
33from .callStack import StackFrame, getCallStack, getStackFrame
34from .comparison import compareConfigs, compareScalars, getComparisonName
35from .config import Config, FieldValidationError, _autocast, _joinNamePath, _typeStr
36from .dictField import Dict, DictField
37
38
39class ConfigDict(Dict[str, Config]):
40 """Internal representation of a dictionary of configuration classes.
41
42 Much like `Dict`, `ConfigDict` is a custom `MutableMapper` which tracks
43 the history of changes to any of its items.
44
45 Parameters
46 ----------
47 config : `~lsst.pex.config.Config`
48 Config to use.
49 field : `~lsst.pex.config.ConfigDictField`
50 Field to use.
51 value : `~typing.Any`
52 Value to store in dict.
53 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`, optional
54 Stack frame for history recording. Will be calculated if `None`.
55 label : `str`, optional
56 Label to use for history recording.
57 setHistory : `bool`, optional
58 Whether to append to the history record.
59 """
60
62 self,
63 config: Config,
64 field: ConfigDictField,
65 value: Mapping[str, Config] | None,
66 *,
67 at: list[StackFrame] | None,
68 label: str,
69 setHistory: bool = True,
70 ):
71 Dict.__init__(self, config, field, value, at=at, label=label, setHistory=False)
72 if setHistory:
73 self.history.append(("Dict initialized", at, label))
74
75 def _copy(self, config: Config) -> Dict:
76 return type(self)(
77 config,
78 self._field,
79 {k: v._copy() for k, v in self._dict.items()},
80 at=None,
81 label="copy",
82 setHistory=False,
83 )
84
85 def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
86 if self._config._frozen:
87 msg = f"Cannot modify a frozen Config. Attempting to set item at key {k!r} to value {x}"
88 raise FieldValidationError(self._field, self._config, msg)
89
90 # validate keytype
91 k = _autocast(k, self._field.keytype)
92 if type(k) is not self._field.keytype:
93 msg = f"Key {k!r} is of type {_typeStr(k)}, expected type {_typeStr(self._field.keytype)}"
94 raise FieldValidationError(self._field, self._config, msg)
95
96 # validate itemtype
97 dtype = self._field.itemtype
98 if type(x) is not self._field.itemtype and x != self._field.itemtype:
99 msg = (
100 f"Value {x} at key {k!r} is of incorrect type {_typeStr(x)}. "
101 f"Expected type {_typeStr(self._field.itemtype)}"
102 )
103 raise FieldValidationError(self._field, self._config, msg)
104
105 # validate key using keycheck
106 if self._field.keyCheck is not None and not self._field.keyCheck(k):
107 msg = f"Key {k!r} is not a valid key"
108 raise FieldValidationError(self._field, self._config, msg)
109
110 if at is None:
111 at = getCallStack()
112 name = _joinNamePath(self._config._name, self._field.name, k)
113 oldValue = self._dict.get(k, None)
114 if oldValue is None:
115 if x == dtype:
116 self._dict[k] = dtype(__name=name, __at=at, __label=label)
117 else:
118 self._dict[k] = dtype(__name=name, __at=at, __label=label, **x._storage)
119 if setHistory:
120 self.history.append((f"Added item at key {k}", at, label))
121 else:
122 if x == dtype:
123 x = dtype()
124 oldValue.update(__at=at, __label=label, **x._storage)
125 if setHistory:
126 self.history.append((f"Modified item at key {k}", at, label))
127
128 def __delitem__(self, k, at=None, label="delitem"):
129 if at is None:
130 at = getCallStack()
131 Dict.__delitem__(self, k, at, label, False)
132 self.history.append((f"Removed item at key {k}", at, label))
133
134
136 """A configuration field (`~lsst.pex.config.Field` subclass) that is a
137 mapping of keys to `~lsst.pex.config.Config` instances.
138
139 ``ConfigDictField`` behaves like `DictField` except that the
140 ``itemtype`` must be a `~lsst.pex.config.Config` subclass.
141
142 Parameters
143 ----------
144 doc : `str`
145 A description of the configuration field.
146 keytype : {`int`, `float`, `complex`, `bool`, `str`}
147 The type of the mapping keys. All keys must have this type.
148 itemtype : `lsst.pex.config.Config`-type
149 The type of the values in the mapping. This must be
150 `~lsst.pex.config.Config` or a subclass.
151 default : optional
152 Unknown.
153 default : ``itemtype``-dtype, optional
154 Default value of this field.
155 optional : `bool`, optional
156 If `True`, this configuration `~lsst.pex.config.Field` is *optional*.
157 Default is `True`.
158 dictCheck : `~collections.abc.Callable` or `None`, optional
159 Callable to check a dict.
160 keyCheck : `~collections.abc.Callable` or `None`, optional
161 Callable to check a key.
162 itemCheck : `~collections.abc.Callable` or `None`, optional
163 Callable to check an item.
164 deprecated : None or `str`, optional
165 A description of why this Field is deprecated, including removal date.
166 If not None, the string is appended to the docstring for this Field.
167
168 Raises
169 ------
170 ValueError
171 Raised if the inputs are invalid:
172
173 - ``keytype`` or ``itemtype`` arguments are not supported types
174 (members of `ConfigDictField.supportedTypes`.
175 - ``dictCheck``, ``keyCheck`` or ``itemCheck`` is not a callable
176 function.
177
178 See Also
179 --------
180 ChoiceField
181 ConfigChoiceField
182 ConfigField
183 ConfigurableField
184 DictField
185 Field
186 ListField
187 RangeField
188 RegistryField
189
190 Notes
191 -----
192 You can use ``ConfigDictField`` to create name-to-config mappings. One use
193 case is for configuring mappings for dataset types in a Butler. In this
194 case, the dataset type names are arbitrary and user-selected while the
195 mapping configurations are known and fixed.
196 """
197
198 DictClass = ConfigDict
199
201 self,
202 doc,
203 keytype,
204 itemtype,
205 default=None,
206 optional=False,
207 dictCheck=None,
208 keyCheck=None,
209 itemCheck=None,
210 deprecated=None,
211 ):
212 source = getStackFrame()
213 self._setup(
214 doc=doc,
215 dtype=ConfigDict,
216 default=default,
217 check=None,
218 optional=optional,
219 source=source,
220 deprecated=deprecated,
221 )
222 if keytype not in self.supportedTypes:
223 raise ValueError(f"'keytype' {_typeStr(keytype)} is not a supported type")
224 elif not issubclass(itemtype, Config):
225 raise ValueError(f"'itemtype' {_typeStr(itemtype)} is not a supported type")
226
227 check_errors = []
228 for name, check in (("dictCheck", dictCheck), ("keyCheck", keyCheck), ("itemCheck", itemCheck)):
229 if check is not None and not callable(check):
230 check_errors.append(name)
231 if check_errors:
232 raise ValueError(f"{', '.join(check_errors)} must be callable")
233
234 self.keytype = keytype
235 self.itemtype = itemtype
236 self.dictCheck = dictCheck
237 self.keyCheck = keyCheck
238 self.itemCheck = itemCheck
239
240 def rename(self, instance):
241 configDict = self.__get__(instance)
242 if configDict is not None:
243 for k in configDict:
244 fullname = _joinNamePath(instance._name, self.name, k)
245 configDict[k]._rename(fullname)
246
247 def validate(self, instance):
248 """Validate the field.
249
250 Parameters
251 ----------
252 instance : `lsst.pex.config.Config`
253 The config instance that contains this field.
254
255 Raises
256 ------
257 lsst.pex.config.FieldValidationError
258 Raised if validation fails for this field.
259
260 Notes
261 -----
262 Individual key checks (``keyCheck``) are applied when each key is added
263 and are not re-checked by this method.
264 """
265 value = self.__get__(instance)
266 if value is not None:
267 for k in value:
268 item = value[k]
269 item.validate()
270 if self.itemCheck is not None and not self.itemCheck(item):
271 msg = f"Item at key {k!r} is not a valid value: {item}"
272 raise FieldValidationError(self, instance, msg)
273 DictField.validate(self, instance)
274
275 def toDict(self, instance):
276 configDict = self.__get__(instance)
277 if configDict is None:
278 return None
279
280 dict_ = {}
281 for k in configDict:
282 dict_[k] = configDict[k].toDict()
283
284 return dict_
285
286 def _collectImports(self, instance, imports):
287 # docstring inherited from Field
288 configDict = self.__get__(instance)
289 if configDict is not None:
290 for v in configDict.values():
291 v._collectImports()
292 imports |= v._imports
293
294 def save(self, outfile, instance):
295 configDict = self.__get__(instance)
296 fullname = _joinNamePath(instance._name, self.name)
297 if configDict is None:
298 outfile.write(f"{fullname}={configDict!r}\n")
299 return
300
301 outfile.write(f"{fullname}={{}}\n")
302 for v in configDict.values():
303 outfile.write(f"{v._name}={_typeStr(v)}()\n")
304 v._save(outfile)
305
306 def freeze(self, instance):
307 configDict = self.__get__(instance)
308 if configDict is not None:
309 for k in configDict:
310 configDict[k].freeze()
311
312 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
313 """Compare two fields for equality.
314
315 Used by `lsst.pex.ConfigDictField.compare`.
316
317 Parameters
318 ----------
319 instance1 : `lsst.pex.config.Config`
320 Left-hand side config instance to compare.
321 instance2 : `lsst.pex.config.Config`
322 Right-hand side config instance to compare.
323 shortcut : `bool`
324 If `True`, this function returns as soon as an inequality if found.
325 rtol : `float`
326 Relative tolerance for floating point comparisons.
327 atol : `float`
328 Absolute tolerance for floating point comparisons.
329 output : callable
330 A callable that takes a string, used (possibly repeatedly) to
331 report inequalities.
332
333 Returns
334 -------
335 isEqual : bool
336 `True` if the fields are equal, `False` otherwise.
337
338 Notes
339 -----
340 Floating point comparisons are performed by `numpy.allclose`.
341 """
342 d1 = getattr(instance1, self.name)
343 d2 = getattr(instance2, self.name)
344 name = getComparisonName(
345 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
346 )
347 if not compareScalars(f"{name} (keys)", set(d1.keys()), set(d2.keys()), output=output):
348 return False
349 equal = True
350 for k, v1 in d1.items():
351 v2 = d2[k]
352 result = compareConfigs(
353 f"{name}[{k!r}]", v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output
354 )
355 if not result and shortcut:
356 return False
357 equal = equal and result
358 return equal
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:714
_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 config, ConfigDictField field, Mapping[str, Config]|None value, *, list[StackFrame]|None at, str label, bool setHistory=True)
__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