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

194 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-20 11:16 +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` or `None`,\ 

214 optional 

215 The call stack (created by 

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

217 label : `str`, optional 

218 Event label for the history. 

219 setHistory : `bool`, optional 

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

221 parameter. Default is `True`. 

222 """ 

223 if at is None: 

224 at = getCallStack() 

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

226 

227 def __repr__(self): 

228 return repr(self._list) 

229 

230 def __str__(self): 

231 return str(self._list) 

232 

233 def __eq__(self, other): 

234 try: 

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

236 return False 

237 

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

239 if i != j: 

240 return False 

241 return True 

242 except AttributeError: 

243 # other is not a sequence type 

244 return False 

245 

246 def __ne__(self, other): 

247 return not self.__eq__(other) 

248 

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

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

251 # This allows properties to work. 

252 object.__setattr__(self, attr, value) 

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

254 # This allows specific private attributes to work. 

255 object.__setattr__(self, attr, value) 

256 else: 

257 # We throw everything else. 

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

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

260 

261 def __reduce__(self): 

262 raise UnexpectedProxyUsageError( 

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

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

265 "being assigned to other objects or variables." 

266 ) 

267 

268 

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

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

271 a list of values of a specific type. 

272 

273 Parameters 

274 ---------- 

275 doc : `str` 

276 A description of the field. 

277 dtype : class, optional 

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

279 argument to the class. 

280 default : sequence, optional 

281 The default items for the field. 

282 optional : `bool`, optional 

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

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

285 `None`. 

286 listCheck : callable, optional 

287 A callable that validates the list as a whole. 

288 itemCheck : callable, optional 

289 A callable that validates individual items in the list. 

290 length : `int`, optional 

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

292 minLength : `int`, optional 

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

294 items. 

295 maxLength : `int`, optional 

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

297 items. 

298 deprecated : None or `str`, optional 

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

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

301 

302 See Also 

303 -------- 

304 ChoiceField 

305 ConfigChoiceField 

306 ConfigDictField 

307 ConfigField 

308 ConfigurableField 

309 DictField 

310 Field 

311 RangeField 

312 RegistryField 

313 """ 

314 

315 def __init__( 

316 self, 

317 doc, 

318 dtype=None, 

319 default=None, 

320 optional=False, 

321 listCheck=None, 

322 itemCheck=None, 

323 length=None, 

324 minLength=None, 

325 maxLength=None, 

326 deprecated=None, 

327 ): 

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

329 raise ValueError( 

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

331 ) 

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

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

334 if length is not None: 

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

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

337 minLength = None 

338 maxLength = None 

339 else: 

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

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

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

343 raise ValueError( 

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

345 ) 

346 

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

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

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

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

351 

352 source = getStackFrame() 

353 self._setup( 

354 doc=doc, 

355 dtype=List, 

356 default=default, 

357 check=None, 

358 optional=optional, 

359 source=source, 

360 deprecated=deprecated, 

361 ) 

362 

363 self.listCheck = listCheck 

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

365 """ 

366 

367 self.itemCheck = itemCheck 

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

369 into the list. 

370 """ 

371 

372 self.itemtype = dtype 

373 """Data type of list items. 

374 """ 

375 

376 self.length = length 

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

378 disable checking the list's length). 

379 """ 

380 

381 self.minLength = minLength 

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

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

384 """ 

385 

386 self.maxLength = maxLength 

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

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

389 """ 

390 

391 def validate(self, instance): 

392 """Validate the field. 

393 

394 Parameters 

395 ---------- 

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

397 The config instance that contains this field. 

398 

399 Raises 

400 ------ 

401 lsst.pex.config.FieldValidationError 

402 Raised if: 

403 

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

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

406 ``minLength``, or ``maxLength`` attributes. 

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

408 

409 Notes 

410 ----- 

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

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

413 """ 

414 Field.validate(self, instance) 

415 value = self.__get__(instance) 

416 if value is not None: 

417 lenValue = len(value) 

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

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

420 raise FieldValidationError(self, instance, msg) 

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

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

423 raise FieldValidationError(self, instance, msg) 

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

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

426 raise FieldValidationError(self, instance, msg) 

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

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

429 raise FieldValidationError(self, instance, msg) 

430 

431 def __set__( 

432 self, 

433 instance: Config, 

434 value: Iterable[FieldTypeVar] | None, 

435 at: Any = None, 

436 label: str = "assignment", 

437 ) -> None: 

438 if instance._frozen: 

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

440 

441 if at is None: 

442 at = getCallStack() 

443 

444 if value is not None: 

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

446 else: 

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

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

449 

450 instance._storage[self.name] = value 

451 

452 def toDict(self, instance): 

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

454 

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

456 

457 Parameters 

458 ---------- 

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

460 The config instance that contains this field. 

461 

462 Returns 

463 ------- 

464 `list` 

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

466 """ 

467 value = self.__get__(instance) 

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

469 

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

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

472 field. 

473 

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

475 

476 Parameters 

477 ---------- 

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

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

480 comparison. 

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

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

483 comparison. 

484 shortcut : `bool` 

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

486 rtol : `float` 

487 Relative tolerance for floating point comparisons. 

488 atol : `float` 

489 Absolute tolerance for floating point comparisons. 

490 output : callable 

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

492 repeatedly) to report inequalities. 

493 

494 Returns 

495 ------- 

496 equal : `bool` 

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

498 

499 Notes 

500 ----- 

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

502 """ 

503 l1 = getattr(instance1, self.name) 

504 l2 = getattr(instance2, self.name) 

505 name = getComparisonName( 

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

507 ) 

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

509 return False 

510 if l1 is None and l2 is None: 

511 return True 

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

513 return False 

514 equal = True 

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

516 result = compareScalars( 

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

518 ) 

519 if not result and shortcut: 

520 return False 

521 equal = equal and result 

522 return equal