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, Config
33from .comparison import compareScalars, getComparisonName
34from .callStack import getCallStack, getStackFrame
36import weakref
39class List(collections.abc.MutableSequence):
40 """List collection used internally by `ListField`.
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`.
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 """
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))
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_()
89 def validateItem(self, i, x):
90 """Validate an item to determine if it can be included in the list.
92 Parameters
93 ----------
94 i : `int`
95 Index of the item in the `list`.
96 x : object
97 Item in the `list`.
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 """
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)
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)
116 def list(self):
117 """Sequence of items contained by the `List` (`list`).
118 """
119 return self._list
121 history = property(lambda x: x._history) 121 ↛ exitline 121 didn't run the lambda on line 121
122 """Read-only history.
123 """
125 def __contains__(self, x):
126 return x in self._list
128 def __len__(self):
129 return len(self._list)
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)
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))
152 def __getitem__(self, i):
153 return self._list[i]
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))
165 def __iter__(self):
166 return iter(self._list)
168 def insert(self, i, x, at=None, label="insert", setHistory=True):
169 """Insert an item into the list at the given index.
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)
190 def __repr__(self):
191 return repr(self._list)
193 def __str__(self):
194 return str(self._list)
196 def __eq__(self, other):
197 try:
198 if len(self) != len(other):
199 return False
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
209 def __ne__(self, other):
210 return not self.__eq__(other)
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)
225class ListField(Field):
226 """A configuration field (`~lsst.pex.config.Field` subclass) that contains
227 a list of values of a specific type.
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.
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))
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")
293 source = getStackFrame()
294 self._setup(doc=doc, dtype=List, default=default, check=None, optional=optional, source=source,
295 deprecated=deprecated)
297 self.listCheck = listCheck
298 """Callable used to check the list as a whole.
299 """
301 self.itemCheck = itemCheck
302 """Callable used to validate individual items as they are inserted
303 into the list.
304 """
306 self.itemtype = dtype
307 """Data type of list items.
308 """
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 """
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 """
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 """
325 def validate(self, instance):
326 """Validate the field.
328 Parameters
329 ----------
330 instance : `lsst.pex.config.Config`
331 The config instance that contains this field.
333 Raises
334 ------
335 lsst.pex.config.FieldValidationError
336 Raised if:
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`.
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)
365 def __set__(self, instance, value, at=None, label="assignment"):
366 if instance._frozen:
367 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
369 if at is None:
370 at = getCallStack()
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))
378 instance._storage[self.name] = value
380 def toDict(self, instance):
381 """Convert the value of this field to a plain `list`.
383 `lsst.pex.config.Config.toDict` is the primary user of this method.
385 Parameters
386 ----------
387 instance : `lsst.pex.config.Config`
388 The config instance that contains this field.
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
398 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
399 """Compare two config instances for equality with respect to this
400 field.
402 `lsst.pex.config.config.compare` is the primary user of this method.
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.
422 Returns
423 -------
424 equal : `bool`
425 `True` if the fields are equal; `False` otherwise.
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