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

67 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-03-12 01:30 -0800

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 .config import Config, Field, FieldValidationError, _joinNamePath, _typeStr 

31from .comparison import compareConfigs, getComparisonName 

32from .callStack import getCallStack, getStackFrame 

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

84 _typeStr(dtype)) 

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

86 default = dtype 

87 source = getStackFrame() 

88 self._setup(doc=doc, dtype=dtype, default=default, check=check, 

89 optional=False, source=source, deprecated=deprecated) 

90 

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

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

93 return self 

94 else: 

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

96 if value is None: 

97 at = getCallStack() 

98 at.insert(0, self.source) 

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

100 return value 

101 

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

103 if instance._frozen: 

104 raise FieldValidationError(self, instance, 

105 "Cannot modify a frozen Config") 

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

107 

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

109 msg = "Value %s is of incorrect type %s. Expected %s" % \ 

110 (value, _typeStr(value), _typeStr(self.dtype)) 

111 raise FieldValidationError(self, instance, msg) 

112 

113 if at is None: 

114 at = getCallStack() 

115 

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

117 if oldValue is None: 

118 if value == self.dtype: 

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

120 else: 

121 instance._storage[self.name] = self.dtype(__name=name, __at=at, 

122 __label=label, **value._storage) 

123 else: 

124 if value == self.dtype: 

125 value = value() 

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

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

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

129 

130 def rename(self, instance): 

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

132 only). 

133 

134 Parameters 

135 ---------- 

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

137 The config instance that contains this field. 

138 

139 Notes 

140 ----- 

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

142 contains this field and should not be called directly. 

143 

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

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

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

147 `lsst.pex.config.config._joinNamePath`. 

148 """ 

149 value = self.__get__(instance) 

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

151 

152 def _collectImports(self, instance, imports): 

153 value = self.__get__(instance) 

154 value._collectImports() 

155 imports |= value._imports 

156 

157 def save(self, outfile, instance): 

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

159 

160 Parameters 

161 ---------- 

162 outfile : file-like object 

163 A writeable field handle. 

164 instance : `Config` 

165 The `Config` instance that contains this field. 

166 

167 Notes 

168 ----- 

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

170 contains this field and should not be called directly. 

171 

172 The output consists of the documentation string 

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

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

175 

176 This output can be executed with Python. 

177 """ 

178 value = self.__get__(instance) 

179 value._save(outfile) 

180 

181 def freeze(self, instance): 

182 """Make this field read-only. 

183 

184 Parameters 

185 ---------- 

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

187 The config instance that contains this field. 

188 

189 Notes 

190 ----- 

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

192 hold subconfigs should freeze each subconfig. 

193 

194 **Subclasses should implement this method.** 

195 """ 

196 value = self.__get__(instance) 

197 value.freeze() 

198 

199 def toDict(self, instance): 

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

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

202 

203 Parameters 

204 ---------- 

205 instance : `Config` 

206 The `Config` that contains this field. 

207 

208 Returns 

209 ------- 

210 value : object 

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

212 

213 Notes 

214 ----- 

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

216 should not be called directly. 

217 

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

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

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

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

222 the field values in the subconfig. 

223 """ 

224 value = self.__get__(instance) 

225 return value.toDict() 

226 

227 def validate(self, instance): 

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

229 

230 Parameters 

231 ---------- 

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

233 The config instance that contains this field. 

234 

235 Raises 

236 ------ 

237 lsst.pex.config.FieldValidationError 

238 Raised if verification fails. 

239 

240 Notes 

241 ----- 

242 This method provides basic validation: 

243 

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

245 - Ensures type correctness. 

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

247 

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

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

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

251 """ 

252 value = self.__get__(instance) 

253 value.validate() 

254 

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

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

257 raise FieldValidationError(self, instance, msg) 

258 

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

260 """Compare two fields for equality. 

261 

262 Used by `ConfigField.compare`. 

263 

264 Parameters 

265 ---------- 

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

267 Left-hand side config instance to compare. 

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

269 Right-hand side config instance to compare. 

270 shortcut : `bool` 

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

272 rtol : `float` 

273 Relative tolerance for floating point comparisons. 

274 atol : `float` 

275 Absolute tolerance for floating point comparisons. 

276 output : callable 

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

278 report inequalities. 

279 

280 Returns 

281 ------- 

282 isEqual : bool 

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

284 

285 Notes 

286 ----- 

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

288 """ 

289 c1 = getattr(instance1, self.name) 

290 c2 = getattr(instance2, self.name) 

291 name = getComparisonName( 

292 _joinNamePath(instance1._name, self.name), 

293 _joinNamePath(instance2._name, self.name) 

294 ) 

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