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 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 

33 

34class ConfigurableActionStructUpdater: 

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

36 update a ConfigurableActionStruct through attribute assignment. This is 

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

38 the command line. 

39 """ 

40 def __set__(self, instance: ConfigurableActionStruct, 

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

42 if isinstance(value, Mapping): 

43 pass 

44 elif isinstance(value, ConfigurableActionStruct): 

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

46 # internal dictionary 

47 value = value._attrs 

48 else: 

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

50 "mapping") 

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

52 setattr(instance, name, action) 

53 

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

55 # This descriptor does not support fetching any value 

56 return None 

57 

58 

59class ConfigurableActionStructRemover: 

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

61 of action names from a ConfigurableActionStruct at one time using 

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

63 configuration through pipelines or on the command line. 

64 

65 Raises 

66 ------ 

67 AttributeError 

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

69 ConfigurableActionStruct 

70 """ 

71 def __set__(self, instance: ConfigurableActionStruct, 

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

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

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

75 # to remove the attribute 

76 if isinstance(value, str): 

77 value = (value, ) 

78 for name in value: 

79 delattr(instance, name) 

80 

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

82 # This descriptor does not support fetching any value 

83 return None 

84 

85 

86class ConfigurableActionStruct: 

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

88 the ConfigurableActionStructField. This class should not be created 

89 directly. 

90 

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

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

93 

94 Attributes can be dynamically added or removed as such: 

95 

96 ConfigurableActionStructInstance.variable1 = a_configurable_action 

97 del ConfigurableActionStructInstance.variable1 

98 

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

100 `lsst.pex.config.Config` object. 

101 

102 ConfigurableActionStruct supports two special convenance attributes. 

103 

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

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

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

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

108 right hand side of the equals sign. 

109 

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

111 iterable of strings which correspond to attribute names on the 

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

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

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

115 have been removed from the `ConfigurableActionStruct` 

116 """ 

117 # declare attributes that are set with __setattr__ 

118 _config: Config 

119 _attrs: Dict[str, ConfigurableAction] 

120 _field: ConfigurableActionStructField 

121 _history: List[tuple] 

122 

123 # create descriptors to handle special update and remove behavior 

124 update = ConfigurableActionStructUpdater() 

125 remove = ConfigurableActionStructRemover() 

126 

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

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

129 object.__setattr__(self, '_config', config) 

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

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

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

133 

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

135 

136 if value is not None: 

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

138 setattr(self, k, v) 

139 

140 @property 

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

142 return self._history 

143 

144 @property 

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

146 return self._attrs.keys() 

147 

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

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

150 

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

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

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

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

155 

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

157 name = _joinNamePath(self._config._name, self._field.name, attr) 

158 if at is None: 

159 at = getCallStack() 

160 if isinstance(value, ConfigurableAction): 

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

162 else: 

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

164 self._attrs[attr] = valueInst 

165 else: 

166 super().__setattr__(attr, value) 

167 

168 def __getattr__(self, attr): 

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

170 return self._attrs[attr] 

171 else: 

172 super().__getattribute__(attr) 

173 

174 def __delattr__(self, name): 

175 if name in self._attrs: 

176 del self._attrs[name] 

177 else: 

178 super().__delattr__(name) 

179 

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

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

182 

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

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

185 

186 

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

188 

189 

190class ConfigurableActionStructField(Field): 

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

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

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

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

195 

196 This class implements a `ConfigurableActionStruct` as an intermediary 

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

198 futher information. 

199 """ 

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

201 # inheritance 

202 StructClass = ConfigurableActionStruct 

203 

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

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

206 name: str 

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

208 

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

210 optional: bool = False, 

211 deprecated=None): 

212 source = getStackFrame() 

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

214 optional=optional, source=source, deprecated=deprecated) 

215 

216 def __set__(self, instance: Config, 

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

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

219 if instance._frozen: 

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

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

222 raise FieldValidationError(self, instance, msg) 

223 

224 if at is None: 

225 at = getCallStack() 

226 

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

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

229 else: 

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

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

232 

233 if not isinstance(value, ConfigurableActionStruct): 

234 raise FieldValidationError(self, instance, 

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

236 instance._storage[self.name] = value 

237 

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

239 label: str = "default" 

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

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

242 return self 

243 else: 

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

245 return field 

246 

247 def rename(self, instance: Config): 

248 actionStruct: ConfigurableActionStruct = self.__get__(instance) 

249 if actionStruct is not None: 

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

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

252 v._rename(fullname) 

253 

254 def validate(self, instance): 

255 value = self.__get__(instance) 

256 if value is not None: 

257 for item in value: 

258 item.validate() 

259 

260 def toDict(self, instance): 

261 actionStruct = self.__get__(instance) 

262 if actionStruct is None: 

263 return None 

264 

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

266 

267 return dict_ 

268 

269 def save(self, outfile, instance): 

270 actionStruct = self.__get__(instance) 

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

272 if actionStruct is None: 

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

274 return 

275 

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

277 for v in actionStruct: 

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

279 v._save(outfile) 

280 

281 def freeze(self, instance): 

282 actionStruct = self.__get__(instance) 

283 if actionStruct is not None: 

284 for v in actionStruct: 

285 v.freeze() 

286 

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

288 """Compare two fields for equality. 

289 

290 Parameters 

291 ---------- 

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

293 Left-hand side config instance to compare. 

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

295 Right-hand side config instance to compare. 

296 shortcut : `bool` 

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

298 rtol : `float` 

299 Relative tolerance for floating point comparisons. 

300 atol : `float` 

301 Absolute tolerance for floating point comparisons. 

302 output : callable 

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

304 report inequalities. 

305 

306 Returns 

307 ------- 

308 isEqual : bool 

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

310 

311 Notes 

312 ----- 

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

314 """ 

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

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

317 name = getComparisonName( 

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

319 _joinNamePath(instance2._name, self.name) 

320 ) 

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

322 return False 

323 equal = True 

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

325 v2 = getattr(d2, k) 

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

327 rtol=rtol, atol=atol, output=output) 

328 if not result and shortcut: 

329 return False 

330 equal = equal and result 

331 return equal