Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

36 

37class ConfigurableInstance: 

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

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

40 

41 Notes 

42 ----- 

43 ``ConfigurableInstance`` implements ``__getattr__`` and ``__setattr__`` 

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

45 ``ConfigurableInstance`` adds a `retarget` method. 

46 

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

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

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

50 using the ``target`` property. 

51 """ 

52 

53 def __initValue(self, at, label): 

54 """Construct value of field. 

55 

56 Notes 

57 ----- 

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

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

60 Otherwise, call ``ConfigClass`` constructor 

61 """ 

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

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

64 storage = self._field.default._storage 

65 else: 

66 storage = {} 

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

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

69 

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

71 object.__setattr__(self, "_config", config) 

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

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

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

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

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

77 

78 if at is None: 

79 at = getCallStack() 

80 at += [self._field.source] 

81 self.__initValue(at, label) 

82 

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

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

85 

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

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

88 """ 

89 

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

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

92 """ 

93 

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

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

96 read-only). 

97 """ 

98 

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

100 """Call the configurable. 

101 

102 Notes 

103 ----- 

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

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

106 the value of `ConfigurableInstance.value`. 

107 """ 

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

109 

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

111 """Target a new configurable and ConfigClass 

112 """ 

113 if self._config._frozen: 

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

115 

116 try: 

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

118 except BaseException as e: 

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

120 

121 if at is None: 

122 at = getCallStack() 

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

124 if ConfigClass != self.ConfigClass: 

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

126 self.__initValue(at, label) 

127 

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

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

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

131 

132 def __getattr__(self, name): 

133 return getattr(self._value, name) 

134 

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

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

137 

138 Attributes defined by ConfigurableInstance will shadow those defined 

139 in ConfigClass 

140 """ 

141 if self._config._frozen: 

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

143 

144 if name in self.__dict__: 

145 # attribute exists in the ConfigurableInstance wrapper 

146 object.__setattr__(self, name, value) 

147 else: 

148 if at is None: 

149 at = getCallStack() 

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

151 

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

153 """ 

154 Pretend to be an isntance of ConfigClass. 

155 Attributes defiend by ConfigurableInstance will shadow those defined 

156 in ConfigClass 

157 """ 

158 if self._config._frozen: 

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

160 

161 try: 

162 # attribute exists in the ConfigurableInstance wrapper 

163 object.__delattr__(self, name) 

164 except AttributeError: 

165 if at is None: 

166 at = getCallStack() 

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

168 

169 

170class ConfigurableField(Field): 

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

172 can be retargeted towards a different configurable (often a 

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

174 

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

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

177 

178 Parameters 

179 ---------- 

180 doc : `str` 

181 A description of the configuration field. 

182 target : configurable class 

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

184 attribute. Within the task framework, configurables are 

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

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

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

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

189 ``target.ConfigClass`` is used. 

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

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

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

193 check : callable, optional 

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

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

196 `False` otherwise). 

197 deprecated : None or `str`, optional 

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

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

200 

201 See also 

202 -------- 

203 ChoiceField 

204 ConfigChoiceField 

205 ConfigDictField 

206 ConfigField 

207 DictField 

208 Field 

209 ListField 

210 RangeField 

211 RegistryField 

212 

213 Notes 

214 ----- 

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

216 fully-configured configurable. 

217 """ 

218 

219 def validateTarget(self, target, ConfigClass): 

220 """Validate the target and configuration class. 

221 

222 Parameters 

223 ---------- 

224 target 

225 The configurable being verified. 

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

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

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

229 

230 Raises 

231 ------ 

232 AttributeError 

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

234 ``ConfigClass`` attribute. 

235 TypeError 

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

237 subclass. 

238 ValueError 

239 Raised if: 

240 

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

242 method). 

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

244 ``__module__`` or ``__name__`` attributes). 

245 """ 

246 if ConfigClass is None: 

247 try: 

248 ConfigClass = target.ConfigClass 

249 except Exception: 

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

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

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

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

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

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

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

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

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

259 return ConfigClass 

260 

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

262 ConfigClass = self.validateTarget(target, ConfigClass) 

263 

264 if default is None: 

265 default = ConfigClass 

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

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

268 (_typeStr(default), _typeStr(ConfigClass))) 

269 

270 source = getStackFrame() 

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

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

273 self.target = target 

274 self.ConfigClass = ConfigClass 

275 

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

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

278 if value is None: 

279 if at is None: 

280 at = getCallStack(1) 

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

282 instance._storage[self.name] = value 

283 return value 

284 

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

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

287 return self 

288 else: 

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

290 

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

292 if instance._frozen: 

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

294 if at is None: 

295 at = getCallStack() 

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

297 

298 if isinstance(value, ConfigurableInstance): 

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

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

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

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

303 elif value == oldValue.ConfigClass: 

304 value = oldValue.ConfigClass() 

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

306 else: 

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

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

309 raise FieldValidationError(self, instance, msg) 

310 

311 def rename(self, instance): 

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

313 value = self.__getOrMake(instance) 

314 value._rename(fullname) 

315 

316 def _collectImports(self, instance, imports): 

317 value = self.__get__(instance) 

318 target = value.target 

319 imports.add(target.__module__) 

320 value.value._collectImports() 

321 imports |= value.value._imports 

322 

323 def save(self, outfile, instance): 

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

325 value = self.__getOrMake(instance) 

326 target = value.target 

327 

328 if target != self.target: 

329 # not targeting the field-default target. 

330 # save target information 

331 ConfigClass = value.ConfigClass 

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

333 _typeStr(target), 

334 _typeStr(ConfigClass))) 

335 # save field values 

336 value._save(outfile) 

337 

338 def freeze(self, instance): 

339 value = self.__getOrMake(instance) 

340 value.freeze() 

341 

342 def toDict(self, instance): 

343 value = self.__get__(instance) 

344 return value.toDict() 

345 

346 def validate(self, instance): 

347 value = self.__get__(instance) 

348 value.validate() 

349 

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

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

352 raise FieldValidationError(self, instance, msg) 

353 

354 def __deepcopy__(self, memo): 

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

356 original typemap. 

357 

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

359 constructor signature! 

360 """ 

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

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

363 

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

365 """Compare two fields for equality. 

366 

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

368 

369 Parameters 

370 ---------- 

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

372 Left-hand side config instance to compare. 

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

374 Right-hand side config instance to compare. 

375 shortcut : `bool` 

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

377 rtol : `float` 

378 Relative tolerance for floating point comparisons. 

379 atol : `float` 

380 Absolute tolerance for floating point comparisons. 

381 output : callable 

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

383 report inequalities. For example: `print`. 

384 

385 Returns 

386 ------- 

387 isEqual : bool 

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

389 

390 Notes 

391 ----- 

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

393 """ 

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

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

396 name = getComparisonName( 

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

398 _joinNamePath(instance2._name, self.name) 

399 ) 

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