LSST Applications g00d0e8bbd7+8c5ae1fdc5,g013ef56533+603670b062,g083dd6704c+2e189452a7,g199a45376c+0ba108daf9,g1c5cce2383+bc9f6103a4,g1fd858c14a+cd69ed4fc1,g210f2d0738+c4742f2e9e,g262e1987ae+612fa42d85,g29ae962dfc+83d129e820,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+5eaa884f2a,g47891489e3+e32160a944,g53246c7159+8c5ae1fdc5,g5b326b94bb+dcc56af22d,g64539dfbff+c4742f2e9e,g67b6fd64d1+e32160a944,g74acd417e5+c122e1277d,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g88cb488625+47d24e4084,g89139ef638+e32160a944,g8d7436a09f+d14b4ff40a,g8ea07a8fe4+b212507b11,g90f42f885a+e1755607f3,g97be763408+34be90ab8c,g98df359435+ec1fa61bf1,ga2180abaac+8c5ae1fdc5,ga9e74d7ce9+43ac651df0,gbf99507273+8c5ae1fdc5,gc2a301910b+c4742f2e9e,gca7fc764a6+e32160a944,gd7ef33dd92+e32160a944,gdab6d2f7ff+c122e1277d,gdb1e2cdc75+1b18322db8,ge410e46f29+e32160a944,ge41e95a9f2+c4742f2e9e,geaed405ab2+0d91c11c6d,w.2025.44
LSST Data Management Base Package
Loading...
Searching...
No Matches
listField.py
Go to the documentation of this file.
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/>.
27
28from __future__ import annotations
29
30__all__ = ["ListField"]
31
32import collections.abc
33import weakref
34from collections.abc import Iterable, MutableSequence, Sequence
35from typing import Any, Generic, overload
36
37from .callStack import StackFrame, getCallStack, getStackFrame
38from .comparison import compareScalars, getComparisonName
39from .config import (
40 Config,
41 Field,
42 FieldTypeVar,
43 FieldValidationError,
44 UnexpectedProxyUsageError,
45 _autocast,
46 _joinNamePath,
47 _typeStr,
48)
49
50
51class List(collections.abc.MutableSequence[FieldTypeVar]):
52 """List collection used internally by `ListField`.
53
54 Parameters
55 ----------
56 config : `lsst.pex.config.Config`
57 Config instance that contains the ``field``.
58 field : `ListField`
59 Instance of the `ListField` using this ``List``.
60 value : sequence
61 Sequence of values that are inserted into this ``List``.
62 at : `list` of `~lsst.pex.config.callStack.StackFrame`
63 The call stack (created by `lsst.pex.config.callStack.getCallStack`).
64 label : `str`
65 Event label for the history.
66 setHistory : `bool`, optional
67 Enable setting the field's history, using the value of the ``at``
68 parameter. Default is `True`.
69
70 Raises
71 ------
72 FieldValidationError
73 Raised if an item in the ``value`` parameter does not have the
74 appropriate type for this field or does not pass the
75 `ListField.itemCheck` method of the ``field`` parameter.
76 """
77
79 self,
80 config: Config,
81 field: ListField,
82 value: Sequence[FieldTypeVar],
83 at: list[StackFrame] | None,
84 label: str,
85 setHistory: bool = True,
86 ):
87 self._field = field
88 self._config_ = weakref.ref(config)
89 self._history = self._config._history.setdefault(self._field.name, [])
90 self._list = []
91 self.__doc__ = field.doc
92 if value is not None:
93 try:
94 for i, x in enumerate(value):
95 self.insert(i, x, setHistory=False)
96 except TypeError as e:
97 msg = f"Value {value} is of incorrect type {_typeStr(value)}. Sequence type expected"
98 raise FieldValidationError(self._field, config, msg) from e
99 if setHistory:
100 self.history.append((list(self._list), at, label))
101
102 @property
103 def _config(self) -> Config:
104 # Config Fields should never outlive their config class instance
105 # assert that as such here
106 value = self._config_()
107 assert value is not None
108 return value
109
110 def validateItem(self, i, x):
111 """Validate an item to determine if it can be included in the list.
112
113 Parameters
114 ----------
115 i : `int`
116 Index of the item in the `list`.
117 x : object
118 Item in the `list`.
119
120 Raises
121 ------
122 FieldValidationError
123 Raised if an item in the ``value`` parameter does not have the
124 appropriate type for this field or does not pass the field's
125 `ListField.itemCheck` method.
126 """
127 if not isinstance(x, self._field.itemtype) and x is not None:
128 msg = (
129 f"Item at position {i} with value {x} is of incorrect type {_typeStr(x)}. "
130 f"Expected {_typeStr(self._field.itemtype)}"
131 )
132 raise FieldValidationError(self._field, self._config, msg)
133
134 if self._field.itemCheck is not None and not self._field.itemCheck(x):
135 msg = f"Item at position {i} is not a valid value: {x}"
136 raise FieldValidationError(self._field, self._config, msg)
137
138 def list(self):
139 """Sequence of items contained by the `List` (`list`)."""
140 return self._list
141
142 history = property(lambda x: x._history)
143 """Read-only history.
144 """
145
146 def _copy(self, config: Config) -> List:
147 return type(self)(config, self._field, self._list.copy(), at=None, label="copy", setHistory=False)
148
149 def __contains__(self, x):
150 return x in self._list
151
152 def __len__(self):
153 return len(self._list)
154
155 @overload
157 self, i: int, x: FieldTypeVar, at: Any = None, label: str = "setitem", setHistory: bool = True
158 ) -> None: ...
159
160 @overload
162 self,
163 i: slice,
164 x: Iterable[FieldTypeVar],
165 at: Any = None,
166 label: str = "setitem",
167 setHistory: bool = True,
168 ) -> None: ...
169
170 def __setitem__(self, i, x, at=None, label="setitem", setHistory=True):
171 if self._config._frozen:
172 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
173 if isinstance(i, slice):
174 k, stop, step = i.indices(len(self))
175 for j, xj in enumerate(x):
176 xj = _autocast(xj, self._field.itemtype)
177 self.validateItem(k, xj)
178 x[j] = xj
179 k += step
180 else:
181 x = _autocast(x, self._field.itemtype)
182 self.validateItem(i, x)
183
184 self._list[i] = x
185 if setHistory:
186 if at is None:
187 at = getCallStack()
188 self.history.append((list(self._list), at, label))
189
190 @overload
191 def __getitem__(self, i: int) -> FieldTypeVar: ...
192
193 @overload
194 def __getitem__(self, i: slice) -> MutableSequence[FieldTypeVar]: ...
195
196 def __getitem__(self, i):
197 return self._list[i]
198
199 def __delitem__(self, i, at=None, label="delitem", setHistory=True):
200 if self._config._frozen:
201 raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
202 del self._list[i]
203 if setHistory:
204 if at is None:
205 at = getCallStack()
206 self.history.append((list(self._list), at, label))
207
208 def __iter__(self):
209 return iter(self._list)
210
211 def insert(self, i, x, at=None, label="insert", setHistory=True):
212 """Insert an item into the list at the given index.
213
214 Parameters
215 ----------
216 i : `int`
217 Index where the item is inserted.
218 x : object
219 Item that is inserted.
220 at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`,\
221 optional
222 The call stack (created by
223 `lsst.pex.config.callStack.getCallStack`).
224 label : `str`, optional
225 Event label for the history.
226 setHistory : `bool`, optional
227 Enable setting the field's history, using the value of the ``at``
228 parameter. Default is `True`.
229 """
230 if at is None:
231 at = getCallStack()
232 self.__setitem__(slice(i, i), [x], at=at, label=label, setHistory=setHistory)
233
234 def __repr__(self):
235 return repr(self._list)
236
237 def __str__(self):
238 return str(self._list)
239
240 def __eq__(self, other):
241 if other is None:
242 return False
243 try:
244 if len(self) != len(other):
245 return False
246
247 for i, j in zip(self, other, strict=True):
248 if i != j:
249 return False
250 return True
251 except AttributeError:
252 # other is not a sequence type
253 return False
254
255 def __ne__(self, other):
256 return not self.__eq__(other)
257
258 def __setattr__(self, attr, value, at=None, label="assignment"):
259 if hasattr(getattr(self.__class__, attr, None), "__set__"):
260 # This allows properties to work.
261 object.__setattr__(self, attr, value)
262 elif attr in self.__dict__ or attr in ["_field", "_config_", "_history", "_list", "__doc__"]:
263 # This allows specific private attributes to work.
264 object.__setattr__(self, attr, value)
265 else:
266 # We throw everything else.
267 msg = f"{_typeStr(self._field)} has no attribute {attr}"
268 raise FieldValidationError(self._field, self._config, msg)
269
270 def __reduce__(self):
272 f"Proxy container for config field {self._field.name} cannot "
273 "be pickled; it should be converted to a built-in container before "
274 "being assigned to other objects or variables."
275 )
276
277
278class ListField(Field[List[FieldTypeVar]], Generic[FieldTypeVar]):
279 """A configuration field (`~lsst.pex.config.Field` subclass) that contains
280 a list of values of a specific type.
281
282 Parameters
283 ----------
284 doc : `str`
285 A description of the field.
286 dtype : class, optional
287 The data type of items in the list. Optional if supplied as typing
288 argument to the class.
289 default : sequence, optional
290 The default items for the field.
291 optional : `bool`, optional
292 Set whether the field is *optional*. When `False`,
293 `lsst.pex.config.Config.validate` will fail if the field's value is
294 `None`.
295 listCheck : callable, optional
296 A callable that validates the list as a whole.
297 itemCheck : callable, optional
298 A callable that validates individual items in the list.
299 length : `int`, optional
300 If set, this field must contain exactly ``length`` number of items.
301 minLength : `int`, optional
302 If set, this field must contain *at least* ``minLength`` number of
303 items.
304 maxLength : `int`, optional
305 If set, this field must contain *no more than* ``maxLength`` number of
306 items.
307 deprecated : None or `str`, optional
308 A description of why this Field is deprecated, including removal date.
309 If not None, the string is appended to the docstring for this Field.
310
311 See Also
312 --------
313 ChoiceField
314 ConfigChoiceField
315 ConfigDictField
316 ConfigField
317 ConfigurableField
318 DictField
319 Field
320 RangeField
321 RegistryField
322 """
323
325 self,
326 doc,
327 dtype=None,
328 default=None,
329 optional=False,
330 listCheck=None,
331 itemCheck=None,
332 length=None,
333 minLength=None,
334 maxLength=None,
335 deprecated=None,
336 ):
337 if dtype is None:
338 raise ValueError(
339 "dtype must either be supplied as an argument or as a type argument to the class"
340 )
341 if dtype not in Field.supportedTypes:
342 raise ValueError(f"Unsupported dtype {_typeStr(dtype)}")
343 if length is not None:
344 if length <= 0:
345 raise ValueError(f"'length' ({length}) must be positive")
346 minLength = None
347 maxLength = None
348 else:
349 if maxLength is not None and maxLength <= 0:
350 raise ValueError(f"'maxLength' ({maxLength}) must be positive")
351 if minLength is not None and maxLength is not None and minLength > maxLength:
352 raise ValueError(
353 f"'maxLength' ({maxLength}) must be at least as large as 'minLength' ({minLength})"
354 )
355
356 if listCheck is not None and not callable(listCheck):
357 raise ValueError("'listCheck' must be callable")
358 if itemCheck is not None and not callable(itemCheck):
359 raise ValueError("'itemCheck' must be callable")
360
361 source = getStackFrame()
362 self._setup(
363 doc=doc,
364 dtype=List,
365 default=default,
366 check=None,
367 optional=optional,
368 source=source,
369 deprecated=deprecated,
370 )
371
372 self.listCheck = listCheck
373 """Callable used to check the list as a whole.
374 """
375
376 self.itemCheck = itemCheck
377 """Callable used to validate individual items as they are inserted
378 into the list.
379 """
380
381 self.itemtype = dtype
382 """Data type of list items.
383 """
384
385 self.length = length
386 """Number of items that must be present in the list (or `None` to
387 disable checking the list's length).
388 """
389
390 self.minLength = minLength
391 """Minimum number of items that must be present in the list (or `None`
392 to disable checking the list's minimum length).
393 """
394
395 self.maxLength = maxLength
396 """Maximum number of items that must be present in the list (or `None`
397 to disable checking the list's maximum length).
398 """
399
400 def validate(self, instance):
401 """Validate the field.
402
403 Parameters
404 ----------
405 instance : `lsst.pex.config.Config`
406 The config instance that contains this field.
407
408 Raises
409 ------
410 lsst.pex.config.FieldValidationError
411 Raised if:
412
413 - The field is not optional, but the value is `None`.
414 - The list itself does not meet the requirements of the ``length``,
415 ``minLength``, or ``maxLength`` attributes.
416 - The ``listCheck`` callable returns `False`.
417
418 Notes
419 -----
420 Individual item checks (``itemCheck``) are applied when each item is
421 set and are not re-checked by this method.
422 """
423 Field.validate(self, instance)
424 value = self.__get__(instance)
425 if value is not None:
426 lenValue = len(value)
427 if self.length is not None and not lenValue == self.length:
428 msg = f"Required list length={self.length}, got length={lenValue}"
429 raise FieldValidationError(self, instance, msg)
430 elif self.minLength is not None and lenValue < self.minLength:
431 msg = f"Minimum allowed list length={self.minLength}, got length={lenValue}"
432 raise FieldValidationError(self, instance, msg)
433 elif self.maxLength is not None and lenValue > self.maxLength:
434 msg = f"Maximum allowed list length={self.maxLength}, got length={lenValue}"
435 raise FieldValidationError(self, instance, msg)
436 elif self.listCheck is not None and not self.listCheck(value):
437 msg = f"{value} is not a valid value"
438 raise FieldValidationError(self, instance, msg)
439
441 self,
442 instance: Config,
443 value: Iterable[FieldTypeVar] | None,
444 at: Any = None,
445 label: str = "assignment",
446 ) -> None:
447 if instance._frozen:
448 raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
449
450 if at is None:
451 at = getCallStack()
452
453 if value is not None:
454 value = List(instance, self, value, at, label)
455 else:
456 history = instance._history.setdefault(self.name, [])
457 history.append((value, at, label))
458
459 instance._storage[self.name] = value
460
461 def toDict(self, instance):
462 """Convert the value of this field to a plain `list`.
463
464 `lsst.pex.config.Config.toDict` is the primary user of this method.
465
466 Parameters
467 ----------
468 instance : `lsst.pex.config.Config`
469 The config instance that contains this field.
470
471 Returns
472 -------
473 `list`
474 Plain `list` of items, or `None` if the field is not set.
475 """
476 value = self.__get__(instance)
477 return list(value) if value is not None else None
478
479 def _copy_storage(self, old: Config, new: Config) -> List[FieldTypeVar] | None:
480 value: List[FieldTypeVar] | None = old._storage[self.name]
481 if value is not None:
482 return value._copy(new)
483 else:
484 return None
485
486 def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
487 """Compare two config instances for equality with respect to this
488 field.
489
490 `lsst.pex.config.config.compare` is the primary user of this method.
491
492 Parameters
493 ----------
494 instance1 : `lsst.pex.config.Config`
495 Left-hand-side `~lsst.pex.config.Config` instance in the
496 comparison.
497 instance2 : `lsst.pex.config.Config`
498 Right-hand-side `~lsst.pex.config.Config` instance in the
499 comparison.
500 shortcut : `bool`
501 If `True`, return as soon as an **inequality** is found.
502 rtol : `float`
503 Relative tolerance for floating point comparisons.
504 atol : `float`
505 Absolute tolerance for floating point comparisons.
506 output : callable
507 If not None, a callable that takes a `str`, used (possibly
508 repeatedly) to report inequalities.
509
510 Returns
511 -------
512 equal : `bool`
513 `True` if the fields are equal; `False` otherwise.
514
515 Notes
516 -----
517 Floating point comparisons are performed by `numpy.allclose`.
518 """
519 l1 = getattr(instance1, self.name)
520 l2 = getattr(instance2, self.name)
521 name = getComparisonName(
522 _joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
523 )
524 if l1 is None or l2 is None:
525 return compareScalars(name, l1, l2, output=output)
526 if not compareScalars(f"{name} (len)", len(l1), len(l2), output=output):
527 return False
528 equal = True
529 for n, v1, v2 in zip(range(len(l1)), l1, l2, strict=True):
530 result = compareScalars(
531 f"{name}[{n}]", v1, v2, dtype=self.dtype, rtol=rtol, atol=atol, output=output
532 )
533
534 if not result and shortcut:
535 return False
536 equal = equal and result
537 return equal
Field[FieldTypeVar] __get__(self, None instance, Any owner=None, Any at=None, str label="default")
Definition config.py:714
_setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition config.py:486
_compare(self, instance1, instance2, shortcut, rtol, atol, output)
Definition listField.py:486
__init__(self, doc, dtype=None, default=None, optional=False, listCheck=None, itemCheck=None, length=None, minLength=None, maxLength=None, deprecated=None)
Definition listField.py:336
None __set__(self, Config instance, Iterable[FieldTypeVar]|None value, Any at=None, str label="assignment")
Definition listField.py:446
List[FieldTypeVar]|None _copy_storage(self, Config old, Config new)
Definition listField.py:479
FieldTypeVar __getitem__(self, int i)
Definition listField.py:191
None __setitem__(self, int i, FieldTypeVar x, Any at=None, str label="setitem", bool setHistory=True)
Definition listField.py:158
List _copy(self, Config config)
Definition listField.py:146
insert(self, i, x, at=None, label="insert", setHistory=True)
Definition listField.py:211
__delitem__(self, i, at=None, label="delitem", setHistory=True)
Definition listField.py:199
__setattr__(self, attr, value, at=None, label="assignment")
Definition listField.py:258
__init__(self, Config config, ListField field, Sequence[FieldTypeVar] value, list[StackFrame]|None at, str label, bool setHistory=True)
Definition listField.py:86
getComparisonName(name1, name2)
Definition comparison.py:40
compareScalars(name, v1, v2, output, rtol=1e-8, atol=1e-8, dtype=None)
Definition comparison.py:62
_autocast(x, dtype)
Definition config.py:122
_joinNamePath(prefix=None, name=None, index=None)
Definition config.py:107