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

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/>.
28__all__ = ["ListField"]
30import collections.abc
32from .config import Field, FieldValidationError, _typeStr, _autocast, _joinNamePath
33from .comparison import compareScalars, getComparisonName
34from .callStack import getCallStack, getStackFrame
37class List(collections.abc.MutableSequence):
38 """List collection used internally by `ListField`.
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`.
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 """
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))
80 def validateItem(self, i, x):
81 """Validate an item to determine if it can be included in the list.
83 Parameters
84 ----------
85 i : `int`
86 Index of the item in the `list`.
87 x : object
88 Item in the `list`.
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 """
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)
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)
107 def list(self):
108 """Sequence of items contained by the `List` (`list`).
109 """
110 return self._list
112 history = property(lambda x: x._history) 112 ↛ exitline 112 didn't run the lambda on line 112
113 """Read-only history.
114 """
116 def __contains__(self, x):
117 return x in self._list
119 def __len__(self):
120 return len(self._list)
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)
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))
143 def __getitem__(self, i):
144 return self._list[i]
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))
156 def __iter__(self):
157 return iter(self._list)
159 def insert(self, i, x, at=None, label="insert", setHistory=True):
160 """Insert an item into the list at the given index.
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)
181 def __repr__(self):
182 return repr(self._list)
184 def __str__(self):
185 return str(self._list)
187 def __eq__(self, other):
188 try:
189 if len(self) != len(other):
190 return False
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
200 def __ne__(self, other):
201 return not self.__eq__(other)
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)
216class ListField(Field):
217 """A configuration field (`~lsst.pex.config.Field` subclass) that contains
218 a list of values of a specific type.
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.
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))
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")
284 source = getStackFrame()
285 self._setup(doc=doc, dtype=List, default=default, check=None, optional=optional, source=source,
286 deprecated=deprecated)
288 self.listCheck = listCheck
289 """Callable used to check the list as a whole.
290 """
292 self.itemCheck = itemCheck
293 """Callable used to validate individual items as they are inserted
294 into the list.
295 """
297 self.itemtype = dtype
298 """Data type of list items.
299 """
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 """
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 """
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 """
316 def validate(self, instance):
317 """Validate the field.
319 Parameters
320 ----------
321 instance : `lsst.pex.config.Config`
322 The config instance that contains this field.
324 Raises
325 ------
326 lsst.pex.config.FieldValidationError
327 Raised if:
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`.
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)
356 def __set__(self, instance, value, at=None, label="assignment"):
357 if instance._frozen:
358 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
360 if at is None:
361 at = getCallStack()
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))
369 instance._storage[self.name] = value
371 def toDict(self, instance):
372 """Convert the value of this field to a plain `list`.
374 `lsst.pex.config.Config.toDict` is the primary user of this method.
376 Parameters
377 ----------
378 instance : `lsst.pex.config.Config`
379 The config instance that contains this field.
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
389 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
390 """Compare two config instances for equality with respect to this
391 field.
393 `lsst.pex.config.config.compare` is the primary user of this method.
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.
413 Returns
414 -------
415 equal : `bool`
416 `True` if the fields are equal; `False` otherwise.
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