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

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

169 statements  

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 Config, Field, FieldValidationError, _autocast, _joinNamePath, _typeStr 

36 

37 

38class List(collections.abc.MutableSequence): 

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

40 

41 Parameters 

42 ---------- 

43 config : `lsst.pex.config.Config` 

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

45 field : `ListField` 

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

47 value : sequence 

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

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

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

51 label : `str` 

52 Event label for the history. 

53 setHistory : `bool`, optional 

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

55 parameter. Default is `True`. 

56 

57 Raises 

58 ------ 

59 FieldValidationError 

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

61 appropriate type for this field or does not pass the 

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

63 """ 

64 

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

66 self._field = field 

67 self._config_ = weakref.ref(config) 

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

69 self._list = [] 

70 self.__doc__ = field.doc 

71 if value is not None: 

72 try: 

73 for i, x in enumerate(value): 

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

75 except TypeError: 

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

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

78 if setHistory: 

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

80 

81 @property 

82 def _config(self) -> Config: 

83 # Config Fields should never outlive their config class instance 

84 # assert that as such here 

85 assert self._config_() is not None 

86 return self._config_() 

87 

88 def validateItem(self, i, x): 

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

90 

91 Parameters 

92 ---------- 

93 i : `int` 

94 Index of the item in the `list`. 

95 x : object 

96 Item in the `list`. 

97 

98 Raises 

99 ------ 

100 FieldValidationError 

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

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

103 `ListField.itemCheck` method. 

104 """ 

105 

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

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

108 i, 

109 x, 

110 _typeStr(x), 

111 _typeStr(self._field.itemtype), 

112 ) 

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

114 

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

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

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

118 

119 def list(self): 

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

121 return self._list 

122 

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

124 """Read-only history. 

125 """ 

126 

127 def __contains__(self, x): 

128 return x in self._list 

129 

130 def __len__(self): 

131 return len(self._list) 

132 

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

134 if self._config._frozen: 

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

136 if isinstance(i, slice): 

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

138 for j, xj in enumerate(x): 

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

140 self.validateItem(k, xj) 

141 x[j] = xj 

142 k += step 

143 else: 

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

145 self.validateItem(i, x) 

146 

147 self._list[i] = x 

148 if setHistory: 

149 if at is None: 

150 at = getCallStack() 

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

152 

153 def __getitem__(self, i): 

154 return self._list[i] 

155 

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

157 if self._config._frozen: 

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

159 del self._list[i] 

160 if setHistory: 

161 if at is None: 

162 at = getCallStack() 

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

164 

165 def __iter__(self): 

166 return iter(self._list) 

167 

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

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

170 

171 Parameters 

172 ---------- 

173 i : `int` 

174 Index where the item is inserted. 

175 x : object 

176 Item that is inserted. 

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

178 The call stack (created by 

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

180 label : `str`, optional 

181 Event label for the history. 

182 setHistory : `bool`, optional 

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

184 parameter. Default is `True`. 

185 """ 

186 if at is None: 

187 at = getCallStack() 

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

189 

190 def __repr__(self): 

191 return repr(self._list) 

192 

193 def __str__(self): 

194 return str(self._list) 

195 

196 def __eq__(self, other): 

197 try: 

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

199 return False 

200 

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

202 if i != j: 

203 return False 

204 return True 

205 except AttributeError: 

206 # other is not a sequence type 

207 return False 

208 

209 def __ne__(self, other): 

210 return not self.__eq__(other) 

211 

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

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

214 # This allows properties to work. 

215 object.__setattr__(self, attr, value) 

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

217 # This allows specific private attributes to work. 

218 object.__setattr__(self, attr, value) 

219 else: 

220 # We throw everything else. 

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

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

223 

224 

225class ListField(Field): 

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

227 a list of values of a specific type. 

228 

229 Parameters 

230 ---------- 

231 doc : `str` 

232 A description of the field. 

233 dtype : class 

234 The data type of items in the list. 

235 default : sequence, optional 

236 The default items for the field. 

237 optional : `bool`, optional 

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

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

240 `None`. 

241 listCheck : callable, optional 

242 A callable that validates the list as a whole. 

243 itemCheck : callable, optional 

244 A callable that validates individual items in the list. 

245 length : `int`, optional 

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

247 minLength : `int`, optional 

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

249 items. 

250 maxLength : `int`, optional 

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

252 items. 

253 deprecated : None or `str`, optional 

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

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

256 

257 See also 

258 -------- 

259 ChoiceField 

260 ConfigChoiceField 

261 ConfigDictField 

262 ConfigField 

263 ConfigurableField 

264 DictField 

265 Field 

266 RangeField 

267 RegistryField 

268 """ 

269 

270 def __init__( 

271 self, 

272 doc, 

273 dtype, 

274 default=None, 

275 optional=False, 

276 listCheck=None, 

277 itemCheck=None, 

278 length=None, 

279 minLength=None, 

280 maxLength=None, 

281 deprecated=None, 

282 ): 

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

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

285 if length is not None: 

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

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

288 minLength = None 

289 maxLength = None 

290 else: 

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

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

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

294 raise ValueError( 

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

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

297 ) 

298 

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

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

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

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

303 

304 source = getStackFrame() 

305 self._setup( 

306 doc=doc, 

307 dtype=List, 

308 default=default, 

309 check=None, 

310 optional=optional, 

311 source=source, 

312 deprecated=deprecated, 

313 ) 

314 

315 self.listCheck = listCheck 

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

317 """ 

318 

319 self.itemCheck = itemCheck 

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

321 into the list. 

322 """ 

323 

324 self.itemtype = dtype 

325 """Data type of list items. 

326 """ 

327 

328 self.length = length 

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

330 disable checking the list's length). 

331 """ 

332 

333 self.minLength = minLength 

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

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

336 """ 

337 

338 self.maxLength = maxLength 

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

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

341 """ 

342 

343 def validate(self, instance): 

344 """Validate the field. 

345 

346 Parameters 

347 ---------- 

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

349 The config instance that contains this field. 

350 

351 Raises 

352 ------ 

353 lsst.pex.config.FieldValidationError 

354 Raised if: 

355 

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

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

358 `minLength`, or `maxLength` attributes. 

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

360 

361 Notes 

362 ----- 

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

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

365 """ 

366 Field.validate(self, instance) 

367 value = self.__get__(instance) 

368 if value is not None: 

369 lenValue = len(value) 

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

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

372 raise FieldValidationError(self, instance, msg) 

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

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

375 raise FieldValidationError(self, instance, msg) 

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

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

378 raise FieldValidationError(self, instance, msg) 

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

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

381 raise FieldValidationError(self, instance, msg) 

382 

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

384 if instance._frozen: 

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

386 

387 if at is None: 

388 at = getCallStack() 

389 

390 if value is not None: 

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

392 else: 

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

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

395 

396 instance._storage[self.name] = value 

397 

398 def toDict(self, instance): 

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

400 

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

402 

403 Parameters 

404 ---------- 

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

406 The config instance that contains this field. 

407 

408 Returns 

409 ------- 

410 `list` 

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

412 """ 

413 value = self.__get__(instance) 

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

415 

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

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

418 field. 

419 

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

421 

422 Parameters 

423 ---------- 

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

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

426 comparison. 

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

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

429 comparison. 

430 shortcut : `bool` 

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

432 rtol : `float` 

433 Relative tolerance for floating point comparisons. 

434 atol : `float` 

435 Absolute tolerance for floating point comparisons. 

436 output : callable 

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

438 repeatedly) to report inequalities. 

439 

440 Returns 

441 ------- 

442 equal : `bool` 

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

444 

445 Notes 

446 ----- 

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

448 """ 

449 l1 = getattr(instance1, self.name) 

450 l2 = getattr(instance2, self.name) 

451 name = getComparisonName( 

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

453 ) 

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

455 return False 

456 if l1 is None and l2 is None: 

457 return True 

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

459 return False 

460 equal = True 

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

462 result = compareScalars( 

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

464 ) 

465 if not result and shortcut: 

466 return False 

467 equal = equal and result 

468 return equal