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

167 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-25 09:13 +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 types import SimpleNamespace 

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

27 

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

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

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

31from lsst.pipe.base import Struct 

32 

33from . import ConfigurableAction 

34 

35import weakref 

36 

37 

38class ConfigurableActionStructUpdater: 

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

40 update a ConfigurableActionStruct through attribute assignment. This is 

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

42 the command line. 

43 """ 

44 def __set__(self, instance: ConfigurableActionStruct, 

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

46 if isinstance(value, Mapping): 

47 pass 

48 elif isinstance(value, ConfigurableActionStruct): 

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

50 # internal dictionary 

51 value = value._attrs 

52 else: 

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

54 "mapping") 

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

56 setattr(instance, name, action) 

57 

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

59 # This descriptor does not support fetching any value 

60 return None 

61 

62 

63class ConfigurableActionStructRemover: 

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

65 of action names from a ConfigurableActionStruct at one time using 

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

67 configuration through pipelines or on the command line. 

68 

69 Raises 

70 ------ 

71 AttributeError 

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

73 ConfigurableActionStruct 

74 """ 

75 def __set__(self, instance: ConfigurableActionStruct, 

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

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

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

79 # to remove the attribute 

80 if isinstance(value, str): 

81 value = (value, ) 

82 for name in value: 

83 delattr(instance, name) 

84 

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

86 # This descriptor does not support fetching any value 

87 return None 

88 

89 

90class ConfigurableActionStruct: 

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

92 the ConfigurableActionStructField. This class should not be created 

93 directly. 

94 

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

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

97 

98 Attributes can be dynamically added or removed as such: 

99 

100 ConfigurableActionStructInstance.variable1 = a_configurable_action 

101 del ConfigurableActionStructInstance.variable1 

102 

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

104 `lsst.pex.config.Config` object. 

105 

106 ConfigurableActionStruct supports two special convenance attributes. 

107 

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

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

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

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

112 right hand side of the equals sign. 

113 

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

115 iterable of strings which correspond to attribute names on the 

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

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

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

119 have been removed from the `ConfigurableActionStruct` 

120 """ 

121 # declare attributes that are set with __setattr__ 

122 _config: Config 

123 _attrs: Dict[str, ConfigurableAction] 

124 _field: ConfigurableActionStructField 

125 _history: List[tuple] 

126 

127 # create descriptors to handle special update and remove behavior 

128 update = ConfigurableActionStructUpdater() 

129 remove = ConfigurableActionStructRemover() 

130 

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

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

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

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

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

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

137 

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

139 

140 if value is not None: 

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

142 setattr(self, k, v) 

143 

144 @property 

145 def _config(self) -> Config: 

146 # Config Fields should never outlive their config class instance 

147 # assert that as such here 

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

149 return self._config_() 

150 

151 @property 

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

153 return self._history 

154 

155 @property 

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

157 return self._attrs.keys() 

158 

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

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

161 

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

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

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

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

166 

167 # verify that someone has not passed a string with a space or leading 

168 # number or something through the dict assignment update interface 

169 if not attr.isidentifier(): 

170 raise ValueError("Names used in ConfigurableStructs must be valid as python variable names") 

171 

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

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

174 name = _joinNamePath(base_name, attr) 

175 if at is None: 

176 at = getCallStack() 

177 if isinstance(value, ConfigurableAction): 

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

179 else: 

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

181 self._attrs[attr] = valueInst 

182 else: 

183 super().__setattr__(attr, value) 

184 

185 def __getattr__(self, attr): 

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

187 return self._attrs[attr] 

188 else: 

189 super().__getattribute__(attr) 

190 

191 def __delattr__(self, name): 

192 if name in self._attrs: 

193 del self._attrs[name] 

194 else: 

195 super().__delattr__(name) 

196 

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

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

199 

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

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

202 

203 

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

205 

206 

207class ConfigurableActionStructField(Field): 

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

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

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

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

212 

213 This class implements a `ConfigurableActionStruct` as an intermediary 

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

215 futher information. 

216 """ 

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

218 # inheritance 

219 StructClass = ConfigurableActionStruct 

220 

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

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

223 name: str 

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

225 

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

227 optional: bool = False, 

228 deprecated=None): 

229 source = getStackFrame() 

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

231 optional=optional, source=source, deprecated=deprecated) 

232 

233 def __set__(self, instance: Config, 

234 value: Union[None, Mapping[str, ConfigurableAction], 

235 ConfigurableActionStruct, 

236 ConfigurableActionStructField, 

237 Type[ConfigurableActionStructField]], 

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

239 if instance._frozen: 

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

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

242 raise FieldValidationError(self, instance, msg) 

243 

244 if at is None: 

245 at = getCallStack() 

246 

247 if value is None or (self.default is not None and self.default == value): 

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

249 else: 

250 # An actual value is being assigned check for what it is 

251 if isinstance(value, self.StructClass): 

252 # If this is a ConfigurableActionStruct, we need to make our own 

253 # copy that references this current field 

254 value = self.StructClass(instance, self, value._attrs, at=at, label=label) 

255 elif isinstance(value, (SimpleNamespace, Struct)): 

256 # If this is a a python analogous container, we need to make 

257 # a ConfigurableActionStruct initialized with this data 

258 value = self.StructClass(instance, self, vars(value), at=at, label=label) 

259 

260 elif type(value) == ConfigurableActionStructField: 

261 raise ValueError("ConfigurableActionStructFields can only be used in a class body declaration" 

262 f"Use a {self.StructClass}, SimpleNamespace or Struct") 

263 else: 

264 raise ValueError(f"Unrecognized value {value}, cannot be assigned to this field") 

265 

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

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

268 

269 if not isinstance(value, ConfigurableActionStruct): 

270 raise FieldValidationError(self, instance, 

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

272 instance._storage[self.name] = value 

273 

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

275 label: str = "default" 

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

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

278 return self 

279 else: 

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

281 return field 

282 

283 def rename(self, instance: Config): 

284 actionStruct: ConfigurableActionStruct = self.__get__(instance) 

285 if actionStruct is not None: 

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

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

288 fullname = _joinNamePath(base_name, k) 

289 v._rename(fullname) 

290 

291 def validate(self, instance): 

292 value = self.__get__(instance) 

293 if value is not None: 

294 for item in value: 

295 item.validate() 

296 

297 def toDict(self, instance): 

298 actionStruct = self.__get__(instance) 

299 if actionStruct is None: 

300 return None 

301 

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

303 

304 return dict_ 

305 

306 def save(self, outfile, instance): 

307 actionStruct = self.__get__(instance) 

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

309 if actionStruct is None: 

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

311 return 

312 

313 for v in actionStruct: 

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

315 v._save(outfile) 

316 

317 def freeze(self, instance): 

318 actionStruct = self.__get__(instance) 

319 if actionStruct is not None: 

320 for v in actionStruct: 

321 v.freeze() 

322 

323 def _collectImports(self, instance, imports): 

324 # docstring inherited from Field 

325 actionStruct = self.__get__(instance) 

326 for v in actionStruct: 

327 v._collectImports() 

328 imports |= v._imports 

329 

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

331 """Compare two fields for equality. 

332 

333 Parameters 

334 ---------- 

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

336 Left-hand side config instance to compare. 

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

338 Right-hand side config instance to compare. 

339 shortcut : `bool` 

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

341 rtol : `float` 

342 Relative tolerance for floating point comparisons. 

343 atol : `float` 

344 Absolute tolerance for floating point comparisons. 

345 output : callable 

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

347 report inequalities. 

348 

349 Returns 

350 ------- 

351 isEqual : bool 

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

353 

354 Notes 

355 ----- 

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

357 """ 

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

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

360 name = getComparisonName( 

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

362 _joinNamePath(instance2._name, self.name) 

363 ) 

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

365 return False 

366 equal = True 

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

368 v2 = getattr(d2, k) 

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

370 rtol=rtol, atol=atol, output=output) 

371 if not result and shortcut: 

372 return False 

373 equal = equal and result 

374 return equal