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

197 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-28 07:53 +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 sys 

32import weakref 

33from typing import Any, Generic, Iterable, MutableSequence, Union, 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 

48if int(sys.version_info.minor) < 9: 48 ↛ 49line 48 didn't jump to line 49, because the condition on line 48 was never true

49 _bases = (collections.abc.MutableSequence, Generic[FieldTypeVar]) 

50else: 

51 _bases = (collections.abc.MutableSequence[FieldTypeVar],) 

52 

53 

54class List(*_bases): 

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

56 

57 Parameters 

58 ---------- 

59 config : `lsst.pex.config.Config` 

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

61 field : `ListField` 

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

63 value : sequence 

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

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

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

67 label : `str` 

68 Event label for the history. 

69 setHistory : `bool`, optional 

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

71 parameter. Default is `True`. 

72 

73 Raises 

74 ------ 

75 FieldValidationError 

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

77 appropriate type for this field or does not pass the 

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

79 """ 

80 

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

82 self._field = field 

83 self._config_ = weakref.ref(config) 

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

85 self._list = [] 

86 self.__doc__ = field.doc 

87 if value is not None: 

88 try: 

89 for i, x in enumerate(value): 

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

91 except TypeError: 

92 msg = "Value %s is of incorrect type %s. Sequence type expected" % (value, _typeStr(value)) 

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

94 if setHistory: 

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

96 

97 @property 

98 def _config(self) -> Config: 

99 # Config Fields should never outlive their config class instance 

100 # assert that as such here 

101 value = self._config_() 

102 assert value is not None 

103 return value 

104 

105 def validateItem(self, i, x): 

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

107 

108 Parameters 

109 ---------- 

110 i : `int` 

111 Index of the item in the `list`. 

112 x : object 

113 Item in the `list`. 

114 

115 Raises 

116 ------ 

117 FieldValidationError 

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

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

120 `ListField.itemCheck` method. 

121 """ 

122 

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

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

125 i, 

126 x, 

127 _typeStr(x), 

128 _typeStr(self._field.itemtype), 

129 ) 

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

131 

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

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

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

135 

136 def list(self): 

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

138 return self._list 

139 

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

141 """Read-only history. 

142 """ 

143 

144 def __contains__(self, x): 

145 return x in self._list 

146 

147 def __len__(self): 

148 return len(self._list) 

149 

150 @overload 

151 def __setitem__( 

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

153 ) -> None: 

154 ... 

155 

156 @overload 

157 def __setitem__( 

158 self, 

159 i: slice, 

160 x: Iterable[FieldTypeVar], 

161 at: Any = None, 

162 label: str = "setitem", 

163 setHistory: bool = True, 

164 ) -> None: 

165 ... 

166 

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

168 if self._config._frozen: 

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

170 if isinstance(i, slice): 

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

172 for j, xj in enumerate(x): 

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

174 self.validateItem(k, xj) 

175 x[j] = xj 

176 k += step 

177 else: 

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

179 self.validateItem(i, x) 

180 

181 self._list[i] = x 

182 if setHistory: 

183 if at is None: 

184 at = getCallStack() 

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

186 

187 @overload 

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

189 ... 

190 

191 @overload 

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

193 ... 

194 

195 def __getitem__(self, i): 

196 return self._list[i] 

197 

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

199 if self._config._frozen: 

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

201 del self._list[i] 

202 if setHistory: 

203 if at is None: 

204 at = getCallStack() 

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

206 

207 def __iter__(self): 

208 return iter(self._list) 

209 

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

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

212 

213 Parameters 

214 ---------- 

215 i : `int` 

216 Index where the item is inserted. 

217 x : object 

218 Item that is inserted. 

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

220 The call stack (created by 

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

222 label : `str`, optional 

223 Event label for the history. 

224 setHistory : `bool`, optional 

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

226 parameter. Default is `True`. 

227 """ 

228 if at is None: 

229 at = getCallStack() 

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

231 

232 def __repr__(self): 

233 return repr(self._list) 

234 

235 def __str__(self): 

236 return str(self._list) 

237 

238 def __eq__(self, other): 

239 try: 

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

241 return False 

242 

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

244 if i != j: 

245 return False 

246 return True 

247 except AttributeError: 

248 # other is not a sequence type 

249 return False 

250 

251 def __ne__(self, other): 

252 return not self.__eq__(other) 

253 

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

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

256 # This allows properties to work. 

257 object.__setattr__(self, attr, value) 

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

259 # This allows specific private attributes to work. 

260 object.__setattr__(self, attr, value) 

261 else: 

262 # We throw everything else. 

263 msg = "%s has no attribute %s" % (_typeStr(self._field), attr) 

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

265 

266 def __reduce__(self): 

267 raise UnexpectedProxyUsageError( 

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

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

270 "being assigned to other objects or variables." 

271 ) 

272 

273 

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

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

276 a list of values of a specific type. 

277 

278 Parameters 

279 ---------- 

280 doc : `str` 

281 A description of the field. 

282 dtype : class, optional 

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

284 argument to the class. 

285 default : sequence, optional 

286 The default items for the field. 

287 optional : `bool`, optional 

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

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

290 `None`. 

291 listCheck : callable, optional 

292 A callable that validates the list as a whole. 

293 itemCheck : callable, optional 

294 A callable that validates individual items in the list. 

295 length : `int`, optional 

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

297 minLength : `int`, optional 

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

299 items. 

300 maxLength : `int`, optional 

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

302 items. 

303 deprecated : None or `str`, optional 

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

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

306 

307 See also 

308 -------- 

309 ChoiceField 

310 ConfigChoiceField 

311 ConfigDictField 

312 ConfigField 

313 ConfigurableField 

314 DictField 

315 Field 

316 RangeField 

317 RegistryField 

318 """ 

319 

320 def __init__( 

321 self, 

322 doc, 

323 dtype=None, 

324 default=None, 

325 optional=False, 

326 listCheck=None, 

327 itemCheck=None, 

328 length=None, 

329 minLength=None, 

330 maxLength=None, 

331 deprecated=None, 

332 ): 

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

334 raise ValueError( 

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

336 ) 

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

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

339 if length is not None: 

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

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

342 minLength = None 

343 maxLength = None 

344 else: 

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

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

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

348 raise ValueError( 

349 "'maxLength' (%d) must be at least" 

350 " as large as 'minLength' (%d)" % (maxLength, minLength) 

351 ) 

352 

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

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

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

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

357 

358 source = getStackFrame() 

359 self._setup( 

360 doc=doc, 

361 dtype=List, 

362 default=default, 

363 check=None, 

364 optional=optional, 

365 source=source, 

366 deprecated=deprecated, 

367 ) 

368 

369 self.listCheck = listCheck 

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

371 """ 

372 

373 self.itemCheck = itemCheck 

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

375 into the list. 

376 """ 

377 

378 self.itemtype = dtype 

379 """Data type of list items. 

380 """ 

381 

382 self.length = length 

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

384 disable checking the list's length). 

385 """ 

386 

387 self.minLength = minLength 

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

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

390 """ 

391 

392 self.maxLength = maxLength 

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

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

395 """ 

396 

397 def validate(self, instance): 

398 """Validate the field. 

399 

400 Parameters 

401 ---------- 

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

403 The config instance that contains this field. 

404 

405 Raises 

406 ------ 

407 lsst.pex.config.FieldValidationError 

408 Raised if: 

409 

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

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

412 `minLength`, or `maxLength` attributes. 

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

414 

415 Notes 

416 ----- 

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

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

419 """ 

420 Field.validate(self, instance) 

421 value = self.__get__(instance) 

422 if value is not None: 

423 lenValue = len(value) 

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

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

426 raise FieldValidationError(self, instance, msg) 

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

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

429 raise FieldValidationError(self, instance, msg) 

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

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

432 raise FieldValidationError(self, instance, msg) 

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

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

435 raise FieldValidationError(self, instance, msg) 

436 

437 def __set__( 

438 self, 

439 instance: Config, 

440 value: Union[Iterable[FieldTypeVar], None], 

441 at: Any = None, 

442 label: str = "assignment", 

443 ) -> None: 

444 if instance._frozen: 

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

446 

447 if at is None: 

448 at = getCallStack() 

449 

450 if value is not None: 

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

452 else: 

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

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

455 

456 instance._storage[self.name] = value 

457 

458 def toDict(self, instance): 

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

460 

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

462 

463 Parameters 

464 ---------- 

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

466 The config instance that contains this field. 

467 

468 Returns 

469 ------- 

470 `list` 

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

472 """ 

473 value = self.__get__(instance) 

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

475 

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

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

478 field. 

479 

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

481 

482 Parameters 

483 ---------- 

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

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

486 comparison. 

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

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

489 comparison. 

490 shortcut : `bool` 

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

492 rtol : `float` 

493 Relative tolerance for floating point comparisons. 

494 atol : `float` 

495 Absolute tolerance for floating point comparisons. 

496 output : callable 

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

498 repeatedly) to report inequalities. 

499 

500 Returns 

501 ------- 

502 equal : `bool` 

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

504 

505 Notes 

506 ----- 

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

508 """ 

509 l1 = getattr(instance1, self.name) 

510 l2 = getattr(instance2, self.name) 

511 name = getComparisonName( 

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

513 ) 

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

515 return False 

516 if l1 is None and l2 is None: 

517 return True 

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

519 return False 

520 equal = True 

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

522 result = compareScalars( 

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

524 ) 

525 if not result and shortcut: 

526 return False 

527 equal = equal and result 

528 return equal