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

160 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-01 19:59 +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__ = ('ConfigurableInstance', 'ConfigurableField') 

29 

30import copy 

31 

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

33from .comparison import compareConfigs, getComparisonName 

34from .callStack import getCallStack, getStackFrame 

35 

36import weakref 

37 

38 

39class ConfigurableInstance: 

40 """A retargetable configuration in a `ConfigurableField` that proxies 

41 a `~lsst.pex.config.Config`. 

42 

43 Notes 

44 ----- 

45 ``ConfigurableInstance`` implements ``__getattr__`` and ``__setattr__`` 

46 methods that forward to the `~lsst.pex.config.Config` it holds. 

47 ``ConfigurableInstance`` adds a `retarget` method. 

48 

49 The actual `~lsst.pex.config.Config` instance is accessed using the 

50 ``value`` property (e.g. to get its documentation). The associated 

51 configurable object (usually a `~lsst.pipe.base.Task`) is accessed 

52 using the ``target`` property. 

53 """ 

54 

55 def __initValue(self, at, label): 

56 """Construct value of field. 

57 

58 Notes 

59 ----- 

60 If field.default is an instance of `lsst.pex.config.ConfigClass`, 

61 custom construct ``_value`` with the correct values from default. 

62 Otherwise, call ``ConfigClass`` constructor 

63 """ 

64 name = _joinNamePath(self._config._name, self._field.name) 

65 if type(self._field.default) == self.ConfigClass: 

66 storage = self._field.default._storage 

67 else: 

68 storage = {} 

69 value = self._ConfigClass(__name=name, __at=at, __label=label, **storage) 

70 object.__setattr__(self, "_value", value) 

71 

72 def __init__(self, config, field, at=None, label="default"): 

73 object.__setattr__(self, "_config_", weakref.ref(config)) 

74 object.__setattr__(self, "_field", field) 

75 object.__setattr__(self, "__doc__", config) 

76 object.__setattr__(self, "_target", field.target) 

77 object.__setattr__(self, "_ConfigClass", field.ConfigClass) 

78 object.__setattr__(self, "_value", None) 

79 

80 if at is None: 

81 at = getCallStack() 

82 at += [self._field.source] 

83 self.__initValue(at, label) 

84 

85 history = config._history.setdefault(field.name, []) 

86 history.append(("Targeted and initialized from defaults", at, label)) 

87 

88 @property 

89 def _config(self) -> Config: 

90 # Config Fields should never outlive their config class instance 

91 # assert that as such here 

92 assert(self._config_() is not None) 

93 return self._config_() 

94 

95 target = property(lambda x: x._target) 95 ↛ exitline 95 didn't run the lambda on line 95

96 """The targeted configurable (read-only). 

97 """ 

98 

99 ConfigClass = property(lambda x: x._ConfigClass) 99 ↛ exitline 99 didn't run the lambda on line 99

100 """The configuration class (read-only) 

101 """ 

102 

103 value = property(lambda x: x._value) 103 ↛ exitline 103 didn't run the lambda on line 103

104 """The `ConfigClass` instance (`lsst.pex.config.ConfigClass`-type, 

105 read-only). 

106 """ 

107 

108 def apply(self, *args, **kw): 

109 """Call the configurable. 

110 

111 Notes 

112 ----- 

113 In addition to the user-provided positional and keyword arguments, 

114 the configurable is also provided a keyword argument ``config`` with 

115 the value of `ConfigurableInstance.value`. 

116 """ 

117 return self.target(*args, config=self.value, **kw) 

118 

119 def retarget(self, target, ConfigClass=None, at=None, label="retarget"): 

120 """Target a new configurable and ConfigClass 

121 """ 

122 if self._config._frozen: 

123 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config") 

124 

125 try: 

126 ConfigClass = self._field.validateTarget(target, ConfigClass) 

127 except BaseException as e: 

128 raise FieldValidationError(self._field, self._config, e.message) 

129 

130 if at is None: 

131 at = getCallStack() 

132 object.__setattr__(self, "_target", target) 

133 if ConfigClass != self.ConfigClass: 

134 object.__setattr__(self, "_ConfigClass", ConfigClass) 

135 self.__initValue(at, label) 

136 

137 history = self._config._history.setdefault(self._field.name, []) 

138 msg = "retarget(target=%s, ConfigClass=%s)" % (_typeStr(target), _typeStr(ConfigClass)) 

139 history.append((msg, at, label)) 

140 

141 def __getattr__(self, name): 

142 return getattr(self._value, name) 

143 

144 def __setattr__(self, name, value, at=None, label="assignment"): 

145 """Pretend to be an instance of ConfigClass. 

146 

147 Attributes defined by ConfigurableInstance will shadow those defined 

148 in ConfigClass 

149 """ 

150 if self._config._frozen: 

151 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config") 

152 

153 if name in self.__dict__: 

154 # attribute exists in the ConfigurableInstance wrapper 

155 object.__setattr__(self, name, value) 

156 else: 

157 if at is None: 

158 at = getCallStack() 

159 self._value.__setattr__(name, value, at=at, label=label) 

160 

161 def __delattr__(self, name, at=None, label="delete"): 

162 """ 

163 Pretend to be an isntance of ConfigClass. 

164 Attributes defiend by ConfigurableInstance will shadow those defined 

165 in ConfigClass 

166 """ 

167 if self._config._frozen: 

168 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config") 

169 

170 try: 

171 # attribute exists in the ConfigurableInstance wrapper 

172 object.__delattr__(self, name) 

173 except AttributeError: 

174 if at is None: 

175 at = getCallStack() 

176 self._value.__delattr__(name, at=at, label=label) 

177 

178 

179class ConfigurableField(Field): 

180 """A configuration field (`~lsst.pex.config.Field` subclass) that can be 

181 can be retargeted towards a different configurable (often a 

182 `lsst.pipe.base.Task` subclass). 

183 

184 The ``ConfigurableField`` is often used to configure subtasks, which are 

185 tasks (`~lsst.pipe.base.Task`) called by a parent task. 

186 

187 Parameters 

188 ---------- 

189 doc : `str` 

190 A description of the configuration field. 

191 target : configurable class 

192 The configurable target. Configurables have a ``ConfigClass`` 

193 attribute. Within the task framework, configurables are 

194 `lsst.pipe.base.Task` subclasses) 

195 ConfigClass : `lsst.pex.config.Config`-type, optional 

196 The subclass of `lsst.pex.config.Config` expected as the configuration 

197 class of the ``target``. If ``ConfigClass`` is unset then 

198 ``target.ConfigClass`` is used. 

199 default : ``ConfigClass``-type, optional 

200 The default configuration class. Normally this parameter is not set, 

201 and defaults to ``ConfigClass`` (or ``target.ConfigClass``). 

202 check : callable, optional 

203 Callable that takes the field's value (the ``target``) as its only 

204 positional argument, and returns `True` if the ``target`` is valid (and 

205 `False` otherwise). 

206 deprecated : None or `str`, optional 

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

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

209 

210 See also 

211 -------- 

212 ChoiceField 

213 ConfigChoiceField 

214 ConfigDictField 

215 ConfigField 

216 DictField 

217 Field 

218 ListField 

219 RangeField 

220 RegistryField 

221 

222 Notes 

223 ----- 

224 You can use the `ConfigurableInstance.apply` method to construct a 

225 fully-configured configurable. 

226 """ 

227 

228 def validateTarget(self, target, ConfigClass): 

229 """Validate the target and configuration class. 

230 

231 Parameters 

232 ---------- 

233 target 

234 The configurable being verified. 

235 ConfigClass : `lsst.pex.config.Config`-type or `None` 

236 The configuration class associated with the ``target``. This can 

237 be `None` if ``target`` has a ``ConfigClass`` attribute. 

238 

239 Raises 

240 ------ 

241 AttributeError 

242 Raised if ``ConfigClass`` is `None` and ``target`` does not have a 

243 ``ConfigClass`` attribute. 

244 TypeError 

245 Raised if ``ConfigClass`` is not a `~lsst.pex.config.Config` 

246 subclass. 

247 ValueError 

248 Raised if: 

249 

250 - ``target`` is not callable (callables have a ``__call__`` 

251 method). 

252 - ``target`` is not startically defined (does not have 

253 ``__module__`` or ``__name__`` attributes). 

254 """ 

255 if ConfigClass is None: 

256 try: 

257 ConfigClass = target.ConfigClass 

258 except Exception: 

259 raise AttributeError("'target' must define attribute 'ConfigClass'") 

260 if not issubclass(ConfigClass, Config): 260 ↛ 261line 260 didn't jump to line 261, because the condition on line 260 was never true

261 raise TypeError("'ConfigClass' is of incorrect type %s." 

262 "'ConfigClass' must be a subclass of Config" % _typeStr(ConfigClass)) 

263 if not hasattr(target, '__call__'): 263 ↛ 264line 263 didn't jump to line 264, because the condition on line 263 was never true

264 raise ValueError("'target' must be callable") 

265 if not hasattr(target, '__module__') or not hasattr(target, '__name__'): 265 ↛ 266line 265 didn't jump to line 266, because the condition on line 265 was never true

266 raise ValueError("'target' must be statically defined" 

267 "(must have '__module__' and '__name__' attributes)") 

268 return ConfigClass 

269 

270 def __init__(self, doc, target, ConfigClass=None, default=None, check=None, deprecated=None): 

271 ConfigClass = self.validateTarget(target, ConfigClass) 

272 

273 if default is None: 

274 default = ConfigClass 

275 if default != ConfigClass and type(default) != ConfigClass: 275 ↛ 276line 275 didn't jump to line 276, because the condition on line 275 was never true

276 raise TypeError("'default' is of incorrect type %s. Expected %s" % 

277 (_typeStr(default), _typeStr(ConfigClass))) 

278 

279 source = getStackFrame() 

280 self._setup(doc=doc, dtype=ConfigurableInstance, default=default, 

281 check=check, optional=False, source=source, deprecated=deprecated) 

282 self.target = target 

283 self.ConfigClass = ConfigClass 

284 

285 def __getOrMake(self, instance, at=None, label="default"): 

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

287 if value is None: 

288 if at is None: 

289 at = getCallStack(1) 

290 value = ConfigurableInstance(instance, self, at=at, label=label) 

291 instance._storage[self.name] = value 

292 return value 

293 

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

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

296 return self 

297 else: 

298 return self.__getOrMake(instance, at=at, label=label) 

299 

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

301 if instance._frozen: 

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

303 if at is None: 

304 at = getCallStack() 

305 oldValue = self.__getOrMake(instance, at=at) 

306 

307 if isinstance(value, ConfigurableInstance): 

308 oldValue.retarget(value.target, value.ConfigClass, at, label) 

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

310 elif type(value) == oldValue._ConfigClass: 

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

312 elif value == oldValue.ConfigClass: 

313 value = oldValue.ConfigClass() 

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

315 else: 

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

317 (value, _typeStr(value), _typeStr(oldValue.ConfigClass)) 

318 raise FieldValidationError(self, instance, msg) 

319 

320 def rename(self, instance): 

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

322 value = self.__getOrMake(instance) 

323 value._rename(fullname) 

324 

325 def _collectImports(self, instance, imports): 

326 value = self.__get__(instance) 

327 target = value.target 

328 imports.add(target.__module__) 

329 value.value._collectImports() 

330 imports |= value.value._imports 

331 

332 def save(self, outfile, instance): 

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

334 value = self.__getOrMake(instance) 

335 target = value.target 

336 

337 if target != self.target: 

338 # not targeting the field-default target. 

339 # save target information 

340 ConfigClass = value.ConfigClass 

341 outfile.write(u"{}.retarget(target={}, ConfigClass={})\n\n".format(fullname, 

342 _typeStr(target), 

343 _typeStr(ConfigClass))) 

344 # save field values 

345 value._save(outfile) 

346 

347 def freeze(self, instance): 

348 value = self.__getOrMake(instance) 

349 value.freeze() 

350 

351 def toDict(self, instance): 

352 value = self.__get__(instance) 

353 return value.toDict() 

354 

355 def validate(self, instance): 

356 value = self.__get__(instance) 

357 value.validate() 

358 

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

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

361 raise FieldValidationError(self, instance, msg) 

362 

363 def __deepcopy__(self, memo): 

364 """Customize deep-copying, because we always want a reference to the 

365 original typemap. 

366 

367 WARNING: this must be overridden by subclasses if they change the 

368 constructor signature! 

369 """ 

370 return type(self)(doc=self.doc, target=self.target, ConfigClass=self.ConfigClass, 

371 default=copy.deepcopy(self.default)) 

372 

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

374 """Compare two fields for equality. 

375 

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

377 

378 Parameters 

379 ---------- 

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

381 Left-hand side config instance to compare. 

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

383 Right-hand side config instance to compare. 

384 shortcut : `bool` 

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

386 rtol : `float` 

387 Relative tolerance for floating point comparisons. 

388 atol : `float` 

389 Absolute tolerance for floating point comparisons. 

390 output : callable 

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

392 report inequalities. For example: `print`. 

393 

394 Returns 

395 ------- 

396 isEqual : bool 

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

398 

399 Notes 

400 ----- 

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

402 """ 

403 c1 = getattr(instance1, self.name)._value 

404 c2 = getattr(instance2, self.name)._value 

405 name = getComparisonName( 

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

407 _joinNamePath(instance2._name, self.name) 

408 ) 

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