lsst.pex.config  18.1.0-2-g919ecaf
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 program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 
22 __all__ = ["ListField"]
23 
24 import collections.abc
25 
26 from .config import Field, FieldValidationError, _typeStr, _autocast, _joinNamePath
27 from .comparison import compareScalars, getComparisonName
28 from .callStack import getCallStack, getStackFrame
29 
30 
31 class List(collections.abc.MutableSequence):
32  """List collection used internally by `ListField`.
33 
34  Parameters
35  ----------
36  config : `lsst.pex.config.Config`
37  Config instance that contains the ``field``.
38  field : `ListField`
39  Instance of the `ListField` using this ``List``.
40  value : sequence
41  Sequence of values that are inserted into this ``List``.
42  at : `list` of `lsst.pex.config.callStack.StackFrame`
43  The call stack (created by `lsst.pex.config.callStack.getCallStack`).
44  label : `str`
45  Event label for the history.
46  setHistory : `bool`, optional
47  Enable setting the field's history, using the value of the ``at``
48  parameter. Default is `True`.
49 
50  Raises
51  ------
52  FieldValidationError
53  Raised if an item in the ``value`` parameter does not have the
54  appropriate type for this field or does not pass the
55  `ListField.itemCheck` method of the ``field`` parameter.
56  """
57 
58  def __init__(self, config, field, value, at, label, setHistory=True):
59  self._field = field
60  self._config = config
61  self._history = self._config._history.setdefault(self._field.name, [])
62  self._list = []
63  self.__doc__ = field.doc
64  if value is not None:
65  try:
66  for i, x in enumerate(value):
67  self.insert(i, x, setHistory=False)
68  except TypeError:
69  msg = "Value %s is of incorrect type %s. Sequence type expected" % (value, _typeStr(value))
70  raise FieldValidationError(self._field, self._config, msg)
71  if setHistory:
72  self.history.append((list(self._list), at, label))
73 
74  def validateItem(self, i, x):
75  """Validate an item to determine if it can be included in the list.
76 
77  Parameters
78  ----------
79  i : `int`
80  Index of the item in the `list`.
81  x : object
82  Item in the `list`.
83 
84  Raises
85  ------
86  FieldValidationError
87  Raised if an item in the ``value`` parameter does not have the
88  appropriate type for this field or does not pass the field's
89  `ListField.itemCheck` method.
90  """
91 
92  if not isinstance(x, self._field.itemtype) and x is not None:
93  msg = "Item at position %d with value %s is of incorrect type %s. Expected %s" % \
94  (i, x, _typeStr(x), _typeStr(self._field.itemtype))
95  raise FieldValidationError(self._field, self._config, msg)
96 
97  if self._field.itemCheck is not None and not self._field.itemCheck(x):
98  msg = "Item at position %d is not a valid value: %s" % (i, x)
99  raise FieldValidationError(self._field, self._config, msg)
100 
101  def list(self):
102  """Sequence of items contained by the `List` (`list`).
103  """
104  return self._list
105 
106  history = property(lambda x: x._history)
107  """Read-only history.
108  """
109 
110  def __contains__(self, x):
111  return x in self._list
112 
113  def __len__(self):
114  return len(self._list)
115 
116  def __setitem__(self, i, x, at=None, label="setitem", setHistory=True):
117  if self._config._frozen:
118  raise FieldValidationError(self._field, self._config,
119  "Cannot modify a frozen Config")
120  if isinstance(i, slice):
121  k, stop, step = i.indices(len(self))
122  for j, xj in enumerate(x):
123  xj = _autocast(xj, self._field.itemtype)
124  self.validateItem(k, xj)
125  x[j] = xj
126  k += step
127  else:
128  x = _autocast(x, self._field.itemtype)
129  self.validateItem(i, x)
130 
131  self._list[i] = x
132  if setHistory:
133  if at is None:
134  at = getCallStack()
135  self.history.append((list(self._list), at, label))
136 
137  def __getitem__(self, i):
138  return self._list[i]
139 
140  def __delitem__(self, i, at=None, label="delitem", setHistory=True):
141  if self._config._frozen:
142  raise FieldValidationError(self._field, self._config,
143  "Cannot modify a frozen Config")
144  del self._list[i]
145  if setHistory:
146  if at is None:
147  at = getCallStack()
148  self.history.append((list(self._list), at, label))
149 
150  def __iter__(self):
151  return iter(self._list)
152 
153  def insert(self, i, x, at=None, label="insert", setHistory=True):
154  """Insert an item into the list at the given index.
155 
156  Parameters
157  ----------
158  i : `int`
159  Index where the item is inserted.
160  x : object
161  Item that is inserted.
162  at : `list` of `lsst.pex.config.callStack.StackFrame`, optional
163  The call stack (created by
164  `lsst.pex.config.callStack.getCallStack`).
165  label : `str`, optional
166  Event label for the history.
167  setHistory : `bool`, optional
168  Enable setting the field's history, using the value of the ``at``
169  parameter. Default is `True`.
170  """
171  if at is None:
172  at = getCallStack()
173  self.__setitem__(slice(i, i), [x], at=at, label=label, setHistory=setHistory)
174 
175  def __repr__(self):
176  return repr(self._list)
177 
178  def __str__(self):
179  return str(self._list)
180 
181  def __eq__(self, other):
182  try:
183  if len(self) != len(other):
184  return False
185 
186  for i, j in zip(self, other):
187  if i != j:
188  return False
189  return True
190  except AttributeError:
191  # other is not a sequence type
192  return False
193 
194  def __ne__(self, other):
195  return not self.__eq__(other)
196 
197  def __setattr__(self, attr, value, at=None, label="assignment"):
198  if hasattr(getattr(self.__class__, attr, None), '__set__'):
199  # This allows properties to work.
200  object.__setattr__(self, attr, value)
201  elif attr in self.__dict__ or attr in ["_field", "_config", "_history", "_list", "__doc__"]:
202  # This allows specific private attributes to work.
203  object.__setattr__(self, attr, value)
204  else:
205  # We throw everything else.
206  msg = "%s has no attribute %s" % (_typeStr(self._field), attr)
207  raise FieldValidationError(self._field, self._config, msg)
208 
209 
211  """A configuration field (`~lsst.pex.config.Field` subclass) that contains
212  a list of values of a specific type.
213 
214  Parameters
215  ----------
216  doc : `str`
217  A description of the field.
218  dtype : class
219  The data type of items in the list.
220  default : sequence, optional
221  The default items for the field.
222  optional : `bool`, optional
223  Set whether the field is *optional*. When `False`,
224  `lsst.pex.config.Config.validate` will fail if the field's value is
225  `None`.
226  listCheck : callable, optional
227  A callable that validates the list as a whole.
228  itemCheck : callable, optional
229  A callable that validates individual items in the list.
230  length : `int`, optional
231  If set, this field must contain exactly ``length`` number of items.
232  minLength : `int`, optional
233  If set, this field must contain *at least* ``minLength`` number of
234  items.
235  maxLength : `int`, optional
236  If set, this field must contain *no more than* ``maxLength`` number of
237  items.
238  deprecated : None or `str`, optional
239  A description of why this Field is deprecated, including removal date.
240  If not None, the string is appended to the docstring for this Field.
241 
242  See also
243  --------
244  ChoiceField
245  ConfigChoiceField
246  ConfigDictField
247  ConfigField
248  ConfigurableField
249  DictField
250  Field
251  RangeField
252  RegistryField
253  """
254  def __init__(self, doc, dtype, default=None, optional=False,
255  listCheck=None, itemCheck=None,
256  length=None, minLength=None, maxLength=None,
257  deprecated=None):
258  if dtype not in Field.supportedTypes:
259  raise ValueError("Unsupported dtype %s" % _typeStr(dtype))
260  if length is not None:
261  if length <= 0:
262  raise ValueError("'length' (%d) must be positive" % length)
263  minLength = None
264  maxLength = None
265  else:
266  if maxLength is not None and maxLength <= 0:
267  raise ValueError("'maxLength' (%d) must be positive" % maxLength)
268  if minLength is not None and maxLength is not None \
269  and minLength > maxLength:
270  raise ValueError("'maxLength' (%d) must be at least"
271  " as large as 'minLength' (%d)" % (maxLength, minLength))
272 
273  if listCheck is not None and not hasattr(listCheck, "__call__"):
274  raise ValueError("'listCheck' must be callable")
275  if itemCheck is not None and not hasattr(itemCheck, "__call__"):
276  raise ValueError("'itemCheck' must be callable")
277 
278  source = getStackFrame()
279  self._setup(doc=doc, dtype=List, default=default, check=None, optional=optional, source=source,
280  deprecated=deprecated)
281 
282  self.listCheck = listCheck
283  """Callable used to check the list as a whole.
284  """
285 
286  self.itemCheck = itemCheck
287  """Callable used to validate individual items as they are inserted
288  into the list.
289  """
290 
291  self.itemtype = dtype
292  """Data type of list items.
293  """
294 
295  self.length = length
296  """Number of items that must be present in the list (or `None` to
297  disable checking the list's length).
298  """
299 
300  self.minLength = minLength
301  """Minimum number of items that must be present in the list (or `None`
302  to disable checking the list's minimum length).
303  """
304 
305  self.maxLength = maxLength
306  """Maximum number of items that must be present in the list (or `None`
307  to disable checking the list's maximum length).
308  """
309 
310  def validate(self, instance):
311  """Validate the field.
312 
313  Parameters
314  ----------
315  instance : `lsst.pex.config.Config`
316  The config instance that contains this field.
317 
318  Raises
319  ------
320  lsst.pex.config.FieldValidationError
321  Raised if:
322 
323  - The field is not optional, but the value is `None`.
324  - The list itself does not meet the requirements of the `length`,
325  `minLength`, or `maxLength` attributes.
326  - The `listCheck` callable returns `False`.
327 
328  Notes
329  -----
330  Individual item checks (`itemCheck`) are applied when each item is
331  set and are not re-checked by this method.
332  """
333  Field.validate(self, instance)
334  value = self.__get__(instance)
335  if value is not None:
336  lenValue = len(value)
337  if self.length is not None and not lenValue == self.length:
338  msg = "Required list length=%d, got length=%d" % (self.length, lenValue)
339  raise FieldValidationError(self, instance, msg)
340  elif self.minLength is not None and lenValue < self.minLength:
341  msg = "Minimum allowed list length=%d, got length=%d" % (self.minLength, lenValue)
342  raise FieldValidationError(self, instance, msg)
343  elif self.maxLength is not None and lenValue > self.maxLength:
344  msg = "Maximum allowed list length=%d, got length=%d" % (self.maxLength, lenValue)
345  raise FieldValidationError(self, instance, msg)
346  elif self.listCheck is not None and not self.listCheck(value):
347  msg = "%s is not a valid value" % str(value)
348  raise FieldValidationError(self, instance, msg)
349 
350  def __set__(self, instance, value, at=None, label="assignment"):
351  if instance._frozen:
352  raise FieldValidationError(self, instance, "Cannot modify a frozen Config")
353 
354  if at is None:
355  at = getCallStack()
356 
357  if value is not None:
358  value = List(instance, self, value, at, label)
359  else:
360  history = instance._history.setdefault(self.name, [])
361  history.append((value, at, label))
362 
363  instance._storage[self.name] = value
364 
365  def toDict(self, instance):
366  """Convert the value of this field to a plain `list`.
367 
368  `lsst.pex.config.Config.toDict` is the primary user of this method.
369 
370  Parameters
371  ----------
372  instance : `lsst.pex.config.Config`
373  The config instance that contains this field.
374 
375  Returns
376  -------
377  `list`
378  Plain `list` of items, or `None` if the field is not set.
379  """
380  value = self.__get__(instance)
381  return list(value) if value is not None else None
382 
383  def _compare(self, instance1, instance2, shortcut, rtol, atol, output):
384  """Compare two config instances for equality with respect to this
385  field.
386 
387  `lsst.pex.config.config.compare` is the primary user of this method.
388 
389  Parameters
390  ----------
391  instance1 : `lsst.pex.config.Config`
392  Left-hand-side `~lsst.pex.config.Config` instance in the
393  comparison.
394  instance2 : `lsst.pex.config.Config`
395  Right-hand-side `~lsst.pex.config.Config` instance in the
396  comparison.
397  shortcut : `bool`
398  If `True`, return as soon as an **inequality** is found.
399  rtol : `float`
400  Relative tolerance for floating point comparisons.
401  atol : `float`
402  Absolute tolerance for floating point comparisons.
403  output : callable
404  If not None, a callable that takes a `str`, used (possibly
405  repeatedly) to report inequalities.
406 
407  Returns
408  -------
409  equal : `bool`
410  `True` if the fields are equal; `False` otherwise.
411 
412  Notes
413  -----
414  Floating point comparisons are performed by `numpy.allclose`.
415  """
416  l1 = getattr(instance1, self.name)
417  l2 = getattr(instance2, self.name)
418  name = getComparisonName(
419  _joinNamePath(instance1._name, self.name),
420  _joinNamePath(instance2._name, self.name)
421  )
422  if not compareScalars("isnone for %s" % name, l1 is None, l2 is None, output=output):
423  return False
424  if l1 is None and l2 is None:
425  return True
426  if not compareScalars("size for %s" % name, len(l1), len(l2), output=output):
427  return False
428  equal = True
429  for n, v1, v2 in zip(range(len(l1)), l1, l2):
430  result = compareScalars("%s[%d]" % (name, n), v1, v2, dtype=self.dtype,
431  rtol=rtol, atol=atol, output=output)
432  if not result and shortcut:
433  return False
434  equal = equal and result
435  return equal
def getCallStack(skip=0)
Definition: callStack.py:168
def __set__(self, instance, value, at=None, label="assignment")
Definition: listField.py:350
def validate(self, instance)
Definition: listField.py:310
def __init__(self, config, field, value, at, label, setHistory=True)
Definition: listField.py:58
def insert(self, i, x, at=None, label="insert", setHistory=True)
Definition: listField.py:153
def __contains__(self, x)
Definition: listField.py:110
def getStackFrame(relative=0)
Definition: callStack.py:51
def toDict(self, instance)
Definition: listField.py:365
def __setattr__(self, attr, value, at=None, label="assignment")
Definition: listField.py:197
def __get__(self, instance, owner=None, at=None, label="default")
Definition: config.py:488
def __ne__(self, other)
Definition: listField.py:194
def validateItem(self, i, x)
Definition: listField.py:74
def __setitem__(self, i, x, at=None, label="setitem", setHistory=True)
Definition: listField.py:116
def getComparisonName(name1, name2)
Definition: comparison.py:34
def __init__(self, doc, dtype, default=None, optional=False, listCheck=None, itemCheck=None, length=None, minLength=None, maxLength=None, deprecated=None)
Definition: listField.py:257
def compareScalars(name, v1, v2, output, rtol=1E-8, atol=1E-8, dtype=None)
Definition: comparison.py:56
def __eq__(self, other)
Definition: listField.py:181
def __getitem__(self, i)
Definition: listField.py:137
def __delitem__(self, i, at=None, label="delitem", setHistory=True)
Definition: listField.py:140
def _setup(self, doc, dtype, default, check, optional, source, deprecated)
Definition: config.py:278