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

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 = "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) 

72 

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) 

83 

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)) 

101 

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)) 

107 

108 

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. 

112 

113 ``ConfigDictField`` behaves like `DictField` except that the 

114 ``itemtype`` must be a `~lsst.pex.config.Config` subclass. 

115 

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. 

139 

140 Raises 

141 ------ 

142 ValueError 

143 Raised if the inputs are invalid: 

144 

145 - ``keytype`` or ``itemtype`` arguments are not supported types 

146 (members of `ConfigDictField.supportedTypes`. 

147 - ``dictCheck`` or ``itemCheck`` is not a callable function. 

148 

149 See Also 

150 -------- 

151 ChoiceField 

152 ConfigChoiceField 

153 ConfigField 

154 ConfigurableField 

155 DictField 

156 Field 

157 ListField 

158 RangeField 

159 RegistryField 

160 

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 """ 

168 

169 DictClass = ConfigDict 

170 

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") 

200 

201 self.keytype = keytype 

202 self.itemtype = itemtype 

203 self.dictCheck = dictCheck 

204 self.itemCheck = itemCheck 

205 

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) 

212 

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) 

223 

224 def toDict(self, instance): 

225 configDict = self.__get__(instance) 

226 if configDict is None: 

227 return None 

228 

229 dict_ = {} 

230 for k in configDict: 

231 dict_[k] = configDict[k].toDict() 

232 

233 return dict_ 

234 

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 

242 

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 

249 

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) 

254 

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() 

260 

261 def _compare(self, instance1, instance2, shortcut, rtol, atol, output): 

262 """Compare two fields for equality. 

263 

264 Used by `lsst.pex.ConfigDictField.compare`. 

265 

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. 

281 

282 Returns 

283 ------- 

284 isEqual : bool 

285 `True` if the fields are equal, `False` otherwise. 

286 

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