Coverage for python/lsst/pex/config/configDictField.py: 19%
117 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-01 12:22 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-01 12:22 +0000
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/>.
28__all__ = ["ConfigDictField"]
30from .callStack import getCallStack, getStackFrame
31from .comparison import compareConfigs, compareScalars, getComparisonName
32from .config import Config, FieldValidationError, _autocast, _joinNamePath, _typeStr
33from .dictField import Dict, DictField
36class ConfigDict(Dict[str, Config]):
37 """Internal representation of a dictionary of configuration classes.
39 Much like `Dict`, `ConfigDict` is a custom `MutableMapper` which tracks
40 the history of changes to any of its items.
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 """
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))
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)
65 # validate keytype
66 k = _autocast(k, self._field.keytype)
67 if type(k) is not self._field.keytype:
68 msg = "Key {!r} is of type {}, expected type {}".format(
69 k, _typeStr(k), _typeStr(self._field.keytype)
70 )
71 raise FieldValidationError(self._field, self._config, msg)
73 # validate itemtype
74 dtype = self._field.itemtype
75 if type(x) is not self._field.itemtype and x != self._field.itemtype:
76 msg = "Value {} at key {!r} is of incorrect type {}. Expected type {}".format(
77 x,
78 k,
79 _typeStr(x),
80 _typeStr(self._field.itemtype),
81 )
82 raise FieldValidationError(self._field, self._config, msg)
84 if at is None:
85 at = getCallStack()
86 name = _joinNamePath(self._config._name, self._field.name, k)
87 oldValue = self._dict.get(k, None)
88 if oldValue is None:
89 if x == dtype:
90 self._dict[k] = dtype(__name=name, __at=at, __label=label)
91 else:
92 self._dict[k] = dtype(__name=name, __at=at, __label=label, **x._storage)
93 if setHistory:
94 self.history.append(("Added item at key %s" % k, at, label))
95 else:
96 if x == dtype:
97 x = dtype()
98 oldValue.update(__at=at, __label=label, **x._storage)
99 if setHistory:
100 self.history.append(("Modified item at key %s" % k, at, label))
102 def __delitem__(self, k, at=None, label="delitem"):
103 if at is None:
104 at = getCallStack()
105 Dict.__delitem__(self, k, at, label, False)
106 self.history.append(("Removed item at key %s" % k, at, label))
109class ConfigDictField(DictField):
110 """A configuration field (`~lsst.pex.config.Field` subclass) that is a
111 mapping of keys to `~lsst.pex.config.Config` instances.
113 ``ConfigDictField`` behaves like `DictField` except that the
114 ``itemtype`` must be a `~lsst.pex.config.Config` subclass.
116 Parameters
117 ----------
118 doc : `str`
119 A description of the configuration field.
120 keytype : {`int`, `float`, `complex`, `bool`, `str`}
121 The type of the mapping keys. All keys must have this type.
122 itemtype : `lsst.pex.config.Config`-type
123 The type of the values in the mapping. This must be
124 `~lsst.pex.config.Config` or a subclass.
125 default : optional
126 Unknown.
127 default : ``itemtype``-dtype, optional
128 Default value of this field.
129 optional : `bool`, optional
130 If `True`, this configuration `~lsst.pex.config.Field` is *optional*.
131 Default is `True`.
132 dictCheck : `~collections.abc.Callable` or `None`, optional
133 Callable to check a dict.
134 itemCheck : `~collections.abc.Callable` or `None`, optional
135 Callable to check an item.
136 deprecated : None or `str`, optional
137 A description of why this Field is deprecated, including removal date.
138 If not None, the string is appended to the docstring for this Field.
140 Raises
141 ------
142 ValueError
143 Raised if the inputs are invalid:
145 - ``keytype`` or ``itemtype`` arguments are not supported types
146 (members of `ConfigDictField.supportedTypes`.
147 - ``dictCheck`` or ``itemCheck`` is not a callable function.
149 See Also
150 --------
151 ChoiceField
152 ConfigChoiceField
153 ConfigField
154 ConfigurableField
155 DictField
156 Field
157 ListField
158 RangeField
159 RegistryField
161 Notes
162 -----
163 You can use ``ConfigDictField`` to create name-to-config mappings. One use
164 case is for configuring mappings for dataset types in a Butler. In this
165 case, the dataset type names are arbitrary and user-selected while the
166 mapping configurations are known and fixed.
167 """
169 DictClass = ConfigDict
171 def __init__(
172 self,
173 doc,
174 keytype,
175 itemtype,
176 default=None,
177 optional=False,
178 dictCheck=None,
179 itemCheck=None,
180 deprecated=None,
181 ):
182 source = getStackFrame()
183 self._setup(
184 doc=doc,
185 dtype=ConfigDict,
186 default=default,
187 check=None,
188 optional=optional,
189 source=source,
190 deprecated=deprecated,
191 )
192 if keytype not in self.supportedTypes: 192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true
193 raise ValueError("'keytype' %s is not a supported type" % _typeStr(keytype))
194 elif not issubclass(itemtype, Config): 194 ↛ 195line 194 didn't jump to line 195, because the condition on line 194 was never true
195 raise ValueError("'itemtype' %s is not a supported type" % _typeStr(itemtype))
196 if dictCheck is not None and not hasattr(dictCheck, "__call__"): 196 ↛ 197line 196 didn't jump to line 197, because the condition on line 196 was never true
197 raise ValueError("'dictCheck' must be callable")
198 if itemCheck is not None and not hasattr(itemCheck, "__call__"): 198 ↛ 199line 198 didn't jump to line 199, because the condition on line 198 was never true
199 raise ValueError("'itemCheck' must be callable")
201 self.keytype = keytype
202 self.itemtype = itemtype
203 self.dictCheck = dictCheck
204 self.itemCheck = itemCheck
206 def rename(self, instance):
207 configDict = self.__get__(instance)
208 if configDict is not None:
209 for k in configDict:
210 fullname = _joinNamePath(instance._name, self.name, k)
211 configDict[k]._rename(fullname)
213 def validate(self, instance):
214 value = self.__get__(instance)
215 if value is not None:
216 for k in value:
217 item = value[k]
218 item.validate()
219 if self.itemCheck is not None and not self.itemCheck(item):
220 msg = f"Item at key {k!r} is not a valid value: {item}"
221 raise FieldValidationError(self, instance, msg)
222 DictField.validate(self, instance)
224 def toDict(self, instance):
225 configDict = self.__get__(instance)
226 if configDict is None:
227 return None
229 dict_ = {}
230 for k in configDict:
231 dict_[k] = configDict[k].toDict()
233 return dict_
235 def _collectImports(self, instance, imports):
236 # docstring inherited from Field
237 configDict = self.__get__(instance)
238 if configDict is not None:
239 for v in configDict.values():
240 v._collectImports()
241 imports |= v._imports
243 def save(self, outfile, instance):
244 configDict = self.__get__(instance)
245 fullname = _joinNamePath(instance._name, self.name)
246 if configDict is None:
247 outfile.write(f"{fullname}={configDict!r}\n")
248 return
250 outfile.write(f"{fullname}={{}}\n")
251 for v in configDict.values():
252 outfile.write(f"{v._name}={_typeStr(v)}()\n")
253 v._save(outfile)
255 def freeze(self, instance):
256 configDict = self.__get__(instance)
257 if configDict is not None:
258 for k in configDict:
259 configDict[k].freeze()
261 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
262 """Compare two fields for equality.
264 Used by `lsst.pex.ConfigDictField.compare`.
266 Parameters
267 ----------
268 instance1 : `lsst.pex.config.Config`
269 Left-hand side config instance to compare.
270 instance2 : `lsst.pex.config.Config`
271 Right-hand side config instance to compare.
272 shortcut : `bool`
273 If `True`, this function returns as soon as an inequality if found.
274 rtol : `float`
275 Relative tolerance for floating point comparisons.
276 atol : `float`
277 Absolute tolerance for floating point comparisons.
278 output : callable
279 A callable that takes a string, used (possibly repeatedly) to
280 report inequalities.
282 Returns
283 -------
284 isEqual : bool
285 `True` if the fields are equal, `False` otherwise.
287 Notes
288 -----
289 Floating point comparisons are performed by `numpy.allclose`.
290 """
291 d1 = getattr(instance1, self.name)
292 d2 = getattr(instance2, self.name)
293 name = getComparisonName(
294 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
295 )
296 if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
297 return False
298 equal = True
299 for k, v1 in d1.items():
300 v2 = d2[k]
301 result = compareConfigs(
302 f"{name}[{k!r}]", v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output
303 )
304 if not result and shortcut:
305 return False
306 equal = equal and result
307 return equal