Coverage for python/lsst/pex/config/configDictField.py: 21%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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):
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.
41 """
43 def __init__(self, config, field, value, at, label):
44 Dict.__init__(self, config, field, value, at, label, setHistory=False)
45 self.history.append(("Dict initialized", at, label))
47 def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
48 if self._config._frozen:
49 msg = "Cannot modify a frozen Config. Attempting to set item at key %r to value %s" % (k, x)
50 raise FieldValidationError(self._field, self._config, msg)
52 # validate keytype
53 k = _autocast(k, self._field.keytype)
54 if type(k) != self._field.keytype:
55 msg = "Key %r is of type %s, expected type %s" % (k, _typeStr(k), _typeStr(self._field.keytype))
56 raise FieldValidationError(self._field, self._config, msg)
58 # validate itemtype
59 dtype = self._field.itemtype
60 if type(x) != self._field.itemtype and x != self._field.itemtype:
61 msg = "Value %s at key %r is of incorrect type %s. Expected type %s" % (
62 x,
63 k,
64 _typeStr(x),
65 _typeStr(self._field.itemtype),
66 )
67 raise FieldValidationError(self._field, self._config, msg)
69 if at is None:
70 at = getCallStack()
71 name = _joinNamePath(self._config._name, self._field.name, k)
72 oldValue = self._dict.get(k, None)
73 if oldValue is None:
74 if x == dtype:
75 self._dict[k] = dtype(__name=name, __at=at, __label=label)
76 else:
77 self._dict[k] = dtype(__name=name, __at=at, __label=label, **x._storage)
78 if setHistory:
79 self.history.append(("Added item at key %s" % k, at, label))
80 else:
81 if x == dtype:
82 x = dtype()
83 oldValue.update(__at=at, __label=label, **x._storage)
84 if setHistory:
85 self.history.append(("Modified item at key %s" % k, at, label))
87 def __delitem__(self, k, at=None, label="delitem"):
88 if at is None:
89 at = getCallStack()
90 Dict.__delitem__(self, k, at, label, False)
91 self.history.append(("Removed item at key %s" % k, at, label))
94class ConfigDictField(DictField):
95 """A configuration field (`~lsst.pex.config.Field` subclass) that is a
96 mapping of keys to `~lsst.pex.config.Config` instances.
98 ``ConfigDictField`` behaves like `DictField` except that the
99 ``itemtype`` must be a `~lsst.pex.config.Config` subclass.
101 Parameters
102 ----------
103 doc : `str`
104 A description of the configuration field.
105 keytype : {`int`, `float`, `complex`, `bool`, `str`}
106 The type of the mapping keys. All keys must have this type.
107 itemtype : `lsst.pex.config.Config`-type
108 The type of the values in the mapping. This must be
109 `~lsst.pex.config.Config` or a subclass.
110 default : optional
111 Unknown.
112 default : ``itemtype``-dtype, optional
113 Default value of this field.
114 optional : `bool`, optional
115 If `True`, this configuration `~lsst.pex.config.Field` is *optional*.
116 Default is `True`.
117 deprecated : None or `str`, optional
118 A description of why this Field is deprecated, including removal date.
119 If not None, the string is appended to the docstring for this Field.
121 Raises
122 ------
123 ValueError
124 Raised if the inputs are invalid:
126 - ``keytype`` or ``itemtype`` arguments are not supported types
127 (members of `ConfigDictField.supportedTypes`.
128 - ``dictCheck`` or ``itemCheck`` is not a callable function.
130 See also
131 --------
132 ChoiceField
133 ConfigChoiceField
134 ConfigField
135 ConfigurableField
136 DictField
137 Field
138 ListField
139 RangeField
140 RegistryField
142 Notes
143 -----
144 You can use ``ConfigDictField`` to create name-to-config mappings. One use
145 case is for configuring mappings for dataset types in a Butler. In this
146 case, the dataset type names are arbitrary and user-selected while the
147 mapping configurations are known and fixed.
148 """
150 DictClass = ConfigDict
152 def __init__(
153 self,
154 doc,
155 keytype,
156 itemtype,
157 default=None,
158 optional=False,
159 dictCheck=None,
160 itemCheck=None,
161 deprecated=None,
162 ):
163 source = getStackFrame()
164 self._setup(
165 doc=doc,
166 dtype=ConfigDict,
167 default=default,
168 check=None,
169 optional=optional,
170 source=source,
171 deprecated=deprecated,
172 )
173 if keytype not in self.supportedTypes: 173 ↛ 174line 173 didn't jump to line 174, because the condition on line 173 was never true
174 raise ValueError("'keytype' %s is not a supported type" % _typeStr(keytype))
175 elif not issubclass(itemtype, Config): 175 ↛ 176line 175 didn't jump to line 176, because the condition on line 175 was never true
176 raise ValueError("'itemtype' %s is not a supported type" % _typeStr(itemtype))
177 if dictCheck is not None and not hasattr(dictCheck, "__call__"): 177 ↛ 178line 177 didn't jump to line 178, because the condition on line 177 was never true
178 raise ValueError("'dictCheck' must be callable")
179 if itemCheck is not None and not hasattr(itemCheck, "__call__"): 179 ↛ 180line 179 didn't jump to line 180, because the condition on line 179 was never true
180 raise ValueError("'itemCheck' must be callable")
182 self.keytype = keytype
183 self.itemtype = itemtype
184 self.dictCheck = dictCheck
185 self.itemCheck = itemCheck
187 def rename(self, instance):
188 configDict = self.__get__(instance)
189 if configDict is not None:
190 for k in configDict:
191 fullname = _joinNamePath(instance._name, self.name, k)
192 configDict[k]._rename(fullname)
194 def validate(self, instance):
195 value = self.__get__(instance)
196 if value is not None:
197 for k in value:
198 item = value[k]
199 item.validate()
200 if self.itemCheck is not None and not self.itemCheck(item):
201 msg = "Item at key %r is not a valid value: %s" % (k, item)
202 raise FieldValidationError(self, instance, msg)
203 DictField.validate(self, instance)
205 def toDict(self, instance):
206 configDict = self.__get__(instance)
207 if configDict is None:
208 return None
210 dict_ = {}
211 for k in configDict:
212 dict_[k] = configDict[k].toDict()
214 return dict_
216 def save(self, outfile, instance):
217 configDict = self.__get__(instance)
218 fullname = _joinNamePath(instance._name, self.name)
219 if configDict is None:
220 outfile.write("{}={!r}\n".format(fullname, configDict))
221 return
223 outfile.write("{}={!r}\n".format(fullname, {}))
224 for v in configDict.values():
225 outfile.write("{}={}()\n".format(v._name, _typeStr(v)))
226 v._save(outfile)
228 def freeze(self, instance):
229 configDict = self.__get__(instance)
230 if configDict is not None:
231 for k in configDict:
232 configDict[k].freeze()
234 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
235 """Compare two fields for equality.
237 Used by `lsst.pex.ConfigDictField.compare`.
239 Parameters
240 ----------
241 instance1 : `lsst.pex.config.Config`
242 Left-hand side config instance to compare.
243 instance2 : `lsst.pex.config.Config`
244 Right-hand side config instance to compare.
245 shortcut : `bool`
246 If `True`, this function returns as soon as an inequality if found.
247 rtol : `float`
248 Relative tolerance for floating point comparisons.
249 atol : `float`
250 Absolute tolerance for floating point comparisons.
251 output : callable
252 A callable that takes a string, used (possibly repeatedly) to
253 report inequalities.
255 Returns
256 -------
257 isEqual : bool
258 `True` if the fields are equal, `False` otherwise.
260 Notes
261 -----
262 Floating point comparisons are performed by `numpy.allclose`.
263 """
264 d1 = getattr(instance1, self.name)
265 d2 = getattr(instance2, self.name)
266 name = getComparisonName(
267 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
268 )
269 if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output):
270 return False
271 equal = True
272 for k, v1 in d1.items():
273 v2 = d2[k]
274 result = compareConfigs(
275 "%s[%r]" % (name, k), v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output
276 )
277 if not result and shortcut:
278 return False
279 equal = equal and result
280 return equal