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

150 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-01 21:12 +0000

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 name = _joinNamePath(self._config._name, self._field.name, attr) 

167 if at is None: 

168 at = getCallStack() 

169 if isinstance(value, ConfigurableAction): 

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

171 else: 

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

173 self._attrs[attr] = valueInst 

174 else: 

175 super().__setattr__(attr, value) 

176 

177 def __getattr__(self, attr): 

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

179 return self._attrs[attr] 

180 else: 

181 super().__getattribute__(attr) 

182 

183 def __delattr__(self, name): 

184 if name in self._attrs: 

185 del self._attrs[name] 

186 else: 

187 super().__delattr__(name) 

188 

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

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

191 

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

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

194 

195 

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

197 

198 

199class ConfigurableActionStructField(Field): 

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

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

202 `~lsst.pex.config.Config` class in a manor similar to how a 

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

204 

205 This class implements a `ConfigurableActionStruct` as an intermediary 

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

207 futher information. 

208 """ 

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

210 # inheritance 

211 StructClass = ConfigurableActionStruct 

212 

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

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

215 name: str 

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

217 

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

219 optional: bool = False, 

220 deprecated=None): 

221 source = getStackFrame() 

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

223 optional=optional, source=source, deprecated=deprecated) 

224 

225 def __set__(self, instance: Config, 

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

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

228 if instance._frozen: 

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

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

231 raise FieldValidationError(self, instance, msg) 

232 

233 if at is None: 

234 at = getCallStack() 

235 

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

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

238 else: 

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

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

241 

242 if not isinstance(value, ConfigurableActionStruct): 

243 raise FieldValidationError(self, instance, 

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

245 instance._storage[self.name] = value 

246 

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

248 label: str = "default" 

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

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

251 return self 

252 else: 

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

254 return field 

255 

256 def rename(self, instance: Config): 

257 actionStruct: ConfigurableActionStruct = self.__get__(instance) 

258 if actionStruct is not None: 

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

260 fullname = _joinNamePath(instance._name, self.name, k) 

261 v._rename(fullname) 

262 

263 def validate(self, instance): 

264 value = self.__get__(instance) 

265 if value is not None: 

266 for item in value: 

267 item.validate() 

268 

269 def toDict(self, instance): 

270 actionStruct = self.__get__(instance) 

271 if actionStruct is None: 

272 return None 

273 

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

275 

276 return dict_ 

277 

278 def save(self, outfile, instance): 

279 actionStruct = self.__get__(instance) 

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

281 if actionStruct is None: 

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

283 return 

284 

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

286 for v in actionStruct: 

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

288 v._save(outfile) 

289 

290 def freeze(self, instance): 

291 actionStruct = self.__get__(instance) 

292 if actionStruct is not None: 

293 for v in actionStruct: 

294 v.freeze() 

295 

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

297 """Compare two fields for equality. 

298 

299 Parameters 

300 ---------- 

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

302 Left-hand side config instance to compare. 

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

304 Right-hand side config instance to compare. 

305 shortcut : `bool` 

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

307 rtol : `float` 

308 Relative tolerance for floating point comparisons. 

309 atol : `float` 

310 Absolute tolerance for floating point comparisons. 

311 output : callable 

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

313 report inequalities. 

314 

315 Returns 

316 ------- 

317 isEqual : bool 

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

319 

320 Notes 

321 ----- 

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

323 """ 

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

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

326 name = getComparisonName( 

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

328 _joinNamePath(instance2._name, self.name) 

329 ) 

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

331 return False 

332 equal = True 

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

334 v2 = getattr(d2, k) 

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

336 rtol=rtol, atol=atol, output=output) 

337 if not result and shortcut: 

338 return False 

339 equal = equal and result 

340 return equal