Coverage for python/lsst/pex/config/listField.py: 28%
197 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-28 09:11 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-28 09:11 +0000
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 sys
32import weakref
33from typing import Any, Generic, Iterable, MutableSequence, Union, overload
35from .callStack import getCallStack, getStackFrame
36from .comparison import compareScalars, getComparisonName
37from .config import (
38 Config,
39 Field,
40 FieldTypeVar,
41 FieldValidationError,
42 UnexpectedProxyUsageError,
43 _autocast,
44 _joinNamePath,
45 _typeStr,
46)
48if int(sys.version_info.minor) < 9: 48 ↛ 49line 48 didn't jump to line 49, because the condition on line 48 was never true
49 _bases = (collections.abc.MutableSequence, Generic[FieldTypeVar])
50else:
51 _bases = (collections.abc.MutableSequence[FieldTypeVar],)
54class List(*_bases):
55 """List collection used internally by `ListField`.
57 Parameters
58 ----------
59 config : `lsst.pex.config.Config`
60 Config instance that contains the ``field``.
61 field : `ListField`
62 Instance of the `ListField` using this ``List``.
63 value : sequence
64 Sequence of values that are inserted into this ``List``.
65 at : `list` of `lsst.pex.config.callStack.StackFrame`
66 The call stack (created by `lsst.pex.config.callStack.getCallStack`).
67 label : `str`
68 Event label for the history.
69 setHistory : `bool`, optional
70 Enable setting the field's history, using the value of the ``at``
71 parameter. Default is `True`.
73 Raises
74 ------
75 FieldValidationError
76 Raised if an item in the ``value`` parameter does not have the
77 appropriate type for this field or does not pass the
78 `ListField.itemCheck` method of the ``field`` parameter.
79 """
81 def __init__(self, config, field, value, at, label, setHistory=True):
82 self._field = field
83 self._config_ = weakref.ref(config)
84 self._history = self._config._history.setdefault(self._field.name, [])
85 self._list = []
86 self.__doc__ = field.doc
87 if value is not None:
88 try:
89 for i, x in enumerate(value):
90 self.insert(i, x, setHistory=False)
91 except TypeError:
92 msg = "Value %s is of incorrect type %s. Sequence type expected" % (value, _typeStr(value))
93 raise FieldValidationError(self._field, config, msg)
94 if setHistory:
95 self.history.append((list(self._list), at, label))
97 @property
98 def _config(self) -> Config:
99 # Config Fields should never outlive their config class instance
100 # assert that as such here
101 value = self._config_()
102 assert value is not None
103 return value
105 def validateItem(self, i, x):
106 """Validate an item to determine if it can be included in the list.
108 Parameters
109 ----------
110 i : `int`
111 Index of the item in the `list`.
112 x : object
113 Item in the `list`.
115 Raises
116 ------
117 FieldValidationError
118 Raised if an item in the ``value`` parameter does not have the
119 appropriate type for this field or does not pass the field's
120 `ListField.itemCheck` method.
121 """
123 if not isinstance(x, self._field.itemtype) and x is not None:
124 msg = "Item at position %d with value %s is of incorrect type %s. Expected %s" % (
125 i,
126 x,
127 _typeStr(x),
128 _typeStr(self._field.itemtype),
129 )
130 raise FieldValidationError(self._field, self._config, msg)
132 if self._field.itemCheck is not None and not self._field.itemCheck(x):
133 msg = "Item at position %d is not a valid value: %s" % (i, x)
134 raise FieldValidationError(self._field, self._config, msg)
136 def list(self):
137 """Sequence of items contained by the `List` (`list`)."""
138 return self._list
140 history = property(lambda x: x._history) 140 ↛ exitline 140 didn't run the lambda on line 140
141 """Read-only history.
142 """
144 def __contains__(self, x):
145 return x in self._list
147 def __len__(self):
148 return len(self._list)
150 @overload
151 def __setitem__(
152 self, i: int, x: FieldTypeVar, at: Any = None, label: str = "setitem", setHistory: bool = True
153 ) -> None:
154 ...
156 @overload
157 def __setitem__(
158 self,
159 i: slice,
160 x: Iterable[FieldTypeVar],
161 at: Any = None,
162 label: str = "setitem",
163 setHistory: bool = True,
164 ) -> None:
165 ...
167 def __setitem__(self, i, x, at=None, label="setitem", setHistory=True):
168 if self._config._frozen:
169 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
170 if isinstance(i, slice):
171 k, stop, step = i.indices(len(self))
172 for j, xj in enumerate(x):
173 xj = _autocast(xj, self._field.itemtype)
174 self.validateItem(k, xj)
175 x[j] = xj
176 k += step
177 else:
178 x = _autocast(x, self._field.itemtype)
179 self.validateItem(i, x)
181 self._list[i] = x
182 if setHistory:
183 if at is None:
184 at = getCallStack()
185 self.history.append((list(self._list), at, label))
187 @overload
188 def __getitem__(self, i: int) -> FieldTypeVar:
189 ...
191 @overload
192 def __getitem__(self, i: slice) -> MutableSequence[FieldTypeVar]:
193 ...
195 def __getitem__(self, i):
196 return self._list[i]
198 def __delitem__(self, i, at=None, label="delitem", setHistory=True):
199 if self._config._frozen:
200 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
201 del self._list[i]
202 if setHistory:
203 if at is None:
204 at = getCallStack()
205 self.history.append((list(self._list), at, label))
207 def __iter__(self):
208 return iter(self._list)
210 def insert(self, i, x, at=None, label="insert", setHistory=True):
211 """Insert an item into the list at the given index.
213 Parameters
214 ----------
215 i : `int`
216 Index where the item is inserted.
217 x : object
218 Item that is inserted.
219 at : `list` of `lsst.pex.config.callStack.StackFrame`, optional
220 The call stack (created by
221 `lsst.pex.config.callStack.getCallStack`).
222 label : `str`, optional
223 Event label for the history.
224 setHistory : `bool`, optional
225 Enable setting the field's history, using the value of the ``at``
226 parameter. Default is `True`.
227 """
228 if at is None:
229 at = getCallStack()
230 self.__setitem__(slice(i, i), [x], at=at, label=label, setHistory=setHistory)
232 def __repr__(self):
233 return repr(self._list)
235 def __str__(self):
236 return str(self._list)
238 def __eq__(self, other):
239 try:
240 if len(self) != len(other):
241 return False
243 for i, j in zip(self, other):
244 if i != j:
245 return False
246 return True
247 except AttributeError:
248 # other is not a sequence type
249 return False
251 def __ne__(self, other):
252 return not self.__eq__(other)
254 def __setattr__(self, attr, value, at=None, label="assignment"):
255 if hasattr(getattr(self.__class__, attr, None), "__set__"):
256 # This allows properties to work.
257 object.__setattr__(self, attr, value)
258 elif attr in self.__dict__ or attr in ["_field", "_config_", "_history", "_list", "__doc__"]:
259 # This allows specific private attributes to work.
260 object.__setattr__(self, attr, value)
261 else:
262 # We throw everything else.
263 msg = "%s has no attribute %s" % (_typeStr(self._field), attr)
264 raise FieldValidationError(self._field, self._config, msg)
266 def __reduce__(self):
267 raise UnexpectedProxyUsageError(
268 f"Proxy container for config field {self._field.name} cannot "
269 "be pickled; it should be converted to a built-in container before "
270 "being assigned to other objects or variables."
271 )
274class ListField(Field[List[FieldTypeVar]], Generic[FieldTypeVar]):
275 """A configuration field (`~lsst.pex.config.Field` subclass) that contains
276 a list of values of a specific type.
278 Parameters
279 ----------
280 doc : `str`
281 A description of the field.
282 dtype : class, optional
283 The data type of items in the list. Optional if supplied as typing
284 argument to the class.
285 default : sequence, optional
286 The default items for the field.
287 optional : `bool`, optional
288 Set whether the field is *optional*. When `False`,
289 `lsst.pex.config.Config.validate` will fail if the field's value is
290 `None`.
291 listCheck : callable, optional
292 A callable that validates the list as a whole.
293 itemCheck : callable, optional
294 A callable that validates individual items in the list.
295 length : `int`, optional
296 If set, this field must contain exactly ``length`` number of items.
297 minLength : `int`, optional
298 If set, this field must contain *at least* ``minLength`` number of
299 items.
300 maxLength : `int`, optional
301 If set, this field must contain *no more than* ``maxLength`` number of
302 items.
303 deprecated : None or `str`, optional
304 A description of why this Field is deprecated, including removal date.
305 If not None, the string is appended to the docstring for this Field.
307 See also
308 --------
309 ChoiceField
310 ConfigChoiceField
311 ConfigDictField
312 ConfigField
313 ConfigurableField
314 DictField
315 Field
316 RangeField
317 RegistryField
318 """
320 def __init__(
321 self,
322 doc,
323 dtype=None,
324 default=None,
325 optional=False,
326 listCheck=None,
327 itemCheck=None,
328 length=None,
329 minLength=None,
330 maxLength=None,
331 deprecated=None,
332 ):
333 if dtype is None: 333 ↛ 334line 333 didn't jump to line 334, because the condition on line 333 was never true
334 raise ValueError(
335 "dtype must either be supplied as an argument or as a type argument to the class"
336 )
337 if dtype not in Field.supportedTypes: 337 ↛ 338line 337 didn't jump to line 338, because the condition on line 337 was never true
338 raise ValueError("Unsupported dtype %s" % _typeStr(dtype))
339 if length is not None:
340 if length <= 0: 340 ↛ 341line 340 didn't jump to line 341, because the condition on line 340 was never true
341 raise ValueError("'length' (%d) must be positive" % length)
342 minLength = None
343 maxLength = None
344 else:
345 if maxLength is not None and maxLength <= 0: 345 ↛ 346line 345 didn't jump to line 346, because the condition on line 345 was never true
346 raise ValueError("'maxLength' (%d) must be positive" % maxLength)
347 if minLength is not None and maxLength is not None and minLength > maxLength: 347 ↛ 348line 347 didn't jump to line 348, because the condition on line 347 was never true
348 raise ValueError(
349 "'maxLength' (%d) must be at least as large as 'minLength' (%d)" % (maxLength, minLength)
350 )
352 if listCheck is not None and not hasattr(listCheck, "__call__"): 352 ↛ 353line 352 didn't jump to line 353, because the condition on line 352 was never true
353 raise ValueError("'listCheck' must be callable")
354 if itemCheck is not None and not hasattr(itemCheck, "__call__"): 354 ↛ 355line 354 didn't jump to line 355, because the condition on line 354 was never true
355 raise ValueError("'itemCheck' must be callable")
357 source = getStackFrame()
358 self._setup(
359 doc=doc,
360 dtype=List,
361 default=default,
362 check=None,
363 optional=optional,
364 source=source,
365 deprecated=deprecated,
366 )
368 self.listCheck = listCheck
369 """Callable used to check the list as a whole.
370 """
372 self.itemCheck = itemCheck
373 """Callable used to validate individual items as they are inserted
374 into the list.
375 """
377 self.itemtype = dtype
378 """Data type of list items.
379 """
381 self.length = length
382 """Number of items that must be present in the list (or `None` to
383 disable checking the list's length).
384 """
386 self.minLength = minLength
387 """Minimum number of items that must be present in the list (or `None`
388 to disable checking the list's minimum length).
389 """
391 self.maxLength = maxLength
392 """Maximum number of items that must be present in the list (or `None`
393 to disable checking the list's maximum length).
394 """
396 def validate(self, instance):
397 """Validate the field.
399 Parameters
400 ----------
401 instance : `lsst.pex.config.Config`
402 The config instance that contains this field.
404 Raises
405 ------
406 lsst.pex.config.FieldValidationError
407 Raised if:
409 - The field is not optional, but the value is `None`.
410 - The list itself does not meet the requirements of the `length`,
411 `minLength`, or `maxLength` attributes.
412 - The `listCheck` callable returns `False`.
414 Notes
415 -----
416 Individual item checks (`itemCheck`) are applied when each item is
417 set and are not re-checked by this method.
418 """
419 Field.validate(self, instance)
420 value = self.__get__(instance)
421 if value is not None:
422 lenValue = len(value)
423 if self.length is not None and not lenValue == self.length:
424 msg = "Required list length=%d, got length=%d" % (self.length, lenValue)
425 raise FieldValidationError(self, instance, msg)
426 elif self.minLength is not None and lenValue < self.minLength:
427 msg = "Minimum allowed list length=%d, got length=%d" % (self.minLength, lenValue)
428 raise FieldValidationError(self, instance, msg)
429 elif self.maxLength is not None and lenValue > self.maxLength:
430 msg = "Maximum allowed list length=%d, got length=%d" % (self.maxLength, lenValue)
431 raise FieldValidationError(self, instance, msg)
432 elif self.listCheck is not None and not self.listCheck(value):
433 msg = "%s is not a valid value" % str(value)
434 raise FieldValidationError(self, instance, msg)
436 def __set__(
437 self,
438 instance: Config,
439 value: Union[Iterable[FieldTypeVar], None],
440 at: Any = None,
441 label: str = "assignment",
442 ) -> None:
443 if instance._frozen:
444 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
446 if at is None:
447 at = getCallStack()
449 if value is not None:
450 value = List(instance, self, value, at, label)
451 else:
452 history = instance._history.setdefault(self.name, [])
453 history.append((value, at, label))
455 instance._storage[self.name] = value
457 def toDict(self, instance):
458 """Convert the value of this field to a plain `list`.
460 `lsst.pex.config.Config.toDict` is the primary user of this method.
462 Parameters
463 ----------
464 instance : `lsst.pex.config.Config`
465 The config instance that contains this field.
467 Returns
468 -------
469 `list`
470 Plain `list` of items, or `None` if the field is not set.
471 """
472 value = self.__get__(instance)
473 return list(value) if value is not None else None
475 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
476 """Compare two config instances for equality with respect to this
477 field.
479 `lsst.pex.config.config.compare` is the primary user of this method.
481 Parameters
482 ----------
483 instance1 : `lsst.pex.config.Config`
484 Left-hand-side `~lsst.pex.config.Config` instance in the
485 comparison.
486 instance2 : `lsst.pex.config.Config`
487 Right-hand-side `~lsst.pex.config.Config` instance in the
488 comparison.
489 shortcut : `bool`
490 If `True`, return as soon as an **inequality** is found.
491 rtol : `float`
492 Relative tolerance for floating point comparisons.
493 atol : `float`
494 Absolute tolerance for floating point comparisons.
495 output : callable
496 If not None, a callable that takes a `str`, used (possibly
497 repeatedly) to report inequalities.
499 Returns
500 -------
501 equal : `bool`
502 `True` if the fields are equal; `False` otherwise.
504 Notes
505 -----
506 Floating point comparisons are performed by `numpy.allclose`.
507 """
508 l1 = getattr(instance1, self.name)
509 l2 = getattr(instance2, self.name)
510 name = getComparisonName(
511 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
512 )
513 if not compareScalars("isnone for %s" % name, l1 is None, l2 is None, output=output):
514 return False
515 if l1 is None and l2 is None:
516 return True
517 if not compareScalars("size for %s" % name, len(l1), len(l2), output=output):
518 return False
519 equal = True
520 for n, v1, v2 in zip(range(len(l1)), l1, l2):
521 result = compareScalars(
522 "%s[%d]" % (name, n), v1, v2, dtype=self.dtype, rtol=rtol, atol=atol, output=output
523 )
524 if not result and shortcut:
525 return False
526 equal = equal and result
527 return equal