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

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 

31 

32from .config import Field, FieldValidationError, _typeStr, _autocast, _joinNamePath, Config 

33from .comparison import compareScalars, getComparisonName 

34from .callStack import getCallStack, getStackFrame 

35 

36import weakref 

37 

38 

39class List(collections.abc.MutableSequence): 

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

41 

42 Parameters 

43 ---------- 

44 config : `lsst.pex.config.Config` 

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

46 field : `ListField` 

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

48 value : sequence 

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

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

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

52 label : `str` 

53 Event label for the history. 

54 setHistory : `bool`, optional 

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

56 parameter. Default is `True`. 

57 

58 Raises 

59 ------ 

60 FieldValidationError 

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

62 appropriate type for this field or does not pass the 

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

64 """ 

65 

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

67 self._field = field 

68 self._config_ = weakref.ref(config) 

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

70 self._list = [] 

71 self.__doc__ = field.doc 

72 if value is not None: 

73 try: 

74 for i, x in enumerate(value): 

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

76 except TypeError: 

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

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

79 if setHistory: 

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

81 

82 @property 

83 def _config(self) -> Config: 

84 # Config Fields should never outlive their config class instance 

85 # assert that as such here 

86 assert(self._config_() is not None) 

87 return self._config_() 

88 

89 def validateItem(self, i, x): 

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

91 

92 Parameters 

93 ---------- 

94 i : `int` 

95 Index of the item in the `list`. 

96 x : object 

97 Item in the `list`. 

98 

99 Raises 

100 ------ 

101 FieldValidationError 

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

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

104 `ListField.itemCheck` method. 

105 """ 

106 

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

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

109 (i, x, _typeStr(x), _typeStr(self._field.itemtype)) 

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

111 

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

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

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

115 

116 def list(self): 

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

118 """ 

119 return self._list 

120 

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

122 """Read-only history. 

123 """ 

124 

125 def __contains__(self, x): 

126 return x in self._list 

127 

128 def __len__(self): 

129 return len(self._list) 

130 

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

132 if self._config._frozen: 

133 raise FieldValidationError(self._field, self._config, 

134 "Cannot modify a frozen Config") 

135 if isinstance(i, slice): 

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

137 for j, xj in enumerate(x): 

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

139 self.validateItem(k, xj) 

140 x[j] = xj 

141 k += step 

142 else: 

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

144 self.validateItem(i, x) 

145 

146 self._list[i] = x 

147 if setHistory: 

148 if at is None: 

149 at = getCallStack() 

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

151 

152 def __getitem__(self, i): 

153 return self._list[i] 

154 

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

156 if self._config._frozen: 

157 raise FieldValidationError(self._field, self._config, 

158 "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 def __init__(self, doc, dtype, default=None, optional=False, 

270 listCheck=None, itemCheck=None, 

271 length=None, minLength=None, maxLength=None, 

272 deprecated=None): 

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

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

275 if length is not None: 

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

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

278 minLength = None 

279 maxLength = None 

280 else: 

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

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

283 if minLength is not None and maxLength is not None \ 283 ↛ 285line 283 didn't jump to line 285, because the condition on line 283 was never true

284 and minLength > maxLength: 

285 raise ValueError("'maxLength' (%d) must be at least" 

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

287 

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

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

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

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

292 

293 source = getStackFrame() 

294 self._setup(doc=doc, dtype=List, default=default, check=None, optional=optional, source=source, 

295 deprecated=deprecated) 

296 

297 self.listCheck = listCheck 

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

299 """ 

300 

301 self.itemCheck = itemCheck 

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

303 into the list. 

304 """ 

305 

306 self.itemtype = dtype 

307 """Data type of list items. 

308 """ 

309 

310 self.length = length 

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

312 disable checking the list's length). 

313 """ 

314 

315 self.minLength = minLength 

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

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

318 """ 

319 

320 self.maxLength = maxLength 

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

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

323 """ 

324 

325 def validate(self, instance): 

326 """Validate the field. 

327 

328 Parameters 

329 ---------- 

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

331 The config instance that contains this field. 

332 

333 Raises 

334 ------ 

335 lsst.pex.config.FieldValidationError 

336 Raised if: 

337 

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

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

340 `minLength`, or `maxLength` attributes. 

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

342 

343 Notes 

344 ----- 

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

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

347 """ 

348 Field.validate(self, instance) 

349 value = self.__get__(instance) 

350 if value is not None: 

351 lenValue = len(value) 

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

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

354 raise FieldValidationError(self, instance, msg) 

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

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

357 raise FieldValidationError(self, instance, msg) 

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

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

360 raise FieldValidationError(self, instance, msg) 

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

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

363 raise FieldValidationError(self, instance, msg) 

364 

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

366 if instance._frozen: 

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

368 

369 if at is None: 

370 at = getCallStack() 

371 

372 if value is not None: 

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

374 else: 

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

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

377 

378 instance._storage[self.name] = value 

379 

380 def toDict(self, instance): 

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

382 

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

384 

385 Parameters 

386 ---------- 

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

388 The config instance that contains this field. 

389 

390 Returns 

391 ------- 

392 `list` 

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

394 """ 

395 value = self.__get__(instance) 

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

397 

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

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

400 field. 

401 

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

403 

404 Parameters 

405 ---------- 

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

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

408 comparison. 

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

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

411 comparison. 

412 shortcut : `bool` 

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

414 rtol : `float` 

415 Relative tolerance for floating point comparisons. 

416 atol : `float` 

417 Absolute tolerance for floating point comparisons. 

418 output : callable 

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

420 repeatedly) to report inequalities. 

421 

422 Returns 

423 ------- 

424 equal : `bool` 

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

426 

427 Notes 

428 ----- 

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

430 """ 

431 l1 = getattr(instance1, self.name) 

432 l2 = getattr(instance2, self.name) 

433 name = getComparisonName( 

434 _joinNamePath(instance1._name, self.name), 

435 _joinNamePath(instance2._name, self.name) 

436 ) 

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

438 return False 

439 if l1 is None and l2 is None: 

440 return True 

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

442 return False 

443 equal = True 

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

445 result = compareScalars("%s[%d]" % (name, n), v1, v2, dtype=self.dtype, 

446 rtol=rtol, atol=atol, output=output) 

447 if not result and shortcut: 

448 return False 

449 equal = equal and result 

450 return equal