Coverage for python/lsst/pipe/tasks/configurableActions/_configurableActionStructField.py: 25%

Shortcuts 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

156 statements  

1# This file is part of pipe_tasks. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://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 program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21from __future__ import annotations 

22 

23__all__ = ("ConfigurableActionStructField", "ConfigurableActionStruct") 

24 

25from typing import Iterable, Mapping, Optional, TypeVar, Union, Type, Tuple, List, Any, Dict 

26 

27from lsst.pex.config.config import Config, Field, FieldValidationError, _typeStr, _joinNamePath 

28from lsst.pex.config.comparison import compareConfigs, compareScalars, getComparisonName 

29from lsst.pex.config.callStack import StackFrame, getCallStack, getStackFrame 

30 

31from . import ConfigurableAction 

32 

33import weakref 

34 

35 

36class ConfigurableActionStructUpdater: 

37 """This descriptor exists to abstract the logic of using a dictionary to 

38 update a ConfigurableActionStruct through attribute assignment. This is 

39 useful in the context of setting configuration through pipelines or on 

40 the command line. 

41 """ 

42 def __set__(self, instance: ConfigurableActionStruct, 

43 value: Union[Mapping[str, ConfigurableAction], ConfigurableActionStruct]) -> None: 

44 if isinstance(value, Mapping): 

45 pass 

46 elif isinstance(value, ConfigurableActionStruct): 

47 # If the update target is a ConfigurableActionStruct, get the 

48 # internal dictionary 

49 value = value._attrs 

50 else: 

51 raise ValueError("Can only update a ConfigurableActionStruct with an instance of such, or a " 

52 "mapping") 

53 for name, action in value.items(): 

54 setattr(instance, name, action) 

55 

56 def __get__(self, instance, objtype=None) -> None: 

57 # This descriptor does not support fetching any value 

58 return None 

59 

60 

61class ConfigurableActionStructRemover: 

62 """This descriptor exists to abstract the logic of removing an interable 

63 of action names from a ConfigurableActionStruct at one time using 

64 attribute assignment. This is useful in the context of setting 

65 configuration through pipelines or on the command line. 

66 

67 Raises 

68 ------ 

69 AttributeError 

70 Raised if an attribute specified for removal does not exist in the 

71 ConfigurableActionStruct 

72 """ 

73 def __set__(self, instance: ConfigurableActionStruct, 

74 value: Union[str, Iterable[str]]) -> None: 

75 # strings are iterable, but not in the way that is intended. If a 

76 # single name is specified, turn it into a tuple before attempting 

77 # to remove the attribute 

78 if isinstance(value, str): 

79 value = (value, ) 

80 for name in value: 

81 delattr(instance, name) 

82 

83 def __get__(self, instance, objtype=None) -> None: 

84 # This descriptor does not support fetching any value 

85 return None 

86 

87 

88class ConfigurableActionStruct: 

89 """A ConfigurableActionStruct is the storage backend class that supports 

90 the ConfigurableActionStructField. This class should not be created 

91 directly. 

92 

93 This class allows managing a collection of `ConfigurableActions` with a 

94 struct like interface, that is to say in an attribute like notation. 

95 

96 Attributes can be dynamically added or removed as such: 

97 

98 ConfigurableActionStructInstance.variable1 = a_configurable_action 

99 del ConfigurableActionStructInstance.variable1 

100 

101 Each action is then available to be individually configured as a normal 

102 `lsst.pex.config.Config` object. 

103 

104 ConfigurableActionStruct supports two special convenance attributes. 

105 

106 The first is `update`. You may assign a dict of `ConfigurableActions` or 

107 a `ConfigurableActionStruct` to this attribute which will update the 

108 `ConfigurableActionStruct` on which the attribute is invoked such that it 

109 will be updated to contain the entries specified by the structure on the 

110 right hand side of the equals sign. 

111 

112 The second convenience attribute is named remove. You may assign an 

113 iterable of strings which correspond to attribute names on the 

114 `ConfigurableActionStruct`. All of the corresponding attributes will then 

115 be removed. If any attribute does not exist, an `AttributeError` will be 

116 raised. Any attributes in the Iterable prior to the name which raises will 

117 have been removed from the `ConfigurableActionStruct` 

118 """ 

119 # declare attributes that are set with __setattr__ 

120 _config: Config 

121 _attrs: Dict[str, ConfigurableAction] 

122 _field: ConfigurableActionStructField 

123 _history: List[tuple] 

124 

125 # create descriptors to handle special update and remove behavior 

126 update = ConfigurableActionStructUpdater() 

127 remove = ConfigurableActionStructRemover() 

128 

129 def __init__(self, config: Config, field: ConfigurableActionStructField, 

130 value: Mapping[str, ConfigurableAction], at: Any, label: str): 

131 object.__setattr__(self, '_config_', weakref.ref(config)) 

132 object.__setattr__(self, '_attrs', {}) 

133 object.__setattr__(self, '_field', field) 

134 object.__setattr__(self, '_history', []) 

135 

136 self.history.append(("Struct initialized", at, label)) 

137 

138 if value is not None: 

139 for k, v in value.items(): 

140 setattr(self, k, v) 

141 

142 @property 

143 def _config(self) -> Config: 

144 # Config Fields should never outlive their config class instance 

145 # assert that as such here 

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

147 return self._config_() 

148 

149 @property 

150 def history(self) -> List[tuple]: 

151 return self._history 

152 

153 @property 

154 def fieldNames(self) -> Iterable[str]: 

155 return self._attrs.keys() 

156 

157 def __setattr__(self, attr: str, value: Union[ConfigurableAction, Type[ConfigurableAction]], 

158 at=None, label='setattr', setHistory=False) -> None: 

159 

160 if hasattr(self._config, '_frozen') and self._config._frozen: 

161 msg = "Cannot modify a frozen Config. "\ 

162 f"Attempting to set item {attr} to value {value}" 

163 raise FieldValidationError(self._field, self._config, msg) 

164 

165 if attr not in (self.__dict__.keys() | type(self).__dict__.keys()): 

166 base_name = _joinNamePath(self._config._name, self._field.name) 

167 name = _joinNamePath(base_name, attr) 

168 if at is None: 

169 at = getCallStack() 

170 if isinstance(value, ConfigurableAction): 

171 valueInst = type(value)(__name=name, __at=at, __label=label, **value._storage) 

172 else: 

173 valueInst = value(__name=name, __at=at, __label=label) 

174 self._attrs[attr] = valueInst 

175 else: 

176 super().__setattr__(attr, value) 

177 

178 def __getattr__(self, attr): 

179 if attr in object.__getattribute__(self, '_attrs'): 

180 return self._attrs[attr] 

181 else: 

182 super().__getattribute__(attr) 

183 

184 def __delattr__(self, name): 

185 if name in self._attrs: 

186 del self._attrs[name] 

187 else: 

188 super().__delattr__(name) 

189 

190 def __iter__(self) -> Iterable[ConfigurableAction]: 

191 return iter(self._attrs.values()) 

192 

193 def items(self) -> Iterable[Tuple[str, ConfigurableAction]]: 

194 return iter(self._attrs.items()) 

195 

196 

197T = TypeVar("T", bound="ConfigurableActionStructField") 

198 

199 

200class ConfigurableActionStructField(Field): 

201 r"""`ConfigurableActionStructField` is a `~lsst.pex.config.Field` subclass 

202 that allows `ConfigurableAction`\ s to be organized in a 

203 `~lsst.pex.config.Config` class in a manner similar to how a 

204 `~lsst.pipe.base.Struct` works. 

205 

206 This class implements a `ConfigurableActionStruct` as an intermediary 

207 object to organize the `ConfigurableActions`. See it's documentation for 

208 futher information. 

209 """ 

210 # specify StructClass to make this more generic for potential future 

211 # inheritance 

212 StructClass = ConfigurableActionStruct 

213 

214 # Explicitly annotate these on the class, they are present in the base 

215 # class through injection, so type systems have trouble seeing them. 

216 name: str 

217 default: Optional[Mapping[str, ConfigurableAction]] 

218 

219 def __init__(self, doc: str, default: Optional[Mapping[str, ConfigurableAction]] = None, 

220 optional: bool = False, 

221 deprecated=None): 

222 source = getStackFrame() 

223 self._setup(doc=doc, dtype=self.__class__, default=default, check=None, 

224 optional=optional, source=source, deprecated=deprecated) 

225 

226 def __set__(self, instance: Config, 

227 value: Union[None, Mapping[str, ConfigurableAction], ConfigurableActionStruct], 

228 at: Iterable[StackFrame] = None, label: str = 'assigment'): 

229 if instance._frozen: 

230 msg = "Cannot modify a frozen Config. "\ 

231 "Attempting to set field to value %s" % value 

232 raise FieldValidationError(self, instance, msg) 

233 

234 if at is None: 

235 at = getCallStack() 

236 

237 if value is None or value == self.default: 

238 value = self.StructClass(instance, self, value, at=at, label=label) 

239 else: 

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

241 history.append((value, at, label)) 

242 

243 if not isinstance(value, ConfigurableActionStruct): 

244 raise FieldValidationError(self, instance, 

245 "Can only assign things that are subclasses of Configurable Action") 

246 instance._storage[self.name] = value 

247 

248 def __get__(self: T, instance: Config, owner: None = None, at: Iterable[StackFrame] = None, 

249 label: str = "default" 

250 ) -> Union[None, T, ConfigurableActionStruct]: 

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

252 return self 

253 else: 

254 field: Optional[ConfigurableActionStruct] = instance._storage[self.name] 

255 return field 

256 

257 def rename(self, instance: Config): 

258 actionStruct: ConfigurableActionStruct = self.__get__(instance) 

259 if actionStruct is not None: 

260 for k, v in actionStruct.items(): 

261 base_name = _joinNamePath(instance._name, self.name) 

262 fullname = _joinNamePath(base_name, k) 

263 v._rename(fullname) 

264 

265 def validate(self, instance): 

266 value = self.__get__(instance) 

267 if value is not None: 

268 for item in value: 

269 item.validate() 

270 

271 def toDict(self, instance): 

272 actionStruct = self.__get__(instance) 

273 if actionStruct is None: 

274 return None 

275 

276 dict_ = {k: v.toDict() for k, v in actionStruct.items()} 

277 

278 return dict_ 

279 

280 def save(self, outfile, instance): 

281 actionStruct = self.__get__(instance) 

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

283 if actionStruct is None: 

284 outfile.write(u"{}={!r}\n".format(fullname, actionStruct)) 

285 return 

286 

287 for v in actionStruct: 

288 outfile.write(u"{}={}()\n".format(v._name, _typeStr(v))) 

289 v._save(outfile) 

290 

291 def freeze(self, instance): 

292 actionStruct = self.__get__(instance) 

293 if actionStruct is not None: 

294 for v in actionStruct: 

295 v.freeze() 

296 

297 def _collectImports(self, instance, imports): 

298 # docstring inherited from Field 

299 actionStruct = self.__get__(instance) 

300 for v in actionStruct: 

301 v._collectImports() 

302 imports |= v._imports 

303 

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

305 """Compare two fields for equality. 

306 

307 Parameters 

308 ---------- 

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

310 Left-hand side config instance to compare. 

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

312 Right-hand side config instance to compare. 

313 shortcut : `bool` 

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

315 rtol : `float` 

316 Relative tolerance for floating point comparisons. 

317 atol : `float` 

318 Absolute tolerance for floating point comparisons. 

319 output : callable 

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

321 report inequalities. 

322 

323 Returns 

324 ------- 

325 isEqual : bool 

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

327 

328 Notes 

329 ----- 

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

331 """ 

332 d1: ConfigurableActionStruct = getattr(instance1, self.name) 

333 d2: ConfigurableActionStruct = getattr(instance2, self.name) 

334 name = getComparisonName( 

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

336 _joinNamePath(instance2._name, self.name) 

337 ) 

338 if not compareScalars(f"keys for {name}", set(d1.fieldNames), set(d2.fieldNames), output=output): 

339 return False 

340 equal = True 

341 for k, v1 in d1.items(): 

342 v2 = getattr(d2, k) 

343 result = compareConfigs(f"{name}.{k}", v1, v2, shortcut=shortcut, 

344 rtol=rtol, atol=atol, output=output) 

345 if not result and shortcut: 

346 return False 

347 equal = equal and result 

348 return equal