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
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
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
31import weakref
33from .callStack import getCallStack, getStackFrame
34from .comparison import compareScalars, getComparisonName
35from .config import Config, Field, FieldValidationError, _autocast, _joinNamePath, _typeStr
38class List(collections.abc.MutableSequence):
39 """List collection used internally by `ListField`.
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`.
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 """
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))
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_()
88 def validateItem(self, i, x):
89 """Validate an item to determine if it can be included in the list.
91 Parameters
92 ----------
93 i : `int`
94 Index of the item in the `list`.
95 x : object
96 Item in the `list`.
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 """
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)
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)
119 def list(self):
120 """Sequence of items contained by the `List` (`list`)."""
121 return self._list
123 history = property(lambda x: x._history) 123 ↛ exitline 123 didn't run the lambda on line 123
124 """Read-only history.
125 """
127 def __contains__(self, x):
128 return x in self._list
130 def __len__(self):
131 return len(self._list)
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)
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))
153 def __getitem__(self, i):
154 return self._list[i]
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))
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 """
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 )
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")
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 )
315 self.listCheck = listCheck
316 """Callable used to check the list as a whole.
317 """
319 self.itemCheck = itemCheck
320 """Callable used to validate individual items as they are inserted
321 into the list.
322 """
324 self.itemtype = dtype
325 """Data type of list items.
326 """
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 """
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 """
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 """
343 def validate(self, instance):
344 """Validate the field.
346 Parameters
347 ----------
348 instance : `lsst.pex.config.Config`
349 The config instance that contains this field.
351 Raises
352 ------
353 lsst.pex.config.FieldValidationError
354 Raised if:
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`.
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)
383 def __set__(self, instance, value, at=None, label="assignment"):
384 if instance._frozen:
385 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
387 if at is None:
388 at = getCallStack()
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))
396 instance._storage[self.name] = value
398 def toDict(self, instance):
399 """Convert the value of this field to a plain `list`.
401 `lsst.pex.config.Config.toDict` is the primary user of this method.
403 Parameters
404 ----------
405 instance : `lsst.pex.config.Config`
406 The config instance that contains this field.
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
416 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
417 """Compare two config instances for equality with respect to this
418 field.
420 `lsst.pex.config.config.compare` is the primary user of this method.
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.
440 Returns
441 -------
442 equal : `bool`
443 `True` if the fields are equal; `False` otherwise.
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