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

165 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-23 09:46 +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 

31import weakref 

32 

33from .callStack import getCallStack, getStackFrame 

34from .comparison import compareConfigs, getComparisonName 

35from .config import Config, Field, FieldValidationError, UnexpectedProxyUsageError, _joinNamePath, _typeStr 

36 

37 

38class ConfigurableInstance: 

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

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

41 

42 Notes 

43 ----- 

44 ``ConfigurableInstance`` implements ``__getattr__`` and ``__setattr__`` 

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

46 ``ConfigurableInstance`` adds a `retarget` method. 

47 

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

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

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

51 using the ``target`` property. 

52 """ 

53 

54 def __initValue(self, at, label): 

55 """Construct value of field. 

56 

57 Notes 

58 ----- 

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

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

61 Otherwise, call ``ConfigClass`` constructor 

62 """ 

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

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

65 storage = self._field.default._storage 

66 else: 

67 storage = {} 

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

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

70 

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

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

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

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

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

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

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

78 

79 if at is None: 

80 at = getCallStack() 

81 at += [self._field.source] 

82 self.__initValue(at, label) 

83 

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

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

86 

87 @property 

88 def _config(self) -> Config: 

89 # Config Fields should never outlive their config class instance 

90 # assert that as such here 

91 assert self._config_() is not None 

92 return self._config_() 

93 

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

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

96 """ 

97 

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

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

100 """ 

101 

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

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

104 read-only). 

105 """ 

106 

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

108 """Call the configurable. 

109 

110 Notes 

111 ----- 

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

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

114 the value of `ConfigurableInstance.value`. 

115 """ 

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

117 

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

119 """Target a new configurable and ConfigClass""" 

120 if self._config._frozen: 

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

122 

123 try: 

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

125 except BaseException as e: 

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

127 

128 if at is None: 

129 at = getCallStack() 

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

131 if ConfigClass != self.ConfigClass: 

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

133 self.__initValue(at, label) 

134 

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

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

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

138 

139 def __getattr__(self, name): 

140 return getattr(self._value, name) 

141 

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

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

144 

145 Attributes defined by ConfigurableInstance will shadow those defined 

146 in ConfigClass 

147 """ 

148 if self._config._frozen: 

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

150 

151 if name in self.__dict__: 

152 # attribute exists in the ConfigurableInstance wrapper 

153 object.__setattr__(self, name, value) 

154 else: 

155 if at is None: 

156 at = getCallStack() 

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

158 

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

160 """ 

161 Pretend to be an isntance of ConfigClass. 

162 Attributes defiend by ConfigurableInstance will shadow those defined 

163 in ConfigClass 

164 """ 

165 if self._config._frozen: 

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

167 

168 try: 

169 # attribute exists in the ConfigurableInstance wrapper 

170 object.__delattr__(self, name) 

171 except AttributeError: 

172 if at is None: 

173 at = getCallStack() 

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

175 

176 def __reduce__(self): 

177 raise UnexpectedProxyUsageError( 

178 f"Proxy object for config field {self._field.name} cannot " 

179 "be pickled; it should be converted to a normal `Config` instance " 

180 f"via the `value` property before being assigned to other objects " 

181 "or variables." 

182 ) 

183 

184 

185class ConfigurableField(Field): 

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

187 can be retargeted towards a different configurable (often a 

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

189 

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

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

192 

193 Parameters 

194 ---------- 

195 doc : `str` 

196 A description of the configuration field. 

197 target : configurable class 

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

199 attribute. Within the task framework, configurables are 

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

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

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

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

204 ``target.ConfigClass`` is used. 

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

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

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

208 check : callable, optional 

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

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

211 `False` otherwise). 

212 deprecated : None or `str`, optional 

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

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

215 

216 See also 

217 -------- 

218 ChoiceField 

219 ConfigChoiceField 

220 ConfigDictField 

221 ConfigField 

222 DictField 

223 Field 

224 ListField 

225 RangeField 

226 RegistryField 

227 

228 Notes 

229 ----- 

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

231 fully-configured configurable. 

232 """ 

233 

234 def validateTarget(self, target, ConfigClass): 

235 """Validate the target and configuration class. 

236 

237 Parameters 

238 ---------- 

239 target 

240 The configurable being verified. 

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

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

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

244 

245 Raises 

246 ------ 

247 AttributeError 

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

249 ``ConfigClass`` attribute. 

250 TypeError 

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

252 subclass. 

253 ValueError 

254 Raised if: 

255 

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

257 method). 

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

259 ``__module__`` or ``__name__`` attributes). 

260 """ 

261 if ConfigClass is None: 

262 try: 

263 ConfigClass = target.ConfigClass 

264 except Exception: 

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

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

267 raise TypeError( 

268 "'ConfigClass' is of incorrect type %s." 

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

270 ) 

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

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

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

274 raise ValueError( 

275 "'target' must be statically defined (must have '__module__' and '__name__' attributes)" 

276 ) 

277 return ConfigClass 

278 

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

280 ConfigClass = self.validateTarget(target, ConfigClass) 

281 

282 if default is None: 

283 default = ConfigClass 

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

285 raise TypeError( 

286 "'default' is of incorrect type %s. Expected %s" % (_typeStr(default), _typeStr(ConfigClass)) 

287 ) 

288 

289 source = getStackFrame() 

290 self._setup( 

291 doc=doc, 

292 dtype=ConfigurableInstance, 

293 default=default, 

294 check=check, 

295 optional=False, 

296 source=source, 

297 deprecated=deprecated, 

298 ) 

299 self.target = target 

300 self.ConfigClass = ConfigClass 

301 

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

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

304 if value is None: 

305 if at is None: 

306 at = getCallStack(1) 

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

308 instance._storage[self.name] = value 

309 return value 

310 

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

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

313 return self 

314 else: 

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

316 

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

318 if instance._frozen: 

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

320 if at is None: 

321 at = getCallStack() 

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

323 

324 if isinstance(value, ConfigurableInstance): 

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

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

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

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

329 elif value == oldValue.ConfigClass: 

330 value = oldValue.ConfigClass() 

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

332 else: 

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

334 value, 

335 _typeStr(value), 

336 _typeStr(oldValue.ConfigClass), 

337 ) 

338 raise FieldValidationError(self, instance, msg) 

339 

340 def rename(self, instance): 

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

342 value = self.__getOrMake(instance) 

343 value._rename(fullname) 

344 

345 def _collectImports(self, instance, imports): 

346 value = self.__get__(instance) 

347 target = value.target 

348 imports.add(target.__module__) 

349 value.value._collectImports() 

350 imports |= value.value._imports 

351 

352 def save(self, outfile, instance): 

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

354 value = self.__getOrMake(instance) 

355 target = value.target 

356 

357 if target != self.target: 

358 # not targeting the field-default target. 

359 # save target information 

360 ConfigClass = value.ConfigClass 

361 outfile.write( 

362 "{}.retarget(target={}, ConfigClass={})\n\n".format( 

363 fullname, _typeStr(target), _typeStr(ConfigClass) 

364 ) 

365 ) 

366 # save field values 

367 value._save(outfile) 

368 

369 def freeze(self, instance): 

370 value = self.__getOrMake(instance) 

371 value.freeze() 

372 

373 def toDict(self, instance): 

374 value = self.__get__(instance) 

375 return value.toDict() 

376 

377 def validate(self, instance): 

378 value = self.__get__(instance) 

379 value.validate() 

380 

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

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

383 raise FieldValidationError(self, instance, msg) 

384 

385 def __deepcopy__(self, memo): 

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

387 original typemap. 

388 

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

390 constructor signature! 

391 """ 

392 return type(self)( 

393 doc=self.doc, 

394 target=self.target, 

395 ConfigClass=self.ConfigClass, 

396 default=copy.deepcopy(self.default), 

397 ) 

398 

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

400 """Compare two fields for equality. 

401 

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

403 

404 Parameters 

405 ---------- 

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

407 Left-hand side config instance to compare. 

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

409 Right-hand side config instance to compare. 

410 shortcut : `bool` 

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

412 rtol : `float` 

413 Relative tolerance for floating point comparisons. 

414 atol : `float` 

415 Absolute tolerance for floating point comparisons. 

416 output : callable 

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

418 report inequalities. For example: `print`. 

419 

420 Returns 

421 ------- 

422 isEqual : bool 

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

424 

425 Notes 

426 ----- 

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

428 """ 

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

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

431 name = getComparisonName( 

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

433 ) 

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