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

73 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:41 +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, 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 : `collections.abc.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(f"dtype={_typeStr(dtype)} is not a subclass of Config") 

86 if default is None: 86 ↛ 88line 86 didn't jump to line 88 because the condition on line 86 was always true

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 @overload 

105 def __get__( 

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

107 ) -> FieldTypeVar: ... 

108 

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

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

111 return self 

112 else: 

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

114 if value is None: 

115 at = getCallStack() 

116 at.insert(0, self.source) 

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

118 return value 

119 

120 def __set__( 

121 self, instance: Config, value: FieldTypeVar | None, at: Any = None, label: str = "assignment" 

122 ) -> None: 

123 if instance._frozen: 

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

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

126 

127 if value != self.dtype and type(value) is not self.dtype: 

128 msg = f"Value {value} is of incorrect type {_typeStr(value)}. Expected {_typeStr(self.dtype)}" 

129 raise FieldValidationError(self, instance, msg) 

130 

131 if at is None: 

132 at = getCallStack() 

133 

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

135 if oldValue is None: 

136 if value == self.dtype: 

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

138 else: 

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

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

141 ) 

142 else: 

143 if value == self.dtype: 

144 value = value() 

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

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

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

148 

149 def rename(self, instance): 

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

151 only). 

152 

153 Parameters 

154 ---------- 

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

156 The config instance that contains this field. 

157 

158 Notes 

159 ----- 

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

161 contains this field and should not be called directly. 

162 

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

164 hold subconfigs. `~lsst.pex.config.Field`\s that hold subconfigs should 

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

166 `lsst.pex.config.config._joinNamePath`. 

167 """ 

168 value = self.__get__(instance) 

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

170 

171 def _collectImports(self, instance, imports): 

172 value = self.__get__(instance) 

173 value._collectImports() 

174 imports |= value._imports 

175 

176 def save(self, outfile, instance): 

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

178 

179 Parameters 

180 ---------- 

181 outfile : `typing.IO` 

182 A writeable field handle. 

183 instance : `~lsst.pex.config.Config` 

184 The `~lsst.pex.config.Config` instance that contains this field. 

185 

186 Notes 

187 ----- 

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

189 contains this field and should not be called directly. 

190 

191 The output consists of the documentation string 

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

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

194 

195 This output can be executed with Python. 

196 """ 

197 value = self.__get__(instance) 

198 value._save(outfile) 

199 

200 def freeze(self, instance): 

201 """Make this field read-only. 

202 

203 Parameters 

204 ---------- 

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

206 The config instance that contains this field. 

207 

208 Notes 

209 ----- 

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

211 hold subconfigs should freeze each subconfig. 

212 

213 **Subclasses should implement this method.** 

214 """ 

215 value = self.__get__(instance) 

216 value.freeze() 

217 

218 def toDict(self, instance): 

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

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

221 

222 Parameters 

223 ---------- 

224 instance : `~lsst.pex.config.Config` 

225 The `~lsst.pex.config.Config` that contains this field. 

226 

227 Returns 

228 ------- 

229 value : `object` 

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

231 

232 Notes 

233 ----- 

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

235 should not be called directly. 

236 

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

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

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

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

241 the field values in the subconfig. 

242 """ 

243 value = self.__get__(instance) 

244 return value.toDict() 

245 

246 def _copy_storage(self, old: Config, new: Config) -> Any: 

247 value: Config | None = old._storage.get(self.name) 

248 if value is not None: 

249 return value.copy() 

250 else: 

251 return None 

252 

253 def validate(self, instance): 

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

255 

256 Parameters 

257 ---------- 

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

259 The config instance that contains this field. 

260 

261 Raises 

262 ------ 

263 lsst.pex.config.FieldValidationError 

264 Raised if verification fails. 

265 

266 Notes 

267 ----- 

268 This method provides basic validation: 

269 

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

271 - Ensures type correctness. 

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

273 

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

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

276 `~lsst.pex.config.Field.validate`. 

277 """ 

278 value = self.__get__(instance) 

279 value.validate() 

280 

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

282 msg = f"{value} is not a valid value" 

283 raise FieldValidationError(self, instance, msg) 

284 

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

286 """Compare two fields for equality. 

287 

288 Used by `ConfigField.compare`. 

289 

290 Parameters 

291 ---------- 

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

293 Left-hand side config instance to compare. 

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

295 Right-hand side config instance to compare. 

296 shortcut : `bool` 

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

298 rtol : `float` 

299 Relative tolerance for floating point comparisons. 

300 atol : `float` 

301 Absolute tolerance for floating point comparisons. 

302 output : `collections.abc.Callable` 

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

304 report inequalities. 

305 

306 Returns 

307 ------- 

308 isEqual : bool 

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

310 

311 Notes 

312 ----- 

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

314 """ 

315 c1 = getattr(instance1, self.name) 

316 c2 = getattr(instance2, self.name) 

317 name = getComparisonName( 

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

319 ) 

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