Coverage for python/lsst/pex/config/listField.py: 28%
194 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-10 09:56 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-10 09:56 +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`, optional
214 The call stack (created by
215 `lsst.pex.config.callStack.getCallStack`).
216 label : `str`, optional
217 Event label for the history.
218 setHistory : `bool`, optional
219 Enable setting the field's history, using the value of the ``at``
220 parameter. Default is `True`.
221 """
222 if at is None:
223 at = getCallStack()
224 self.__setitem__(slice(i, i), [x], at=at, label=label, setHistory=setHistory)
226 def __repr__(self):
227 return repr(self._list)
229 def __str__(self):
230 return str(self._list)
232 def __eq__(self, other):
233 try:
234 if len(self) != len(other):
235 return False
237 for i, j in zip(self, other):
238 if i != j:
239 return False
240 return True
241 except AttributeError:
242 # other is not a sequence type
243 return False
245 def __ne__(self, other):
246 return not self.__eq__(other)
248 def __setattr__(self, attr, value, at=None, label="assignment"):
249 if hasattr(getattr(self.__class__, attr, None), "__set__"):
250 # This allows properties to work.
251 object.__setattr__(self, attr, value)
252 elif attr in self.__dict__ or attr in ["_field", "_config_", "_history", "_list", "__doc__"]:
253 # This allows specific private attributes to work.
254 object.__setattr__(self, attr, value)
255 else:
256 # We throw everything else.
257 msg = f"{_typeStr(self._field)} has no attribute {attr}"
258 raise FieldValidationError(self._field, self._config, msg)
260 def __reduce__(self):
261 raise UnexpectedProxyUsageError(
262 f"Proxy container for config field {self._field.name} cannot "
263 "be pickled; it should be converted to a built-in container before "
264 "being assigned to other objects or variables."
265 )
268class ListField(Field[List[FieldTypeVar]], Generic[FieldTypeVar]):
269 """A configuration field (`~lsst.pex.config.Field` subclass) that contains
270 a list of values of a specific type.
272 Parameters
273 ----------
274 doc : `str`
275 A description of the field.
276 dtype : class, optional
277 The data type of items in the list. Optional if supplied as typing
278 argument to the class.
279 default : sequence, optional
280 The default items for the field.
281 optional : `bool`, optional
282 Set whether the field is *optional*. When `False`,
283 `lsst.pex.config.Config.validate` will fail if the field's value is
284 `None`.
285 listCheck : callable, optional
286 A callable that validates the list as a whole.
287 itemCheck : callable, optional
288 A callable that validates individual items in the list.
289 length : `int`, optional
290 If set, this field must contain exactly ``length`` number of items.
291 minLength : `int`, optional
292 If set, this field must contain *at least* ``minLength`` number of
293 items.
294 maxLength : `int`, optional
295 If set, this field must contain *no more than* ``maxLength`` number of
296 items.
297 deprecated : None or `str`, optional
298 A description of why this Field is deprecated, including removal date.
299 If not None, the string is appended to the docstring for this Field.
301 See Also
302 --------
303 ChoiceField
304 ConfigChoiceField
305 ConfigDictField
306 ConfigField
307 ConfigurableField
308 DictField
309 Field
310 RangeField
311 RegistryField
312 """
314 def __init__(
315 self,
316 doc,
317 dtype=None,
318 default=None,
319 optional=False,
320 listCheck=None,
321 itemCheck=None,
322 length=None,
323 minLength=None,
324 maxLength=None,
325 deprecated=None,
326 ):
327 if dtype is None: 327 ↛ 328line 327 didn't jump to line 328, because the condition on line 327 was never true
328 raise ValueError(
329 "dtype must either be supplied as an argument or as a type argument to the class"
330 )
331 if dtype not in Field.supportedTypes: 331 ↛ 332line 331 didn't jump to line 332, because the condition on line 331 was never true
332 raise ValueError("Unsupported dtype %s" % _typeStr(dtype))
333 if length is not None:
334 if length <= 0: 334 ↛ 335line 334 didn't jump to line 335, because the condition on line 334 was never true
335 raise ValueError("'length' (%d) must be positive" % length)
336 minLength = None
337 maxLength = None
338 else:
339 if maxLength is not None and maxLength <= 0: 339 ↛ 340line 339 didn't jump to line 340, because the condition on line 339 was never true
340 raise ValueError("'maxLength' (%d) must be positive" % maxLength)
341 if minLength is not None and maxLength is not None and minLength > maxLength: 341 ↛ 342line 341 didn't jump to line 342, because the condition on line 341 was never true
342 raise ValueError(
343 "'maxLength' (%d) must be at least as large as 'minLength' (%d)" % (maxLength, minLength)
344 )
346 if listCheck is not None and not hasattr(listCheck, "__call__"): 346 ↛ 347line 346 didn't jump to line 347, because the condition on line 346 was never true
347 raise ValueError("'listCheck' must be callable")
348 if itemCheck is not None and not hasattr(itemCheck, "__call__"): 348 ↛ 349line 348 didn't jump to line 349, because the condition on line 348 was never true
349 raise ValueError("'itemCheck' must be callable")
351 source = getStackFrame()
352 self._setup(
353 doc=doc,
354 dtype=List,
355 default=default,
356 check=None,
357 optional=optional,
358 source=source,
359 deprecated=deprecated,
360 )
362 self.listCheck = listCheck
363 """Callable used to check the list as a whole.
364 """
366 self.itemCheck = itemCheck
367 """Callable used to validate individual items as they are inserted
368 into the list.
369 """
371 self.itemtype = dtype
372 """Data type of list items.
373 """
375 self.length = length
376 """Number of items that must be present in the list (or `None` to
377 disable checking the list's length).
378 """
380 self.minLength = minLength
381 """Minimum number of items that must be present in the list (or `None`
382 to disable checking the list's minimum length).
383 """
385 self.maxLength = maxLength
386 """Maximum number of items that must be present in the list (or `None`
387 to disable checking the list's maximum length).
388 """
390 def validate(self, instance):
391 """Validate the field.
393 Parameters
394 ----------
395 instance : `lsst.pex.config.Config`
396 The config instance that contains this field.
398 Raises
399 ------
400 lsst.pex.config.FieldValidationError
401 Raised if:
403 - The field is not optional, but the value is `None`.
404 - The list itself does not meet the requirements of the ``length``,
405 ``minLength``, or ``maxLength`` attributes.
406 - The ``listCheck`` callable returns `False`.
408 Notes
409 -----
410 Individual item checks (``itemCheck``) are applied when each item is
411 set and are not re-checked by this method.
412 """
413 Field.validate(self, instance)
414 value = self.__get__(instance)
415 if value is not None:
416 lenValue = len(value)
417 if self.length is not None and not lenValue == self.length:
418 msg = "Required list length=%d, got length=%d" % (self.length, lenValue)
419 raise FieldValidationError(self, instance, msg)
420 elif self.minLength is not None and lenValue < self.minLength:
421 msg = "Minimum allowed list length=%d, got length=%d" % (self.minLength, lenValue)
422 raise FieldValidationError(self, instance, msg)
423 elif self.maxLength is not None and lenValue > self.maxLength:
424 msg = "Maximum allowed list length=%d, got length=%d" % (self.maxLength, lenValue)
425 raise FieldValidationError(self, instance, msg)
426 elif self.listCheck is not None and not self.listCheck(value):
427 msg = "%s is not a valid value" % str(value)
428 raise FieldValidationError(self, instance, msg)
430 def __set__(
431 self,
432 instance: Config,
433 value: Iterable[FieldTypeVar] | None,
434 at: Any = None,
435 label: str = "assignment",
436 ) -> None:
437 if instance._frozen:
438 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
440 if at is None:
441 at = getCallStack()
443 if value is not None:
444 value = List(instance, self, value, at, label)
445 else:
446 history = instance._history.setdefault(self.name, [])
447 history.append((value, at, label))
449 instance._storage[self.name] = value
451 def toDict(self, instance):
452 """Convert the value of this field to a plain `list`.
454 `lsst.pex.config.Config.toDict` is the primary user of this method.
456 Parameters
457 ----------
458 instance : `lsst.pex.config.Config`
459 The config instance that contains this field.
461 Returns
462 -------
463 `list`
464 Plain `list` of items, or `None` if the field is not set.
465 """
466 value = self.__get__(instance)
467 return list(value) if value is not None else None
469 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
470 """Compare two config instances for equality with respect to this
471 field.
473 `lsst.pex.config.config.compare` is the primary user of this method.
475 Parameters
476 ----------
477 instance1 : `lsst.pex.config.Config`
478 Left-hand-side `~lsst.pex.config.Config` instance in the
479 comparison.
480 instance2 : `lsst.pex.config.Config`
481 Right-hand-side `~lsst.pex.config.Config` instance in the
482 comparison.
483 shortcut : `bool`
484 If `True`, return as soon as an **inequality** is found.
485 rtol : `float`
486 Relative tolerance for floating point comparisons.
487 atol : `float`
488 Absolute tolerance for floating point comparisons.
489 output : callable
490 If not None, a callable that takes a `str`, used (possibly
491 repeatedly) to report inequalities.
493 Returns
494 -------
495 equal : `bool`
496 `True` if the fields are equal; `False` otherwise.
498 Notes
499 -----
500 Floating point comparisons are performed by `numpy.allclose`.
501 """
502 l1 = getattr(instance1, self.name)
503 l2 = getattr(instance2, self.name)
504 name = getComparisonName(
505 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
506 )
507 if not compareScalars("isnone for %s" % name, l1 is None, l2 is None, output=output):
508 return False
509 if l1 is None and l2 is None:
510 return True
511 if not compareScalars("size for %s" % name, len(l1), len(l2), output=output):
512 return False
513 equal = True
514 for n, v1, v2 in zip(range(len(l1)), l1, l2):
515 result = compareScalars(
516 "%s[%d]" % (name, n), v1, v2, dtype=self.dtype, rtol=rtol, atol=atol, output=output
517 )
518 if not result and shortcut:
519 return False
520 equal = equal and result
521 return equal