Coverage for python/lsst/pex/config/configDictField.py: 20%

123 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-09 03:27 -0700

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 

110class ConfigDictField(DictField): 

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 

175 def __init__( 

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: 197 ↛ 198line 197 didn't jump to line 198 because the condition on line 197 was never true

198 raise ValueError(f"'keytype' {_typeStr(keytype)} is not a supported type") 

199 elif not issubclass(itemtype, Config): 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true

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): 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true

205 check_errors.append(name) 

206 if check_errors: 206 ↛ 207line 206 didn't jump to line 207 because the condition on line 206 was never true

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