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