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

177 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-23 09:46 +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 

32 

33from .callStack import getCallStack, getStackFrame 

34from .comparison import compareScalars, getComparisonName 

35from .config import ( 

36 Config, 

37 Field, 

38 FieldValidationError, 

39 UnexpectedProxyUsageError, 

40 _autocast, 

41 _joinNamePath, 

42 _typeStr, 

43) 

44 

45 

46class List(collections.abc.MutableSequence): 

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

48 

49 Parameters 

50 ---------- 

51 config : `lsst.pex.config.Config` 

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

53 field : `ListField` 

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

55 value : sequence 

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

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

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

59 label : `str` 

60 Event label for the history. 

61 setHistory : `bool`, optional 

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

63 parameter. Default is `True`. 

64 

65 Raises 

66 ------ 

67 FieldValidationError 

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

69 appropriate type for this field or does not pass the 

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

71 """ 

72 

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

74 self._field = field 

75 self._config_ = weakref.ref(config) 

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

77 self._list = [] 

78 self.__doc__ = field.doc 

79 if value is not None: 

80 try: 

81 for i, x in enumerate(value): 

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

83 except TypeError: 

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

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

86 if setHistory: 

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

88 

89 @property 

90 def _config(self) -> Config: 

91 # Config Fields should never outlive their config class instance 

92 # assert that as such here 

93 assert self._config_() is not None 

94 return self._config_() 

95 

96 def validateItem(self, i, x): 

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

98 

99 Parameters 

100 ---------- 

101 i : `int` 

102 Index of the item in the `list`. 

103 x : object 

104 Item in the `list`. 

105 

106 Raises 

107 ------ 

108 FieldValidationError 

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

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

111 `ListField.itemCheck` method. 

112 """ 

113 

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

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

116 i, 

117 x, 

118 _typeStr(x), 

119 _typeStr(self._field.itemtype), 

120 ) 

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

122 

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

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

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

126 

127 def list(self): 

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

129 return self._list 

130 

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

132 """Read-only history. 

133 """ 

134 

135 def __contains__(self, x): 

136 return x in self._list 

137 

138 def __len__(self): 

139 return len(self._list) 

140 

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

142 if self._config._frozen: 

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

144 if isinstance(i, slice): 

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

146 for j, xj in enumerate(x): 

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

148 self.validateItem(k, xj) 

149 x[j] = xj 

150 k += step 

151 else: 

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

153 self.validateItem(i, x) 

154 

155 self._list[i] = x 

156 if setHistory: 

157 if at is None: 

158 at = getCallStack() 

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

160 

161 def __getitem__(self, i): 

162 return self._list[i] 

163 

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

165 if self._config._frozen: 

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

167 del self._list[i] 

168 if setHistory: 

169 if at is None: 

170 at = getCallStack() 

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

172 

173 def __iter__(self): 

174 return iter(self._list) 

175 

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

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

178 

179 Parameters 

180 ---------- 

181 i : `int` 

182 Index where the item is inserted. 

183 x : object 

184 Item that is inserted. 

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

186 The call stack (created by 

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

188 label : `str`, optional 

189 Event label for the history. 

190 setHistory : `bool`, optional 

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

192 parameter. Default is `True`. 

193 """ 

194 if at is None: 

195 at = getCallStack() 

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

197 

198 def __repr__(self): 

199 return repr(self._list) 

200 

201 def __str__(self): 

202 return str(self._list) 

203 

204 def __eq__(self, other): 

205 try: 

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

207 return False 

208 

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

210 if i != j: 

211 return False 

212 return True 

213 except AttributeError: 

214 # other is not a sequence type 

215 return False 

216 

217 def __ne__(self, other): 

218 return not self.__eq__(other) 

219 

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

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

222 # This allows properties to work. 

223 object.__setattr__(self, attr, value) 

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

225 # This allows specific private attributes to work. 

226 object.__setattr__(self, attr, value) 

227 else: 

228 # We throw everything else. 

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

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

231 

232 def __reduce__(self): 

233 raise UnexpectedProxyUsageError( 

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

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

236 "being assigned to other objects or variables." 

237 ) 

238 

239 

240class ListField(Field): 

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

242 a list of values of a specific type. 

243 

244 Parameters 

245 ---------- 

246 doc : `str` 

247 A description of the field. 

248 dtype : class 

249 The data type of items in the list. 

250 default : sequence, optional 

251 The default items for the field. 

252 optional : `bool`, optional 

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

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

255 `None`. 

256 listCheck : callable, optional 

257 A callable that validates the list as a whole. 

258 itemCheck : callable, optional 

259 A callable that validates individual items in the list. 

260 length : `int`, optional 

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

262 minLength : `int`, optional 

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

264 items. 

265 maxLength : `int`, optional 

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

267 items. 

268 deprecated : None or `str`, optional 

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

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

271 

272 See also 

273 -------- 

274 ChoiceField 

275 ConfigChoiceField 

276 ConfigDictField 

277 ConfigField 

278 ConfigurableField 

279 DictField 

280 Field 

281 RangeField 

282 RegistryField 

283 """ 

284 

285 def __init__( 

286 self, 

287 doc, 

288 dtype, 

289 default=None, 

290 optional=False, 

291 listCheck=None, 

292 itemCheck=None, 

293 length=None, 

294 minLength=None, 

295 maxLength=None, 

296 deprecated=None, 

297 ): 

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

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

300 if length is not None: 

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

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

303 minLength = None 

304 maxLength = None 

305 else: 

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

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

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

309 raise ValueError( 

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

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

312 ) 

313 

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

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

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

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

318 

319 source = getStackFrame() 

320 self._setup( 

321 doc=doc, 

322 dtype=List, 

323 default=default, 

324 check=None, 

325 optional=optional, 

326 source=source, 

327 deprecated=deprecated, 

328 ) 

329 

330 self.listCheck = listCheck 

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

332 """ 

333 

334 self.itemCheck = itemCheck 

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

336 into the list. 

337 """ 

338 

339 self.itemtype = dtype 

340 """Data type of list items. 

341 """ 

342 

343 self.length = length 

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

345 disable checking the list's length). 

346 """ 

347 

348 self.minLength = minLength 

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

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

351 """ 

352 

353 self.maxLength = maxLength 

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

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

356 """ 

357 

358 def validate(self, instance): 

359 """Validate the field. 

360 

361 Parameters 

362 ---------- 

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

364 The config instance that contains this field. 

365 

366 Raises 

367 ------ 

368 lsst.pex.config.FieldValidationError 

369 Raised if: 

370 

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

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

373 `minLength`, or `maxLength` attributes. 

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

375 

376 Notes 

377 ----- 

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

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

380 """ 

381 Field.validate(self, instance) 

382 value = self.__get__(instance) 

383 if value is not None: 

384 lenValue = len(value) 

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

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

387 raise FieldValidationError(self, instance, msg) 

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

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

390 raise FieldValidationError(self, instance, msg) 

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

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

393 raise FieldValidationError(self, instance, msg) 

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

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

396 raise FieldValidationError(self, instance, msg) 

397 

398 def __set__(self, instance, value, at=None, label="assignment"): 

399 if instance._frozen: 

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

401 

402 if at is None: 

403 at = getCallStack() 

404 

405 if value is not None: 

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

407 else: 

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

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

410 

411 instance._storage[self.name] = value 

412 

413 def toDict(self, instance): 

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

415 

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

417 

418 Parameters 

419 ---------- 

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

421 The config instance that contains this field. 

422 

423 Returns 

424 ------- 

425 `list` 

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

427 """ 

428 value = self.__get__(instance) 

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

430 

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

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

433 field. 

434 

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

436 

437 Parameters 

438 ---------- 

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

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

441 comparison. 

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

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

444 comparison. 

445 shortcut : `bool` 

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

447 rtol : `float` 

448 Relative tolerance for floating point comparisons. 

449 atol : `float` 

450 Absolute tolerance for floating point comparisons. 

451 output : callable 

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

453 repeatedly) to report inequalities. 

454 

455 Returns 

456 ------- 

457 equal : `bool` 

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

459 

460 Notes 

461 ----- 

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

463 """ 

464 l1 = getattr(instance1, self.name) 

465 l2 = getattr(instance2, self.name) 

466 name = getComparisonName( 

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

468 ) 

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

470 return False 

471 if l1 is None and l2 is None: 

472 return True 

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

474 return False 

475 equal = True 

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

477 result = compareScalars( 

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

479 ) 

480 if not result and shortcut: 

481 return False 

482 equal = equal and result 

483 return equal