Hide keyboard shortcuts

Hot-keys 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

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 

33from .comparison import compareScalars, getComparisonName 

34from .callStack import getCallStack, getStackFrame 

35 

36 

37class List(collections.abc.MutableSequence): 

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

39 

40 Parameters 

41 ---------- 

42 config : `lsst.pex.config.Config` 

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

44 field : `ListField` 

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

46 value : sequence 

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

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

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

50 label : `str` 

51 Event label for the history. 

52 setHistory : `bool`, optional 

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

54 parameter. Default is `True`. 

55 

56 Raises 

57 ------ 

58 FieldValidationError 

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

60 appropriate type for this field or does not pass the 

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

62 """ 

63 

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

65 self._field = field 

66 self._config = config 

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

68 self._list = [] 

69 self.__doc__ = field.doc 

70 if value is not None: 

71 try: 

72 for i, x in enumerate(value): 

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

74 except TypeError: 

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

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

77 if setHistory: 

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

79 

80 def validateItem(self, i, x): 

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

82 

83 Parameters 

84 ---------- 

85 i : `int` 

86 Index of the item in the `list`. 

87 x : object 

88 Item in the `list`. 

89 

90 Raises 

91 ------ 

92 FieldValidationError 

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

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

95 `ListField.itemCheck` method. 

96 """ 

97 

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

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

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

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

102 

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

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

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

106 

107 def list(self): 

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

109 """ 

110 return self._list 

111 

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

113 """Read-only history. 

114 """ 

115 

116 def __contains__(self, x): 

117 return x in self._list 

118 

119 def __len__(self): 

120 return len(self._list) 

121 

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

123 if self._config._frozen: 

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

125 "Cannot modify a frozen Config") 

126 if isinstance(i, slice): 

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

128 for j, xj in enumerate(x): 

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

130 self.validateItem(k, xj) 

131 x[j] = xj 

132 k += step 

133 else: 

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

135 self.validateItem(i, x) 

136 

137 self._list[i] = x 

138 if setHistory: 

139 if at is None: 

140 at = getCallStack() 

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

142 

143 def __getitem__(self, i): 

144 return self._list[i] 

145 

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

147 if self._config._frozen: 

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

149 "Cannot modify a frozen Config") 

150 del self._list[i] 

151 if setHistory: 

152 if at is None: 

153 at = getCallStack() 

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

155 

156 def __iter__(self): 

157 return iter(self._list) 

158 

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

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

161 

162 Parameters 

163 ---------- 

164 i : `int` 

165 Index where the item is inserted. 

166 x : object 

167 Item that is inserted. 

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

169 The call stack (created by 

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

171 label : `str`, optional 

172 Event label for the history. 

173 setHistory : `bool`, optional 

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

175 parameter. Default is `True`. 

176 """ 

177 if at is None: 

178 at = getCallStack() 

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

180 

181 def __repr__(self): 

182 return repr(self._list) 

183 

184 def __str__(self): 

185 return str(self._list) 

186 

187 def __eq__(self, other): 

188 try: 

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

190 return False 

191 

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

193 if i != j: 

194 return False 

195 return True 

196 except AttributeError: 

197 # other is not a sequence type 

198 return False 

199 

200 def __ne__(self, other): 

201 return not self.__eq__(other) 

202 

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

204 if hasattr(getattr(self.__class__, attr, None), '__set__'): 

205 # This allows properties to work. 

206 object.__setattr__(self, attr, value) 

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

208 # This allows specific private attributes to work. 

209 object.__setattr__(self, attr, value) 

210 else: 

211 # We throw everything else. 

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

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

214 

215 

216class ListField(Field): 

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

218 a list of values of a specific type. 

219 

220 Parameters 

221 ---------- 

222 doc : `str` 

223 A description of the field. 

224 dtype : class 

225 The data type of items in the list. 

226 default : sequence, optional 

227 The default items for the field. 

228 optional : `bool`, optional 

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

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

231 `None`. 

232 listCheck : callable, optional 

233 A callable that validates the list as a whole. 

234 itemCheck : callable, optional 

235 A callable that validates individual items in the list. 

236 length : `int`, optional 

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

238 minLength : `int`, optional 

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

240 items. 

241 maxLength : `int`, optional 

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

243 items. 

244 deprecated : None or `str`, optional 

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

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

247 

248 See also 

249 -------- 

250 ChoiceField 

251 ConfigChoiceField 

252 ConfigDictField 

253 ConfigField 

254 ConfigurableField 

255 DictField 

256 Field 

257 RangeField 

258 RegistryField 

259 """ 

260 def __init__(self, doc, dtype, default=None, optional=False, 

261 listCheck=None, itemCheck=None, 

262 length=None, minLength=None, maxLength=None, 

263 deprecated=None): 

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

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

266 if length is not None: 

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

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

269 minLength = None 

270 maxLength = None 

271 else: 

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

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

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

275 and minLength > maxLength: 

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

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

278 

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

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

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

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

283 

284 source = getStackFrame() 

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

286 deprecated=deprecated) 

287 

288 self.listCheck = listCheck 

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

290 """ 

291 

292 self.itemCheck = itemCheck 

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

294 into the list. 

295 """ 

296 

297 self.itemtype = dtype 

298 """Data type of list items. 

299 """ 

300 

301 self.length = length 

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

303 disable checking the list's length). 

304 """ 

305 

306 self.minLength = minLength 

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

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

309 """ 

310 

311 self.maxLength = maxLength 

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

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

314 """ 

315 

316 def validate(self, instance): 

317 """Validate the field. 

318 

319 Parameters 

320 ---------- 

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

322 The config instance that contains this field. 

323 

324 Raises 

325 ------ 

326 lsst.pex.config.FieldValidationError 

327 Raised if: 

328 

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

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

331 `minLength`, or `maxLength` attributes. 

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

333 

334 Notes 

335 ----- 

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

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

338 """ 

339 Field.validate(self, instance) 

340 value = self.__get__(instance) 

341 if value is not None: 

342 lenValue = len(value) 

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

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

345 raise FieldValidationError(self, instance, msg) 

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

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

348 raise FieldValidationError(self, instance, msg) 

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

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

351 raise FieldValidationError(self, instance, msg) 

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

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

354 raise FieldValidationError(self, instance, msg) 

355 

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

357 if instance._frozen: 

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

359 

360 if at is None: 

361 at = getCallStack() 

362 

363 if value is not None: 

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

365 else: 

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

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

368 

369 instance._storage[self.name] = value 

370 

371 def toDict(self, instance): 

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

373 

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

375 

376 Parameters 

377 ---------- 

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

379 The config instance that contains this field. 

380 

381 Returns 

382 ------- 

383 `list` 

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

385 """ 

386 value = self.__get__(instance) 

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

388 

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

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

391 field. 

392 

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

394 

395 Parameters 

396 ---------- 

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

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

399 comparison. 

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

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

402 comparison. 

403 shortcut : `bool` 

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

405 rtol : `float` 

406 Relative tolerance for floating point comparisons. 

407 atol : `float` 

408 Absolute tolerance for floating point comparisons. 

409 output : callable 

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

411 repeatedly) to report inequalities. 

412 

413 Returns 

414 ------- 

415 equal : `bool` 

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

417 

418 Notes 

419 ----- 

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

421 """ 

422 l1 = getattr(instance1, self.name) 

423 l2 = getattr(instance2, self.name) 

424 name = getComparisonName( 

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

426 _joinNamePath(instance2._name, self.name) 

427 ) 

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

429 return False 

430 if l1 is None and l2 is None: 

431 return True 

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

433 return False 

434 equal = True 

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

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

437 rtol=rtol, atol=atol, output=output) 

438 if not result and shortcut: 

439 return False 

440 equal = equal and result 

441 return equal