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

117 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-07-09 05:59 -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 

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

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 %s, expected type %s" % (k, _typeStr(k), _typeStr(self._field.keytype)) 

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

57 

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) 

68 

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

86 

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

92 

93 

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. 

97 

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

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

100 

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. 

120 

121 Raises 

122 ------ 

123 ValueError 

124 Raised if the inputs are invalid: 

125 

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

127 (members of `ConfigDictField.supportedTypes`. 

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

129 

130 See also 

131 -------- 

132 ChoiceField 

133 ConfigChoiceField 

134 ConfigField 

135 ConfigurableField 

136 DictField 

137 Field 

138 ListField 

139 RangeField 

140 RegistryField 

141 

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

149 

150 DictClass = ConfigDict 

151 

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

181 

182 self.keytype = keytype 

183 self.itemtype = itemtype 

184 self.dictCheck = dictCheck 

185 self.itemCheck = itemCheck 

186 

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) 

193 

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) 

204 

205 def toDict(self, instance): 

206 configDict = self.__get__(instance) 

207 if configDict is None: 

208 return None 

209 

210 dict_ = {} 

211 for k in configDict: 

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

213 

214 return dict_ 

215 

216 def _collectImports(self, instance, imports): 

217 # docstring inherited from Field 

218 configDict = self.__get__(instance) 

219 if configDict is not None: 

220 for v in configDict.values(): 

221 v._collectImports() 

222 imports |= v._imports 

223 

224 def save(self, outfile, instance): 

225 configDict = self.__get__(instance) 

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

227 if configDict is None: 

228 outfile.write("{}={!r}\n".format(fullname, configDict)) 

229 return 

230 

231 outfile.write("{}={!r}\n".format(fullname, {})) 

232 for v in configDict.values(): 

233 outfile.write("{}={}()\n".format(v._name, _typeStr(v))) 

234 v._save(outfile) 

235 

236 def freeze(self, instance): 

237 configDict = self.__get__(instance) 

238 if configDict is not None: 

239 for k in configDict: 

240 configDict[k].freeze() 

241 

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

243 """Compare two fields for equality. 

244 

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

246 

247 Parameters 

248 ---------- 

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

250 Left-hand side config instance to compare. 

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

252 Right-hand side config instance to compare. 

253 shortcut : `bool` 

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

255 rtol : `float` 

256 Relative tolerance for floating point comparisons. 

257 atol : `float` 

258 Absolute tolerance for floating point comparisons. 

259 output : callable 

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

261 report inequalities. 

262 

263 Returns 

264 ------- 

265 isEqual : bool 

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

267 

268 Notes 

269 ----- 

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

271 """ 

272 d1 = getattr(instance1, self.name) 

273 d2 = getattr(instance2, self.name) 

274 name = getComparisonName( 

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

276 ) 

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

278 return False 

279 equal = True 

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

281 v2 = d2[k] 

282 result = compareConfigs( 

283 "%s[%r]" % (name, k), v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output 

284 ) 

285 if not result and shortcut: 

286 return False 

287 equal = equal and result 

288 return equal