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

117 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-10 09:56 +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 

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

46 

47 def __setitem__(self, k, x, at=None, label="setitem", setHistory=True): 

48 if self._config._frozen: 

49 msg = f"Cannot modify a frozen Config. Attempting to set item at key {k!r} to value {x}" 

50 raise FieldValidationError(self._field, self._config, msg) 

51 

52 # validate keytype 

53 k = _autocast(k, self._field.keytype) 

54 if type(k) != self._field.keytype: 

55 msg = "Key {!r} is of type {}, expected type {}".format( 

56 k, _typeStr(k), _typeStr(self._field.keytype) 

57 ) 

58 raise FieldValidationError(self._field, self._config, msg) 

59 

60 # validate itemtype 

61 dtype = self._field.itemtype 

62 if type(x) != self._field.itemtype and x != self._field.itemtype: 

63 msg = "Value {} at key {!r} is of incorrect type {}. Expected type {}".format( 

64 x, 

65 k, 

66 _typeStr(x), 

67 _typeStr(self._field.itemtype), 

68 ) 

69 raise FieldValidationError(self._field, self._config, msg) 

70 

71 if at is None: 

72 at = getCallStack() 

73 name = _joinNamePath(self._config._name, self._field.name, k) 

74 oldValue = self._dict.get(k, None) 

75 if oldValue is None: 

76 if x == dtype: 

77 self._dict[k] = dtype(__name=name, __at=at, __label=label) 

78 else: 

79 self._dict[k] = dtype(__name=name, __at=at, __label=label, **x._storage) 

80 if setHistory: 

81 self.history.append(("Added item at key %s" % k, at, label)) 

82 else: 

83 if x == dtype: 

84 x = dtype() 

85 oldValue.update(__at=at, __label=label, **x._storage) 

86 if setHistory: 

87 self.history.append(("Modified item at key %s" % k, at, label)) 

88 

89 def __delitem__(self, k, at=None, label="delitem"): 

90 if at is None: 

91 at = getCallStack() 

92 Dict.__delitem__(self, k, at, label, False) 

93 self.history.append(("Removed item at key %s" % k, at, label)) 

94 

95 

96class ConfigDictField(DictField): 

97 """A configuration field (`~lsst.pex.config.Field` subclass) that is a 

98 mapping of keys to `~lsst.pex.config.Config` instances. 

99 

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

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

102 

103 Parameters 

104 ---------- 

105 doc : `str` 

106 A description of the configuration field. 

107 keytype : {`int`, `float`, `complex`, `bool`, `str`} 

108 The type of the mapping keys. All keys must have this type. 

109 itemtype : `lsst.pex.config.Config`-type 

110 The type of the values in the mapping. This must be 

111 `~lsst.pex.config.Config` or a subclass. 

112 default : optional 

113 Unknown. 

114 default : ``itemtype``-dtype, optional 

115 Default value of this field. 

116 optional : `bool`, optional 

117 If `True`, this configuration `~lsst.pex.config.Field` is *optional*. 

118 Default is `True`. 

119 deprecated : None or `str`, optional 

120 A description of why this Field is deprecated, including removal date. 

121 If not None, the string is appended to the docstring for this Field. 

122 

123 Raises 

124 ------ 

125 ValueError 

126 Raised if the inputs are invalid: 

127 

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

129 (members of `ConfigDictField.supportedTypes`. 

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

131 

132 See Also 

133 -------- 

134 ChoiceField 

135 ConfigChoiceField 

136 ConfigField 

137 ConfigurableField 

138 DictField 

139 Field 

140 ListField 

141 RangeField 

142 RegistryField 

143 

144 Notes 

145 ----- 

146 You can use ``ConfigDictField`` to create name-to-config mappings. One use 

147 case is for configuring mappings for dataset types in a Butler. In this 

148 case, the dataset type names are arbitrary and user-selected while the 

149 mapping configurations are known and fixed. 

150 """ 

151 

152 DictClass = ConfigDict 

153 

154 def __init__( 

155 self, 

156 doc, 

157 keytype, 

158 itemtype, 

159 default=None, 

160 optional=False, 

161 dictCheck=None, 

162 itemCheck=None, 

163 deprecated=None, 

164 ): 

165 source = getStackFrame() 

166 self._setup( 

167 doc=doc, 

168 dtype=ConfigDict, 

169 default=default, 

170 check=None, 

171 optional=optional, 

172 source=source, 

173 deprecated=deprecated, 

174 ) 

175 if keytype not in self.supportedTypes: 175 ↛ 176line 175 didn't jump to line 176, because the condition on line 175 was never true

176 raise ValueError("'keytype' %s is not a supported type" % _typeStr(keytype)) 

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

178 raise ValueError("'itemtype' %s is not a supported type" % _typeStr(itemtype)) 

179 if dictCheck is not None and not hasattr(dictCheck, "__call__"): 179 ↛ 180line 179 didn't jump to line 180, because the condition on line 179 was never true

180 raise ValueError("'dictCheck' must be callable") 

181 if itemCheck is not None and not hasattr(itemCheck, "__call__"): 181 ↛ 182line 181 didn't jump to line 182, because the condition on line 181 was never true

182 raise ValueError("'itemCheck' must be callable") 

183 

184 self.keytype = keytype 

185 self.itemtype = itemtype 

186 self.dictCheck = dictCheck 

187 self.itemCheck = itemCheck 

188 

189 def rename(self, instance): 

190 configDict = self.__get__(instance) 

191 if configDict is not None: 

192 for k in configDict: 

193 fullname = _joinNamePath(instance._name, self.name, k) 

194 configDict[k]._rename(fullname) 

195 

196 def validate(self, instance): 

197 value = self.__get__(instance) 

198 if value is not None: 

199 for k in value: 

200 item = value[k] 

201 item.validate() 

202 if self.itemCheck is not None and not self.itemCheck(item): 

203 msg = f"Item at key {k!r} is not a valid value: {item}" 

204 raise FieldValidationError(self, instance, msg) 

205 DictField.validate(self, instance) 

206 

207 def toDict(self, instance): 

208 configDict = self.__get__(instance) 

209 if configDict is None: 

210 return None 

211 

212 dict_ = {} 

213 for k in configDict: 

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

215 

216 return dict_ 

217 

218 def _collectImports(self, instance, imports): 

219 # docstring inherited from Field 

220 configDict = self.__get__(instance) 

221 if configDict is not None: 

222 for v in configDict.values(): 

223 v._collectImports() 

224 imports |= v._imports 

225 

226 def save(self, outfile, instance): 

227 configDict = self.__get__(instance) 

228 fullname = _joinNamePath(instance._name, self.name) 

229 if configDict is None: 

230 outfile.write(f"{fullname}={configDict!r}\n") 

231 return 

232 

233 outfile.write(f"{fullname}={{}}\n") 

234 for v in configDict.values(): 

235 outfile.write(f"{v._name}={_typeStr(v)}()\n") 

236 v._save(outfile) 

237 

238 def freeze(self, instance): 

239 configDict = self.__get__(instance) 

240 if configDict is not None: 

241 for k in configDict: 

242 configDict[k].freeze() 

243 

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

245 """Compare two fields for equality. 

246 

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

248 

249 Parameters 

250 ---------- 

251 instance1 : `lsst.pex.config.Config` 

252 Left-hand side config instance to compare. 

253 instance2 : `lsst.pex.config.Config` 

254 Right-hand side config instance to compare. 

255 shortcut : `bool` 

256 If `True`, this function returns as soon as an inequality if found. 

257 rtol : `float` 

258 Relative tolerance for floating point comparisons. 

259 atol : `float` 

260 Absolute tolerance for floating point comparisons. 

261 output : callable 

262 A callable that takes a string, used (possibly repeatedly) to 

263 report inequalities. 

264 

265 Returns 

266 ------- 

267 isEqual : bool 

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

269 

270 Notes 

271 ----- 

272 Floating point comparisons are performed by `numpy.allclose`. 

273 """ 

274 d1 = getattr(instance1, self.name) 

275 d2 = getattr(instance2, self.name) 

276 name = getComparisonName( 

277 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name) 

278 ) 

279 if not compareScalars("keys for %s" % name, set(d1.keys()), set(d2.keys()), output=output): 

280 return False 

281 equal = True 

282 for k, v1 in d1.items(): 

283 v2 = d2[k] 

284 result = compareConfigs( 

285 f"{name}[{k!r}]", v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output 

286 ) 

287 if not result and shortcut: 

288 return False 

289 equal = equal and result 

290 return equal