Coverage for python/lsst/pex/config/listField.py: 28%

194 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-10 09:56 +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# (http://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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

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

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

18# (at your option) any later version. 

19# 

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

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

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

23# GNU General Public License for more details. 

24# 

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

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

27 

28__all__ = ["ListField"] 

29 

30import collections.abc 

31import weakref 

32from collections.abc import Iterable, MutableSequence 

33from typing import Any, Generic, overload 

34 

35from .callStack import getCallStack, getStackFrame 

36from .comparison import compareScalars, getComparisonName 

37from .config import ( 

38 Config, 

39 Field, 

40 FieldTypeVar, 

41 FieldValidationError, 

42 UnexpectedProxyUsageError, 

43 _autocast, 

44 _joinNamePath, 

45 _typeStr, 

46) 

47 

48 

49class List(collections.abc.MutableSequence[FieldTypeVar]): 

50 """List collection used internally by `ListField`. 

51 

52 Parameters 

53 ---------- 

54 config : `lsst.pex.config.Config` 

55 Config instance that contains the ``field``. 

56 field : `ListField` 

57 Instance of the `ListField` using this ``List``. 

58 value : sequence 

59 Sequence of values that are inserted into this ``List``. 

60 at : `list` of `lsst.pex.config.callStack.StackFrame` 

61 The call stack (created by `lsst.pex.config.callStack.getCallStack`). 

62 label : `str` 

63 Event label for the history. 

64 setHistory : `bool`, optional 

65 Enable setting the field's history, using the value of the ``at`` 

66 parameter. Default is `True`. 

67 

68 Raises 

69 ------ 

70 FieldValidationError 

71 Raised if an item in the ``value`` parameter does not have the 

72 appropriate type for this field or does not pass the 

73 `ListField.itemCheck` method of the ``field`` parameter. 

74 """ 

75 

76 def __init__(self, config, field, value, at, label, setHistory=True): 

77 self._field = field 

78 self._config_ = weakref.ref(config) 

79 self._history = self._config._history.setdefault(self._field.name, []) 

80 self._list = [] 

81 self.__doc__ = field.doc 

82 if value is not None: 

83 try: 

84 for i, x in enumerate(value): 

85 self.insert(i, x, setHistory=False) 

86 except TypeError: 

87 msg = f"Value {value} is of incorrect type {_typeStr(value)}. Sequence type expected" 

88 raise FieldValidationError(self._field, config, msg) 

89 if setHistory: 

90 self.history.append((list(self._list), at, label)) 

91 

92 @property 

93 def _config(self) -> Config: 

94 # Config Fields should never outlive their config class instance 

95 # assert that as such here 

96 value = self._config_() 

97 assert value is not None 

98 return value 

99 

100 def validateItem(self, i, x): 

101 """Validate an item to determine if it can be included in the list. 

102 

103 Parameters 

104 ---------- 

105 i : `int` 

106 Index of the item in the `list`. 

107 x : object 

108 Item in the `list`. 

109 

110 Raises 

111 ------ 

112 FieldValidationError 

113 Raised if an item in the ``value`` parameter does not have the 

114 appropriate type for this field or does not pass the field's 

115 `ListField.itemCheck` method. 

116 """ 

117 if not isinstance(x, self._field.itemtype) and x is not None: 

118 msg = "Item at position %d with value %s is of incorrect type %s. Expected %s" % ( 

119 i, 

120 x, 

121 _typeStr(x), 

122 _typeStr(self._field.itemtype), 

123 ) 

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

125 

126 if self._field.itemCheck is not None and not self._field.itemCheck(x): 

127 msg = "Item at position %d is not a valid value: %s" % (i, x) 

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

129 

130 def list(self): 

131 """Sequence of items contained by the `List` (`list`).""" 

132 return self._list 

133 

134 history = property(lambda x: x._history) 134 ↛ exitline 134 didn't run the lambda on line 134

135 """Read-only history. 

136 """ 

137 

138 def __contains__(self, x): 

139 return x in self._list 

140 

141 def __len__(self): 

142 return len(self._list) 

143 

144 @overload 

145 def __setitem__( 

146 self, i: int, x: FieldTypeVar, at: Any = None, label: str = "setitem", setHistory: bool = True 

147 ) -> None: 

148 ... 

149 

150 @overload 

151 def __setitem__( 

152 self, 

153 i: slice, 

154 x: Iterable[FieldTypeVar], 

155 at: Any = None, 

156 label: str = "setitem", 

157 setHistory: bool = True, 

158 ) -> None: 

159 ... 

160 

161 def __setitem__(self, i, x, at=None, label="setitem", setHistory=True): 

162 if self._config._frozen: 

163 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config") 

164 if isinstance(i, slice): 

165 k, stop, step = i.indices(len(self)) 

166 for j, xj in enumerate(x): 

167 xj = _autocast(xj, self._field.itemtype) 

168 self.validateItem(k, xj) 

169 x[j] = xj 

170 k += step 

171 else: 

172 x = _autocast(x, self._field.itemtype) 

173 self.validateItem(i, x) 

174 

175 self._list[i] = x 

176 if setHistory: 

177 if at is None: 

178 at = getCallStack() 

179 self.history.append((list(self._list), at, label)) 

180 

181 @overload 

182 def __getitem__(self, i: int) -> FieldTypeVar: 

183 ... 

184 

185 @overload 

186 def __getitem__(self, i: slice) -> MutableSequence[FieldTypeVar]: 

187 ... 

188 

189 def __getitem__(self, i): 

190 return self._list[i] 

191 

192 def __delitem__(self, i, at=None, label="delitem", setHistory=True): 

193 if self._config._frozen: 

194 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config") 

195 del self._list[i] 

196 if setHistory: 

197 if at is None: 

198 at = getCallStack() 

199 self.history.append((list(self._list), at, label)) 

200 

201 def __iter__(self): 

202 return iter(self._list) 

203 

204 def insert(self, i, x, at=None, label="insert", setHistory=True): 

205 """Insert an item into the list at the given index. 

206 

207 Parameters 

208 ---------- 

209 i : `int` 

210 Index where the item is inserted. 

211 x : object 

212 Item that is inserted. 

213 at : `list` of `lsst.pex.config.callStack.StackFrame`, optional 

214 The call stack (created by 

215 `lsst.pex.config.callStack.getCallStack`). 

216 label : `str`, optional 

217 Event label for the history. 

218 setHistory : `bool`, optional 

219 Enable setting the field's history, using the value of the ``at`` 

220 parameter. Default is `True`. 

221 """ 

222 if at is None: 

223 at = getCallStack() 

224 self.__setitem__(slice(i, i), [x], at=at, label=label, setHistory=setHistory) 

225 

226 def __repr__(self): 

227 return repr(self._list) 

228 

229 def __str__(self): 

230 return str(self._list) 

231 

232 def __eq__(self, other): 

233 try: 

234 if len(self) != len(other): 

235 return False 

236 

237 for i, j in zip(self, other): 

238 if i != j: 

239 return False 

240 return True 

241 except AttributeError: 

242 # other is not a sequence type 

243 return False 

244 

245 def __ne__(self, other): 

246 return not self.__eq__(other) 

247 

248 def __setattr__(self, attr, value, at=None, label="assignment"): 

249 if hasattr(getattr(self.__class__, attr, None), "__set__"): 

250 # This allows properties to work. 

251 object.__setattr__(self, attr, value) 

252 elif attr in self.__dict__ or attr in ["_field", "_config_", "_history", "_list", "__doc__"]: 

253 # This allows specific private attributes to work. 

254 object.__setattr__(self, attr, value) 

255 else: 

256 # We throw everything else. 

257 msg = f"{_typeStr(self._field)} has no attribute {attr}" 

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

259 

260 def __reduce__(self): 

261 raise UnexpectedProxyUsageError( 

262 f"Proxy container for config field {self._field.name} cannot " 

263 "be pickled; it should be converted to a built-in container before " 

264 "being assigned to other objects or variables." 

265 ) 

266 

267 

268class ListField(Field[List[FieldTypeVar]], Generic[FieldTypeVar]): 

269 """A configuration field (`~lsst.pex.config.Field` subclass) that contains 

270 a list of values of a specific type. 

271 

272 Parameters 

273 ---------- 

274 doc : `str` 

275 A description of the field. 

276 dtype : class, optional 

277 The data type of items in the list. Optional if supplied as typing 

278 argument to the class. 

279 default : sequence, optional 

280 The default items for the field. 

281 optional : `bool`, optional 

282 Set whether the field is *optional*. When `False`, 

283 `lsst.pex.config.Config.validate` will fail if the field's value is 

284 `None`. 

285 listCheck : callable, optional 

286 A callable that validates the list as a whole. 

287 itemCheck : callable, optional 

288 A callable that validates individual items in the list. 

289 length : `int`, optional 

290 If set, this field must contain exactly ``length`` number of items. 

291 minLength : `int`, optional 

292 If set, this field must contain *at least* ``minLength`` number of 

293 items. 

294 maxLength : `int`, optional 

295 If set, this field must contain *no more than* ``maxLength`` number of 

296 items. 

297 deprecated : None or `str`, optional 

298 A description of why this Field is deprecated, including removal date. 

299 If not None, the string is appended to the docstring for this Field. 

300 

301 See Also 

302 -------- 

303 ChoiceField 

304 ConfigChoiceField 

305 ConfigDictField 

306 ConfigField 

307 ConfigurableField 

308 DictField 

309 Field 

310 RangeField 

311 RegistryField 

312 """ 

313 

314 def __init__( 

315 self, 

316 doc, 

317 dtype=None, 

318 default=None, 

319 optional=False, 

320 listCheck=None, 

321 itemCheck=None, 

322 length=None, 

323 minLength=None, 

324 maxLength=None, 

325 deprecated=None, 

326 ): 

327 if dtype is None: 327 ↛ 328line 327 didn't jump to line 328, because the condition on line 327 was never true

328 raise ValueError( 

329 "dtype must either be supplied as an argument or as a type argument to the class" 

330 ) 

331 if dtype not in Field.supportedTypes: 331 ↛ 332line 331 didn't jump to line 332, because the condition on line 331 was never true

332 raise ValueError("Unsupported dtype %s" % _typeStr(dtype)) 

333 if length is not None: 

334 if length <= 0: 334 ↛ 335line 334 didn't jump to line 335, because the condition on line 334 was never true

335 raise ValueError("'length' (%d) must be positive" % length) 

336 minLength = None 

337 maxLength = None 

338 else: 

339 if maxLength is not None and maxLength <= 0: 339 ↛ 340line 339 didn't jump to line 340, because the condition on line 339 was never true

340 raise ValueError("'maxLength' (%d) must be positive" % maxLength) 

341 if minLength is not None and maxLength is not None and minLength > maxLength: 341 ↛ 342line 341 didn't jump to line 342, because the condition on line 341 was never true

342 raise ValueError( 

343 "'maxLength' (%d) must be at least as large as 'minLength' (%d)" % (maxLength, minLength) 

344 ) 

345 

346 if listCheck is not None and not hasattr(listCheck, "__call__"): 346 ↛ 347line 346 didn't jump to line 347, because the condition on line 346 was never true

347 raise ValueError("'listCheck' must be callable") 

348 if itemCheck is not None and not hasattr(itemCheck, "__call__"): 348 ↛ 349line 348 didn't jump to line 349, because the condition on line 348 was never true

349 raise ValueError("'itemCheck' must be callable") 

350 

351 source = getStackFrame() 

352 self._setup( 

353 doc=doc, 

354 dtype=List, 

355 default=default, 

356 check=None, 

357 optional=optional, 

358 source=source, 

359 deprecated=deprecated, 

360 ) 

361 

362 self.listCheck = listCheck 

363 """Callable used to check the list as a whole. 

364 """ 

365 

366 self.itemCheck = itemCheck 

367 """Callable used to validate individual items as they are inserted 

368 into the list. 

369 """ 

370 

371 self.itemtype = dtype 

372 """Data type of list items. 

373 """ 

374 

375 self.length = length 

376 """Number of items that must be present in the list (or `None` to 

377 disable checking the list's length). 

378 """ 

379 

380 self.minLength = minLength 

381 """Minimum number of items that must be present in the list (or `None` 

382 to disable checking the list's minimum length). 

383 """ 

384 

385 self.maxLength = maxLength 

386 """Maximum number of items that must be present in the list (or `None` 

387 to disable checking the list's maximum length). 

388 """ 

389 

390 def validate(self, instance): 

391 """Validate the field. 

392 

393 Parameters 

394 ---------- 

395 instance : `lsst.pex.config.Config` 

396 The config instance that contains this field. 

397 

398 Raises 

399 ------ 

400 lsst.pex.config.FieldValidationError 

401 Raised if: 

402 

403 - The field is not optional, but the value is `None`. 

404 - The list itself does not meet the requirements of the ``length``, 

405 ``minLength``, or ``maxLength`` attributes. 

406 - The ``listCheck`` callable returns `False`. 

407 

408 Notes 

409 ----- 

410 Individual item checks (``itemCheck``) are applied when each item is 

411 set and are not re-checked by this method. 

412 """ 

413 Field.validate(self, instance) 

414 value = self.__get__(instance) 

415 if value is not None: 

416 lenValue = len(value) 

417 if self.length is not None and not lenValue == self.length: 

418 msg = "Required list length=%d, got length=%d" % (self.length, lenValue) 

419 raise FieldValidationError(self, instance, msg) 

420 elif self.minLength is not None and lenValue < self.minLength: 

421 msg = "Minimum allowed list length=%d, got length=%d" % (self.minLength, lenValue) 

422 raise FieldValidationError(self, instance, msg) 

423 elif self.maxLength is not None and lenValue > self.maxLength: 

424 msg = "Maximum allowed list length=%d, got length=%d" % (self.maxLength, lenValue) 

425 raise FieldValidationError(self, instance, msg) 

426 elif self.listCheck is not None and not self.listCheck(value): 

427 msg = "%s is not a valid value" % str(value) 

428 raise FieldValidationError(self, instance, msg) 

429 

430 def __set__( 

431 self, 

432 instance: Config, 

433 value: Iterable[FieldTypeVar] | None, 

434 at: Any = None, 

435 label: str = "assignment", 

436 ) -> None: 

437 if instance._frozen: 

438 raise FieldValidationError(self, instance, "Cannot modify a frozen Config") 

439 

440 if at is None: 

441 at = getCallStack() 

442 

443 if value is not None: 

444 value = List(instance, self, value, at, label) 

445 else: 

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

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

448 

449 instance._storage[self.name] = value 

450 

451 def toDict(self, instance): 

452 """Convert the value of this field to a plain `list`. 

453 

454 `lsst.pex.config.Config.toDict` is the primary user of this method. 

455 

456 Parameters 

457 ---------- 

458 instance : `lsst.pex.config.Config` 

459 The config instance that contains this field. 

460 

461 Returns 

462 ------- 

463 `list` 

464 Plain `list` of items, or `None` if the field is not set. 

465 """ 

466 value = self.__get__(instance) 

467 return list(value) if value is not None else None 

468 

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

470 """Compare two config instances for equality with respect to this 

471 field. 

472 

473 `lsst.pex.config.config.compare` is the primary user of this method. 

474 

475 Parameters 

476 ---------- 

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

478 Left-hand-side `~lsst.pex.config.Config` instance in the 

479 comparison. 

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

481 Right-hand-side `~lsst.pex.config.Config` instance in the 

482 comparison. 

483 shortcut : `bool` 

484 If `True`, return as soon as an **inequality** is found. 

485 rtol : `float` 

486 Relative tolerance for floating point comparisons. 

487 atol : `float` 

488 Absolute tolerance for floating point comparisons. 

489 output : callable 

490 If not None, a callable that takes a `str`, used (possibly 

491 repeatedly) to report inequalities. 

492 

493 Returns 

494 ------- 

495 equal : `bool` 

496 `True` if the fields are equal; `False` otherwise. 

497 

498 Notes 

499 ----- 

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

501 """ 

502 l1 = getattr(instance1, self.name) 

503 l2 = getattr(instance2, self.name) 

504 name = getComparisonName( 

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

506 ) 

507 if not compareScalars("isnone for %s" % name, l1 is None, l2 is None, output=output): 

508 return False 

509 if l1 is None and l2 is None: 

510 return True 

511 if not compareScalars("size for %s" % name, len(l1), len(l2), output=output): 

512 return False 

513 equal = True 

514 for n, v1, v2 in zip(range(len(l1)), l1, l2): 

515 result = compareScalars( 

516 "%s[%d]" % (name, n), v1, v2, dtype=self.dtype, rtol=rtol, atol=atol, output=output 

517 ) 

518 if not result and shortcut: 

519 return False 

520 equal = equal and result 

521 return equal