Coverage for python/lsst/pex/config/configurableActions/_configurableActionStructField.py: 22%

181 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 10:00 +0000

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

25import weakref 

26from types import GenericAlias, SimpleNamespace 

27from typing import ( 

28 Any, 

29 Dict, 

30 Generic, 

31 Iterable, 

32 Iterator, 

33 List, 

34 Mapping, 

35 Optional, 

36 Tuple, 

37 Type, 

38 TypeVar, 

39 Union, 

40 overload, 

41) 

42 

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

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

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

46 

47from . import ActionTypeVar, ConfigurableAction 

48 

49 

50class ConfigurableActionStructUpdater: 

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

52 update a ConfigurableActionStruct through attribute assignment. This is 

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

54 the command line. 

55 """ 

56 

57 def __set__( 

58 self, 

59 instance: ConfigurableActionStruct, 

60 value: Union[Mapping[str, ConfigurableAction], ConfigurableActionStruct], 

61 ) -> None: 

62 if isinstance(value, Mapping): 

63 pass 

64 elif isinstance(value, ConfigurableActionStruct): 

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

66 # internal dictionary 

67 value = value._attrs 

68 else: 

69 raise ValueError( 

70 "Can only update a ConfigurableActionStruct with an instance of such, or a " "mapping" 

71 ) 

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

73 setattr(instance, name, action) 

74 

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

76 # This descriptor does not support fetching any value 

77 return None 

78 

79 

80class ConfigurableActionStructRemover: 

81 """This descriptor exists to abstract the logic of removing an iterable 

82 of action names from a ConfigurableActionStruct at one time using 

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

84 configuration through pipelines or on the command line. 

85 

86 Raises 

87 ------ 

88 AttributeError 

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

90 ConfigurableActionStruct 

91 """ 

92 

93 def __set__(self, instance: ConfigurableActionStruct, value: Union[str, Iterable[str]]) -> None: 

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

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

96 # to remove the attribute 

97 if isinstance(value, str): 

98 value = (value,) 

99 for name in value: 

100 delattr(instance, name) 

101 

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

103 # This descriptor does not support fetching any value 

104 return None 

105 

106 

107class ConfigurableActionStruct(Generic[ActionTypeVar]): 

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

109 the ConfigurableActionStructField. This class should not be created 

110 directly. 

111 

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

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

114 

115 Attributes can be dynamically added or removed as such: 

116 

117 ConfigurableActionStructInstance.variable1 = a_configurable_action 

118 del ConfigurableActionStructInstance.variable1 

119 

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

121 `lsst.pex.config.Config` object. 

122 

123 ConfigurableActionStruct supports two special convenience attributes. 

124 

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

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

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

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

129 right hand side of the equals sign. 

130 

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

132 iterable of strings which correspond to attribute names on the 

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

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

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

136 have been removed from the `ConfigurableActionStruct` 

137 """ 

138 

139 # declare attributes that are set with __setattr__ 

140 _config_: weakref.ref 

141 _attrs: Dict[str, ActionTypeVar] 

142 _field: ConfigurableActionStructField 

143 _history: List[tuple] 

144 

145 # create descriptors to handle special update and remove behavior 

146 update = ConfigurableActionStructUpdater() 

147 remove = ConfigurableActionStructRemover() 

148 

149 def __init__( 

150 self, 

151 config: Config, 

152 field: ConfigurableActionStructField, 

153 value: Mapping[str, ConfigurableAction], 

154 at: Any, 

155 label: str, 

156 ): 

157 object.__setattr__(self, "_config_", weakref.ref(config)) 

158 object.__setattr__(self, "_attrs", {}) 

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

160 object.__setattr__(self, "_history", []) 

161 

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

163 

164 if value is not None: 

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

166 setattr(self, k, v) 

167 

168 @property 

169 def _config(self) -> Config: 

170 # Config Fields should never outlive their config class instance 

171 # assert that as such here 

172 value = self._config_() 

173 assert value is not None 

174 return value 

175 

176 @property 

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

178 return self._history 

179 

180 @property 

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

182 return self._attrs.keys() 

183 

184 def __setattr__( 

185 self, 

186 attr: str, 

187 value: Union[ActionTypeVar, Type[ActionTypeVar]], 

188 at=None, 

189 label="setattr", 

190 setHistory=False, 

191 ) -> None: 

192 if hasattr(self._config, "_frozen") and self._config._frozen: 

193 msg = "Cannot modify a frozen Config. " f"Attempting to set item {attr} to value {value}" 

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

195 

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

197 # number or something through the dict assignment update interface 

198 if not attr.isidentifier(): 

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

200 

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

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

203 name = _joinNamePath(base_name, attr) 

204 if at is None: 

205 at = getCallStack() 

206 if isinstance(value, ConfigurableAction): 

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

208 else: 

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

210 self._attrs[attr] = valueInst 

211 else: 

212 super().__setattr__(attr, value) 

213 

214 def __getattr__(self, attr) -> Any: 

215 if attr in object.__getattribute__(self, "_attrs"): 

216 result = self._attrs[attr] 

217 result.identity = attr 

218 return result 

219 else: 

220 super().__getattribute__(attr) 

221 

222 def __delattr__(self, name): 

223 if name in self._attrs: 

224 del self._attrs[name] 

225 else: 

226 super().__delattr__(name) 

227 

228 def __iter__(self) -> Iterator[ActionTypeVar]: 

229 for name in self.fieldNames: 

230 yield getattr(self, name) 

231 

232 def items(self) -> Iterable[Tuple[str, ActionTypeVar]]: 

233 for name in self.fieldNames: 

234 yield name, getattr(self, name) 

235 

236 def __bool__(self) -> bool: 

237 return bool(self._attrs) 

238 

239 

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

241 

242 

243class ConfigurableActionStructField(Field[ActionTypeVar]): 

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

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

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

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

248 

249 This class uses a `ConfigurableActionStruct` as an intermediary 

250 object to organize the `ConfigurableActions`. See its documentation for 

251 further information. 

252 """ 

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

254 # inheritance 

255 StructClass = ConfigurableActionStruct 

256 

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

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

259 name: str 

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

261 

262 def __init__( 

263 self, 

264 doc: str, 

265 default: Optional[Mapping[str, ConfigurableAction]] = None, 

266 optional: bool = False, 

267 deprecated=None, 

268 ): 

269 source = getStackFrame() 

270 self._setup( 

271 doc=doc, 

272 dtype=self.__class__, 

273 default=default, 

274 check=None, 

275 optional=optional, 

276 source=source, 

277 deprecated=deprecated, 

278 ) 

279 

280 def __class_getitem__(cls, params): 

281 return GenericAlias(cls, params) 

282 

283 def __set__( 

284 self, 

285 instance: Config, 

286 value: Union[ 

287 None, 

288 Mapping[str, ConfigurableAction], 

289 SimpleNamespace, 

290 ConfigurableActionStruct, 

291 ConfigurableActionStructField, 

292 Type[ConfigurableActionStructField], 

293 ], 

294 at: Iterable[StackFrame] = None, 

295 label: str = "assigment", 

296 ): 

297 if instance._frozen: 

298 msg = "Cannot modify a frozen Config. " "Attempting to set field to value %s" % value 

299 raise FieldValidationError(self, instance, msg) 

300 

301 if at is None: 

302 at = getCallStack() 

303 

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

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

306 else: 

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

308 if isinstance(value, self.StructClass): 

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

310 # own copy that references this current field 

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

312 elif isinstance(value, SimpleNamespace): 

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

314 # a ConfigurableActionStruct initialized with this data 

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

316 

317 elif type(value) == ConfigurableActionStructField: 

318 raise ValueError( 

319 "ConfigurableActionStructFields can only be used in a class body declaration" 

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

321 ) 

322 else: 

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

324 

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

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

327 

328 if not isinstance(value, ConfigurableActionStruct): 

329 raise FieldValidationError( 

330 self, instance, "Can only assign things that are subclasses of Configurable Action" 

331 ) 

332 instance._storage[self.name] = value 

333 

334 @overload 

335 def __get__( 

336 self, instance: None, owner: Any = None, at: Any = None, label: str = "default" 

337 ) -> ConfigurableActionStruct[ActionTypeVar]: 

338 ... 

339 

340 @overload 

341 def __get__( 

342 self, instance: Config, owner: Any = None, at: Any = None, label: str = "default" 

343 ) -> ConfigurableActionStruct[ActionTypeVar]: 

344 ... 

345 

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

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

348 return self 

349 else: 

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

351 return field 

352 

353 def rename(self, instance: Config): 

354 actionStruct: ConfigurableActionStruct = self.__get__(instance) 

355 if actionStruct is not None: 

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

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

358 fullname = _joinNamePath(base_name, k) 

359 v._rename(fullname) 

360 

361 def validate(self, instance: Config): 

362 value = self.__get__(instance) 

363 if value is not None: 

364 for item in value: 

365 item.validate() 

366 

367 def toDict(self, instance): 

368 actionStruct = self.__get__(instance) 

369 if actionStruct is None: 

370 return None 

371 

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

373 

374 return dict_ 

375 

376 def save(self, outfile, instance): 

377 actionStruct = self.__get__(instance) 

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

379 

380 # Ensure that a struct is always empty before assigning to it. 

381 outfile.write(f"{fullname}=None\n") 

382 

383 if actionStruct is None: 

384 return 

385 

386 for _, v in sorted(actionStruct.items()): 

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

388 v._save(outfile) 

389 

390 def freeze(self, instance): 

391 actionStruct = self.__get__(instance) 

392 if actionStruct is not None: 

393 for v in actionStruct: 

394 v.freeze() 

395 

396 def _collectImports(self, instance, imports): 

397 # docstring inherited from Field 

398 actionStruct = self.__get__(instance) 

399 for v in actionStruct: 

400 v._collectImports() 

401 imports |= v._imports 

402 

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

404 """Compare two fields for equality. 

405 

406 Parameters 

407 ---------- 

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

409 Left-hand side config instance to compare. 

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

411 Right-hand side config instance to compare. 

412 shortcut : `bool` 

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

414 rtol : `float` 

415 Relative tolerance for floating point comparisons. 

416 atol : `float` 

417 Absolute tolerance for floating point comparisons. 

418 output : callable 

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

420 report inequalities. 

421 

422 Returns 

423 ------- 

424 isEqual : bool 

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

426 

427 Notes 

428 ----- 

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

430 """ 

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

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

433 name = getComparisonName( 

434 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name) 

435 ) 

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

437 return False 

438 equal = True 

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

440 v2 = getattr(d2, k) 

441 result = compareConfigs( 

442 f"{name}.{k}", v1, v2, shortcut=shortcut, rtol=rtol, atol=atol, output=output 

443 ) 

444 if not result and shortcut: 

445 return False 

446 equal = equal and result 

447 return equal