Coverage for python/lsst/pex/config/configField.py: 26%

67 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-06-02 03:31 -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__ = ["ConfigField"] 

29 

30from .callStack import getCallStack, getStackFrame 

31from .comparison import compareConfigs, getComparisonName 

32from .config import Config, Field, FieldValidationError, _joinNamePath, _typeStr 

33 

34 

35class ConfigField(Field): 

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

37 `~lsst.pex.config.Config`-type as a value. 

38 

39 Parameters 

40 ---------- 

41 doc : `str` 

42 A description of the configuration field. 

43 dtype : `lsst.pex.config.Config`-type 

44 The type of the field, which must be a subclass of 

45 `lsst.pex.config.Config`. 

46 default : `lsst.pex.config.Config`, optional 

47 If default is `None`, the field will default to a default-constructed 

48 instance of ``dtype``. Additionally, to allow for fewer deep-copies, 

49 assigning an instance of ``ConfigField`` to ``dtype`` itself, is 

50 considered equivalent to assigning a default-constructed sub-config. 

51 This means that the argument default can be ``dtype``, as well as an 

52 instance of ``dtype``. 

53 check : callable, optional 

54 A callback function that validates the field's value, returning `True` 

55 if the value is valid, and `False` otherwise. 

56 deprecated : None or `str`, optional 

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

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

59 

60 See also 

61 -------- 

62 ChoiceField 

63 ConfigChoiceField 

64 ConfigDictField 

65 ConfigurableField 

66 DictField 

67 Field 

68 ListField 

69 RangeField 

70 RegistryField 

71 

72 Notes 

73 ----- 

74 The behavior of this type of field is much like that of the base `Field` 

75 type. 

76 

77 Assigning to ``ConfigField`` will update all of the fields in the 

78 configuration. 

79 """ 

80 

81 def __init__(self, doc, dtype, default=None, check=None, deprecated=None): 

82 if not issubclass(dtype, Config): 82 ↛ 83line 82 didn't jump to line 83, because the condition on line 82 was never true

83 raise ValueError("dtype=%s is not a subclass of Config" % _typeStr(dtype)) 

84 if default is None: 84 ↛ 86line 84 didn't jump to line 86, because the condition on line 84 was never false

85 default = dtype 

86 source = getStackFrame() 

87 self._setup( 

88 doc=doc, 

89 dtype=dtype, 

90 default=default, 

91 check=check, 

92 optional=False, 

93 source=source, 

94 deprecated=deprecated, 

95 ) 

96 

97 def __get__(self, instance, owner=None): 

98 if instance is None or not isinstance(instance, Config): 

99 return self 

100 else: 

101 value = instance._storage.get(self.name, None) 

102 if value is None: 

103 at = getCallStack() 

104 at.insert(0, self.source) 

105 self.__set__(instance, self.default, at=at, label="default") 

106 return value 

107 

108 def __set__(self, instance, value, at=None, label="assignment"): 

109 if instance._frozen: 

110 raise FieldValidationError(self, instance, "Cannot modify a frozen Config") 

111 name = _joinNamePath(prefix=instance._name, name=self.name) 

112 

113 if value != self.dtype and type(value) != self.dtype: 

114 msg = "Value %s is of incorrect type %s. Expected %s" % ( 

115 value, 

116 _typeStr(value), 

117 _typeStr(self.dtype), 

118 ) 

119 raise FieldValidationError(self, instance, msg) 

120 

121 if at is None: 

122 at = getCallStack() 

123 

124 oldValue = instance._storage.get(self.name, None) 

125 if oldValue is None: 

126 if value == self.dtype: 

127 instance._storage[self.name] = self.dtype(__name=name, __at=at, __label=label) 

128 else: 

129 instance._storage[self.name] = self.dtype( 

130 __name=name, __at=at, __label=label, **value._storage 

131 ) 

132 else: 

133 if value == self.dtype: 

134 value = value() 

135 oldValue.update(__at=at, __label=label, **value._storage) 

136 history = instance._history.setdefault(self.name, []) 

137 history.append(("config value set", at, label)) 

138 

139 def rename(self, instance): 

140 """Rename the field in a `~lsst.pex.config.Config` (for internal use 

141 only). 

142 

143 Parameters 

144 ---------- 

145 instance : `lsst.pex.config.Config` 

146 The config instance that contains this field. 

147 

148 Notes 

149 ----- 

150 This method is invoked by the `lsst.pex.config.Config` object that 

151 contains this field and should not be called directly. 

152 

153 Renaming is only relevant for `~lsst.pex.config.Field` instances that 

154 hold subconfigs. `~lsst.pex.config.Fields` that hold subconfigs should 

155 rename each subconfig with the full field name as generated by 

156 `lsst.pex.config.config._joinNamePath`. 

157 """ 

158 value = self.__get__(instance) 

159 value._rename(_joinNamePath(instance._name, self.name)) 

160 

161 def _collectImports(self, instance, imports): 

162 value = self.__get__(instance) 

163 value._collectImports() 

164 imports |= value._imports 

165 

166 def save(self, outfile, instance): 

167 """Save this field to a file (for internal use only). 

168 

169 Parameters 

170 ---------- 

171 outfile : file-like object 

172 A writeable field handle. 

173 instance : `Config` 

174 The `Config` instance that contains this field. 

175 

176 Notes 

177 ----- 

178 This method is invoked by the `~lsst.pex.config.Config` object that 

179 contains this field and should not be called directly. 

180 

181 The output consists of the documentation string 

182 (`lsst.pex.config.Field.doc`) formatted as a Python comment. The second 

183 line is formatted as an assignment: ``{fullname}={value}``. 

184 

185 This output can be executed with Python. 

186 """ 

187 value = self.__get__(instance) 

188 value._save(outfile) 

189 

190 def freeze(self, instance): 

191 """Make this field read-only. 

192 

193 Parameters 

194 ---------- 

195 instance : `lsst.pex.config.Config` 

196 The config instance that contains this field. 

197 

198 Notes 

199 ----- 

200 Freezing is only relevant for fields that hold subconfigs. Fields which 

201 hold subconfigs should freeze each subconfig. 

202 

203 **Subclasses should implement this method.** 

204 """ 

205 value = self.__get__(instance) 

206 value.freeze() 

207 

208 def toDict(self, instance): 

209 """Convert the field value so that it can be set as the value of an 

210 item in a `dict` (for internal use only). 

211 

212 Parameters 

213 ---------- 

214 instance : `Config` 

215 The `Config` that contains this field. 

216 

217 Returns 

218 ------- 

219 value : object 

220 The field's value. See *Notes*. 

221 

222 Notes 

223 ----- 

224 This method invoked by the owning `~lsst.pex.config.Config` object and 

225 should not be called directly. 

226 

227 Simple values are passed through. Complex data structures must be 

228 manipulated. For example, a `~lsst.pex.config.Field` holding a 

229 subconfig should, instead of the subconfig object, return a `dict` 

230 where the keys are the field names in the subconfig, and the values are 

231 the field values in the subconfig. 

232 """ 

233 value = self.__get__(instance) 

234 return value.toDict() 

235 

236 def validate(self, instance): 

237 """Validate the field (for internal use only). 

238 

239 Parameters 

240 ---------- 

241 instance : `lsst.pex.config.Config` 

242 The config instance that contains this field. 

243 

244 Raises 

245 ------ 

246 lsst.pex.config.FieldValidationError 

247 Raised if verification fails. 

248 

249 Notes 

250 ----- 

251 This method provides basic validation: 

252 

253 - Ensures that the value is not `None` if the field is not optional. 

254 - Ensures type correctness. 

255 - Ensures that the user-provided ``check`` function is valid. 

256 

257 Most `~lsst.pex.config.Field` subclasses should call 

258 `lsst.pex.config.field.Field.validate` if they re-implement 

259 `~lsst.pex.config.field.Field.validate`. 

260 """ 

261 value = self.__get__(instance) 

262 value.validate() 

263 

264 if self.check is not None and not self.check(value): 

265 msg = "%s is not a valid value" % str(value) 

266 raise FieldValidationError(self, instance, msg) 

267 

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

269 """Compare two fields for equality. 

270 

271 Used by `ConfigField.compare`. 

272 

273 Parameters 

274 ---------- 

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

276 Left-hand side config instance to compare. 

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

278 Right-hand side config instance to compare. 

279 shortcut : `bool` 

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

281 rtol : `float` 

282 Relative tolerance for floating point comparisons. 

283 atol : `float` 

284 Absolute tolerance for floating point comparisons. 

285 output : callable 

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

287 report inequalities. 

288 

289 Returns 

290 ------- 

291 isEqual : bool 

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

293 

294 Notes 

295 ----- 

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

297 """ 

298 c1 = getattr(instance1, self.name) 

299 c2 = getattr(instance2, self.name) 

300 name = getComparisonName( 

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

302 ) 

303 return compareConfigs(name, c1, c2, shortcut=shortcut, rtol=rtol, atol=atol, output=output)