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

74 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-28 09:11 +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__ = ["ConfigField"] 

29 

30from typing import Any, Optional, overload 

31 

32from .callStack import getCallStack, getStackFrame 

33from .comparison import compareConfigs, getComparisonName 

34from .config import Config, Field, FieldTypeVar, FieldValidationError, _joinNamePath, _typeStr 

35 

36 

37class ConfigField(Field[FieldTypeVar]): 

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

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

40 

41 Parameters 

42 ---------- 

43 doc : `str` 

44 A description of the configuration field. 

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

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

47 `lsst.pex.config.Config`. 

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

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

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

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

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

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

54 instance of ``dtype``. 

55 check : callable, optional 

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

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

58 deprecated : None or `str`, optional 

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

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

61 

62 See also 

63 -------- 

64 ChoiceField 

65 ConfigChoiceField 

66 ConfigDictField 

67 ConfigurableField 

68 DictField 

69 Field 

70 ListField 

71 RangeField 

72 RegistryField 

73 

74 Notes 

75 ----- 

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

77 type. 

78 

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

80 configuration. 

81 """ 

82 

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

84 if dtype is None or not issubclass(dtype, Config): 84 ↛ 85line 84 didn't jump to line 85, because the condition on line 84 was never true

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

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

87 default = dtype 

88 source = getStackFrame() 

89 self._setup( 

90 doc=doc, 

91 dtype=dtype, 

92 default=default, 

93 check=check, 

94 optional=False, 

95 source=source, 

96 deprecated=deprecated, 

97 ) 

98 

99 @overload 

100 def __get__( 

101 self, instance: None, owner: Any = None, at: Any = None, label: str = "default" 

102 ) -> "ConfigField[FieldTypeVar]": 

103 ... 

104 

105 @overload 

106 def __get__( 

107 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default" 

108 ) -> FieldTypeVar: 

109 ... 

110 

111 def __get__(self, instance, owner=None, at=None, label="default"): 

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

113 return self 

114 else: 

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

116 if value is None: 

117 at = getCallStack() 

118 at.insert(0, self.source) 

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

120 return value 

121 

122 def __set__( 

123 self, instance: Config, value: Optional[FieldTypeVar], at: Any = None, label: str = "assignment" 

124 ) -> None: 

125 if instance._frozen: 

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

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

128 

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

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

131 value, 

132 _typeStr(value), 

133 _typeStr(self.dtype), 

134 ) 

135 raise FieldValidationError(self, instance, msg) 

136 

137 if at is None: 

138 at = getCallStack() 

139 

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

141 if oldValue is None: 

142 if value == self.dtype: 

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

144 else: 

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

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

147 ) 

148 else: 

149 if value == self.dtype: 

150 value = value() 

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

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

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

154 

155 def rename(self, instance): 

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

157 only). 

158 

159 Parameters 

160 ---------- 

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

162 The config instance that contains this field. 

163 

164 Notes 

165 ----- 

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

167 contains this field and should not be called directly. 

168 

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

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

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

172 `lsst.pex.config.config._joinNamePath`. 

173 """ 

174 value = self.__get__(instance) 

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

176 

177 def _collectImports(self, instance, imports): 

178 value = self.__get__(instance) 

179 value._collectImports() 

180 imports |= value._imports 

181 

182 def save(self, outfile, instance): 

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

184 

185 Parameters 

186 ---------- 

187 outfile : file-like object 

188 A writeable field handle. 

189 instance : `Config` 

190 The `Config` instance that contains this field. 

191 

192 Notes 

193 ----- 

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

195 contains this field and should not be called directly. 

196 

197 The output consists of the documentation string 

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

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

200 

201 This output can be executed with Python. 

202 """ 

203 value = self.__get__(instance) 

204 value._save(outfile) 

205 

206 def freeze(self, instance): 

207 """Make this field read-only. 

208 

209 Parameters 

210 ---------- 

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

212 The config instance that contains this field. 

213 

214 Notes 

215 ----- 

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

217 hold subconfigs should freeze each subconfig. 

218 

219 **Subclasses should implement this method.** 

220 """ 

221 value = self.__get__(instance) 

222 value.freeze() 

223 

224 def toDict(self, instance): 

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

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

227 

228 Parameters 

229 ---------- 

230 instance : `Config` 

231 The `Config` that contains this field. 

232 

233 Returns 

234 ------- 

235 value : object 

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

237 

238 Notes 

239 ----- 

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

241 should not be called directly. 

242 

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

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

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

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

247 the field values in the subconfig. 

248 """ 

249 value = self.__get__(instance) 

250 return value.toDict() 

251 

252 def validate(self, instance): 

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

254 

255 Parameters 

256 ---------- 

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

258 The config instance that contains this field. 

259 

260 Raises 

261 ------ 

262 lsst.pex.config.FieldValidationError 

263 Raised if verification fails. 

264 

265 Notes 

266 ----- 

267 This method provides basic validation: 

268 

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

270 - Ensures type correctness. 

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

272 

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

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

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

276 """ 

277 value = self.__get__(instance) 

278 value.validate() 

279 

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

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

282 raise FieldValidationError(self, instance, msg) 

283 

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

285 """Compare two fields for equality. 

286 

287 Used by `ConfigField.compare`. 

288 

289 Parameters 

290 ---------- 

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

292 Left-hand side config instance to compare. 

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

294 Right-hand side config instance to compare. 

295 shortcut : `bool` 

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

297 rtol : `float` 

298 Relative tolerance for floating point comparisons. 

299 atol : `float` 

300 Absolute tolerance for floating point comparisons. 

301 output : callable 

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

303 report inequalities. 

304 

305 Returns 

306 ------- 

307 isEqual : bool 

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

309 

310 Notes 

311 ----- 

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

313 """ 

314 c1 = getattr(instance1, self.name) 

315 c2 = getattr(instance2, self.name) 

316 name = getComparisonName( 

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

318 ) 

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