lsst.daf.base  21.0.0-1-gfc31b0f+3b24369756
propertyContainerContinued.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 #
4 # Copyright 2008-2017 AURA/LSST.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <http://www.lsstcorp.org/LegalNotices/>.
22 #
23 
24 
25 __all__ = ["getPropertySetState", "getPropertyListState", "setPropertySetState", "setPropertyListState"]
26 
27 import enum
28 import numbers
29 from collections.abc import Mapping, KeysView, ValuesView, ItemsView
30 
31 # Ensure that C++ exceptions are properly translated to Python
32 import lsst.pex.exceptions # noqa: F401
33 from lsst.utils import continueClass
34 
35 from .propertySet import PropertySet
36 from .propertyList import PropertyList
37 from ..dateTime import DateTime
38 
39 
40 def getPropertySetState(container, asLists=False):
41  """Get the state of a PropertySet in a form that can be pickled.
42 
43  Parameters
44  ----------
45  container : `PropertySet`
46  The property container.
47  asLists : `bool`, optional
48  If False, the default, `tuple` will be used for the contents. If true
49  a `list` will be used.
50 
51  Returns
52  -------
53  state : `list` of `tuple` or `list` of `list`
54  The state, as a list of tuples (or lists), each of which contains
55  the following 3 items:
56 
57  name (a `str`)
58  the name of the item
59  elementTypeName (a `str`)
60  the suffix of a ``setX`` method name
61  which is appropriate for the data type. For example integer
62  data has ``elementTypeName="Int"` which corresponds to
63  the ``setInt`` method.
64  value
65  the data for the item, in a form compatible
66  with the set method named by ``elementTypeName``
67  """
68  names = container.names(topLevelOnly=True)
69  sequence = list if asLists else tuple
70  return [sequence((name, _propertyContainerElementTypeName(container, name),
71  _propertyContainerGet(container, name, returnStyle=ReturnStyle.AUTO)))
72  for name in names]
73 
74 
75 def getPropertyListState(container, asLists=False):
76  """Get the state of a PropertyList in a form that can be pickled.
77 
78  Parameters
79  ----------
80  container : `PropertyList`
81  The property container.
82  asLists : `bool`, optional
83  If False, the default, `tuple` will be used for the contents. If true
84  a `list` will be used.
85 
86  Returns
87  -------
88  state : `list` of `tuple` or `list` of `list`
89  The state, as a list of tuples (or lists), each of which contains
90  the following 4 items:
91 
92  name (a `str`):
93  the name of the item
94  elementTypeName (a `str`):
95  the suffix of a ``setX`` method name
96  which is appropriate for the data type. For example integer
97  data has ``elementTypeName="Int"` which corresponds to
98  the ``setInt`` method.
99  value
100  the data for the item, in a form compatible
101  with the set method named by ``elementTypeName``
102  comment (a `str`): the comment. This item is only present
103  if ``container`` is a PropertyList.
104  """
105  sequence = list if asLists else tuple
106  return [sequence((name, _propertyContainerElementTypeName(container, name),
107  _propertyContainerGet(container, name, returnStyle=ReturnStyle.AUTO),
108  container.getComment(name)))
109  for name in container.getOrderedNames()]
110 
111 
112 def setPropertySetState(container, state):
113  """Restore the state of a PropertySet, in place.
114 
115  Parameters
116  ----------
117  container : `PropertySet`
118  The property container whose state is to be restored.
119  It should be empty to start with and is updated in place.
120  state : `list`
121  The state, as returned by `getPropertySetState`
122  """
123  for name, elemType, value in state:
124  if elemType is not None:
125  getattr(container, "set" + elemType)(name, value)
126  else:
127  raise ValueError(f"Unrecognized values for state restoration: ({name}, {elemType}, {value})")
128 
129 
130 def setPropertyListState(container, state):
131  """Restore the state of a PropertyList, in place.
132 
133  Parameters
134  ----------
135  container : `PropertyList`
136  The property container whose state is to be restored.
137  It should be empty to start with and is updated in place.
138  state : `list`
139  The state, as returned by ``getPropertyListState``
140  """
141  for name, elemType, value, comment in state:
142  getattr(container, "set" + elemType)(name, value, comment)
143 
144 
145 class ReturnStyle(enum.Enum):
146  ARRAY = enum.auto()
147  SCALAR = enum.auto()
148  AUTO = enum.auto()
149 
150 
151 def _propertyContainerElementTypeName(container, name):
152  """Return name of the type of a particular element
153 
154  Parameters
155  ----------
156  container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
157  Container including the element
158  name : `str`
159  Name of element
160  """
161  try:
162  t = container.typeOf(name)
163  except LookupError as e:
164  # KeyError is more commonly expected when asking for an element
165  # from a mapping.
166  raise KeyError(str(e))
167  for checkType in ("Bool", "Short", "Int", "Long", "LongLong", "UnsignedLongLong",
168  "Float", "Double", "String", "DateTime",
169  "PropertySet", "Undef"):
170  if t == getattr(container, "TYPE_" + checkType):
171  return checkType
172  return None
173 
174 
175 def _propertyContainerGet(container, name, returnStyle):
176  """Get a value of unknown type as a scalar or array
177 
178  Parameters
179  ----------
180  container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
181  Container from which to get the value
182  name : `str`
183  Name of item
184  returnStyle : `ReturnStyle`
185  Control whether numeric or string data is returned as an array
186  or scalar (the other types, ``PropertyList``, ``PropertySet``
187  and ``PersistablePtr``, are always returned as a scalar):
188  - ReturnStyle.ARRAY: return numeric or string data types
189  as an array of values.
190  - ReturnStyle.SCALAR: return numeric or string data types
191  as a single value; if the item has multiple values then
192  return the last value.
193  - ReturnStyle.AUTO: (deprecated) return numeric or string data
194  as a scalar if there is just one item, or as an array
195  otherwise.
196 
197  Raises
198  ------
199  KeyError
200  Raised if the specified key does not exist in the container.
201  TypeError
202  Raised if the value retrieved is of an unexpected type.
203  ValueError
204  Raised if the value for ``returnStyle`` is not correct.
205  """
206  if not container.exists(name):
207  raise KeyError(name + " not found")
208  if returnStyle not in ReturnStyle:
209  raise ValueError("returnStyle {} must be a ReturnStyle".format(returnStyle))
210 
211  elemType = _propertyContainerElementTypeName(container, name)
212  if elemType and elemType != "PropertySet":
213  value = getattr(container, "getArray" + elemType)(name)
214  if returnStyle == ReturnStyle.ARRAY or (returnStyle == ReturnStyle.AUTO and len(value) > 1):
215  return value
216  return value[-1]
217 
218  if container.isPropertySetPtr(name):
219  try:
220  return container.getAsPropertyListPtr(name)
221  except Exception:
222  return container.getAsPropertySetPtr(name)
223  try:
224  return container.getAsPersistablePtr(name)
225  except Exception:
226  pass
227  raise TypeError('Unknown PropertySet value type for ' + name)
228 
229 
230 def _iterable(a):
231  """Make input iterable.
232 
233  Takes whatever is given to it and yields it back one element at a time.
234  If it is not an iterable or it is a string or PropertySet/List,
235  yields itself.
236  """
237  if isinstance(a, (str, PropertyList, PropertySet)):
238  yield a
239  return
240  try:
241  yield from a
242  except Exception:
243  yield a
244 
245 
246 def _guessIntegerType(container, name, value):
247  """Given an existing container and name, determine the type
248  that should be used for the supplied value. The supplied value
249  is assumed to be a scalar.
250 
251  On Python 3 all ints are LongLong but we need to be able to store them
252  in Int containers if that is what is being used (testing for truncation).
253  Int is assumed to mean 32bit integer (2147483647 to -2147483648).
254 
255  If there is no pre-existing value we have to decide what to do. For now
256  we pick Int if the value is less than maxsize.
257 
258  Parameters
259  ----------
260  container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
261  Container from which to get the value
262 
263  name : `str`
264  Name of item
265 
266  value : `object`
267  Value to be assigned a type. Can be an iterable.
268 
269  Returns
270  -------
271  useType : `str` or none
272  Type to use for the supplied value. `None` if the input is
273  `bool` or a non-integral value.
274  """
275  maxInt = 2147483647
276  minInt = -2147483648
277  maxLongLong = 2**63 - 1
278  minLongLong = -2**63
279  maxU64 = 2**64 - 1
280  minU64 = 0
281 
282  # Go through the values to find the range of supplied integers,
283  # stopping early if we don't have an integer.
284  min = None
285  max = None
286  for v in _iterable(value):
287  # Do not try to convert a bool to an integer
288  if not isinstance(v, numbers.Integral) or isinstance(v, bool):
289  return None
290 
291  if min is None:
292  min = v
293  max = v
294  elif v < min:
295  min = v
296  elif v > max:
297  max = v
298 
299  # Safety net
300  if min is None or max is None:
301  raise RuntimeError(f"Internal logic failure calculating integer range of {value}")
302 
303  def _choose_int_from_range(int_value, current_type):
304  # If this is changing type from non-integer the current type
305  # does not matter.
306  if current_type not in {"Int", "LongLong", "UnsignedLongLong"}:
307  current_type = None
308 
309  if int_value <= maxInt and int_value >= minInt and current_type in (None, "Int"):
310  # Use Int only if in range and either no current type or the
311  # current type is an Int.
312  use_type = "Int"
313  elif int_value >= minLongLong and int_value < 0:
314  # All large negatives must be LongLong if they did not fit
315  # in Int clause above.
316  use_type = "LongLong"
317  elif int_value >= 0 and int_value <= maxLongLong and current_type in (None, "Int", "LongLong"):
318  # Larger than Int or already a LongLong
319  use_type = "LongLong"
320  elif int_value <= maxU64 and int_value >= minU64:
321  use_type = "UnsignedLongLong"
322  else:
323  raise RuntimeError("Unable to guess integer type for storing out of "
324  f"range value: {int_value}")
325  return use_type
326 
327  try:
328  containerType = _propertyContainerElementTypeName(container, name)
329  except LookupError:
330  containerType = None
331 
332  useTypeMin = _choose_int_from_range(min, containerType)
333  useTypeMax = _choose_int_from_range(max, containerType)
334 
335  if useTypeMin == useTypeMax:
336  return useTypeMin
337 
338  # When different the combinations are:
339  # Int + LongLong
340  # Int + UnsignedLongLong
341  # LongLong + UnsignedLongLong
342 
343  choices = {useTypeMin, useTypeMax}
344  if choices == {"Int", "LongLong"}:
345  return "LongLong"
346 
347  # If UnsignedLongLong is required things will break if the min
348  # is negative. They will break whatever we choose if that is the case
349  # but we have no choice but to return the UnsignedLongLong regardless.
350  if "UnsignedLongLong" in choices:
351  return "UnsignedLongLong"
352 
353  raise RuntimeError(f"Logic error in guessing integer type from {min} and {max}")
354 
355 
356 def _propertyContainerSet(container, name, value, typeMenu, *args):
357  """Set a single Python value of unknown type
358  """
359  try:
360  exemplar = next(_iterable(value))
361  except StopIteration:
362  # Do nothing if nothing provided. This matches the behavior
363  # of the explicit setX() methods.
364  return
365  t = type(exemplar)
366  setType = _guessIntegerType(container, name, value)
367 
368  if setType is not None or t in typeMenu:
369  if setType is None:
370  setType = typeMenu[t]
371  return getattr(container, "set" + setType)(name, value, *args)
372  # Allow for subclasses
373  for checkType in typeMenu:
374  if (checkType is None and exemplar is None) or \
375  (checkType is not None and isinstance(exemplar, checkType)):
376  return getattr(container, "set" + typeMenu[checkType])(name, value, *args)
377  raise TypeError("Unknown value type for key '%s': %s" % (name, t))
378 
379 
380 def _propertyContainerAdd(container, name, value, typeMenu, *args):
381  """Add a single Python value of unknown type
382  """
383  try:
384  exemplar = next(_iterable(value))
385  except StopIteration:
386  # Adding an empty iterable to an existing entry is a no-op
387  # since there is nothing to add.
388  return
389  t = type(exemplar)
390  addType = _guessIntegerType(container, name, exemplar)
391 
392  if addType is not None or t in typeMenu:
393  if addType is None:
394  addType = typeMenu[t]
395  return getattr(container, "add" + addType)(name, value, *args)
396  # Allow for subclasses
397  for checkType in typeMenu:
398  if (checkType is None and exemplar is None) or \
399  (checkType is not None and isinstance(exemplar, checkType)):
400  return getattr(container, "add" + typeMenu[checkType])(name, value, *args)
401  raise TypeError("Unknown value type for key '%s': %s" % (name, t))
402 
403 
404 def _makePropertySet(state):
405  """Make a `PropertySet` from the state returned by `getPropertySetState`
406 
407  Parameters
408  ----------
409  state : `list`
410  The data returned by `getPropertySetState`.
411  """
412  ps = PropertySet()
413  setPropertySetState(ps, state)
414  return ps
415 
416 
417 def _makePropertyList(state):
418  """Make a `PropertyList` from the state returned by
419  `getPropertyListState`
420 
421  Parameters
422  ----------
423  state : `list`
424  The data returned by `getPropertySetState`.
425  """
426  pl = PropertyList()
427  setPropertyListState(pl, state)
428  return pl
429 
430 
431 @continueClass
433  # Mapping of type to method names;
434  # int types are omitted due to use of _guessIntegerType
435  _typeMenu = {bool: "Bool",
436  float: "Double",
437  str: "String",
438  DateTime: "DateTime",
439  PropertySet: "PropertySet",
440  PropertyList: "PropertySet",
441  None: "Undef",
442  }
443 
444  def get(self, name, default=None):
445  """Return an item as a scalar, else default.
446 
447  Identical to `getScalar` except that a default value is returned
448  if the requested key is not present. If an array item is requested
449  the final value in the array will be returned.
450 
451  Parameters
452  ----------
453  name : `str`
454  Name of item
455  default : `object`, optional
456  Default value to use if the named item is not present.
457 
458  Returns
459  -------
460  value : any type supported by container
461  Single value of any type supported by the container, else the
462  default value if the requested item is not present in the
463  container. For array items the most recently added value is
464  returned.
465  """
466  try:
467  return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR)
468  except KeyError:
469  return default
470 
471  def getArray(self, name):
472  """Return an item as an array if the item is numeric or string
473 
474  If the item is a `PropertySet`, `PropertyList` or
475  `lsst.daf.base.PersistablePtr` then return the item as a scalar.
476 
477  Parameters
478  ----------
479  name : `str`
480  Name of item
481 
482  Returns
483  -------
484  values : `list` of any type supported by container
485  The contents of the item, guaranteed to be returned as a `list.`
486 
487  Raises
488  ------
489  KeyError
490  Raised if the item does not exist.
491  """
492  return _propertyContainerGet(self, name, returnStyle=ReturnStyle.ARRAY)
493 
494  def getScalar(self, name):
495  """Return an item as a scalar
496 
497  If the item has more than one value then the last value is returned.
498 
499  Parameters
500  ----------
501  name : `str`
502  Name of item
503 
504  Returns
505  -------
506  value : scalar item
507  Value stored in the item. If the item refers to an array the
508  most recently added value is returned.
509 
510  Raises
511  ------
512  KeyError
513  Raised if the item does not exist.
514  """
515  return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR)
516 
517  def set(self, name, value):
518  """Set the value of an item
519 
520  If the item already exists it is silently replaced; the types
521  need not match.
522 
523  Parameters
524  ----------
525  name : `str`
526  Name of item
527  value : any supported type
528  Value of item; may be a scalar or array
529  """
530  return _propertyContainerSet(self, name, value, self._typeMenu_typeMenu)
531 
532  def add(self, name, value):
533  """Append one or more values to a given item, which need not exist
534 
535  If the item exists then the new value(s) are appended;
536  otherwise it is like calling `set`
537 
538  Parameters
539  ----------
540  name : `str`
541  Name of item
542  value : any supported type
543  Value of item; may be a scalar or array
544 
545  Notes
546  -----
547  If ``value`` is an `lsst.daf.base.PropertySet` or
548  `lsst.daf.base.PropertyList` then ``value`` replaces
549  the existing value. Also the item is added as a live
550  reference, so updating ``value`` will update this container
551  and vice-versa.
552 
553  Raises
554  ------
555  lsst::pex::exceptions::TypeError
556  Raised if the type of `value` is incompatible with the existing
557  value of the item.
558  """
559  return _propertyContainerAdd(self, name, value, self._typeMenu_typeMenu)
560 
561  def update(self, addition):
562  """Update the current container with the supplied additions.
563 
564  Parameters
565  ----------
566  addition : `collections.abc.Mapping` or `PropertySet`
567  The content to merge into the current container.
568 
569  Notes
570  -----
571  This is not the same as calling `PropertySet.combine` since the
572  behavior differs when both mappings contain the same key. This
573  method updates by overwriting existing values completely with
574  the new value.
575  """
576  if isinstance(addition, PropertySet):
577  # To support array values we can not use the dict interface
578  # and instead use the copy() method which overwrites
579  for k in addition:
580  self.copy(k, addition, k)
581  else:
582  for k, v in addition.items():
583  self[k] = v
584 
585  def toDict(self):
586  """Returns a (possibly nested) dictionary with all properties.
587 
588  Returns
589  -------
590  d : `dict`
591  Dictionary with all names and values (no comments).
592  """
593 
594  d = {}
595  for name in self.names():
596  v = _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO)
597 
598  if isinstance(v, PropertySet):
599  d[name] = PropertySet.toDict(v)
600  else:
601  d[name] = v
602  return d
603 
604  def __eq__(self, other):
605  if type(self) != type(other):
606  return NotImplemented
607 
608  if len(self) != len(other):
609  return False
610 
611  for name in self:
612  if _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO) != \
613  _propertyContainerGet(other, name, returnStyle=ReturnStyle.AUTO):
614  return False
615  if self.typeOf(name) != other.typeOf(name):
616  return False
617 
618  return True
619 
620  def __copy__(self):
621  # Copy without having to go through pickle state
622  ps = PropertySet()
623  for itemName in self:
624  ps.copy(itemName, self, itemName)
625  return ps
626 
627  def __deepcopy__(self, memo):
628  result = self.deepCopy()
629  memo[id(self)] = result
630  return result
631 
632  def __contains__(self, name):
633  """Determines if the name is found at the top level hierarchy
634  of the container.
635 
636  Notes
637  ------
638  Does not use `PropertySet.exists()`` because that includes support
639  for "."-delimited names. This method is consistent with the
640  items returned from ``__iter__``.
641  """
642  return name in self.names(topLevelOnly=True)
643 
644  def __setitem__(self, name, value):
645  """Assigns the supplied value to the container.
646 
647  Parameters
648  ----------
649  name : `str`
650  Name of item to update.
651  value : Value to assign
652  Can be any value supported by the container's ``set()``
653  method. `~collections.abc.Mapping` are converted to
654  `PropertySet` before assignment.
655 
656  Notes
657  -----
658  Uses `PropertySet.set`, overwriting any previous value.
659  """
660  if isinstance(value, Mapping):
661  # Create a property set instead
662  ps = PropertySet()
663  for k, v in value.items():
664  ps[k] = v
665  value = ps
666  self.setset(name, value)
667 
668  def __getitem__(self, name):
669  """Returns a scalar item from the container.
670 
671  Notes
672  -----
673  Uses `PropertySet.getScalar` to guarantee that a single value
674  will be returned.
675  """
676  return self.getScalargetScalar(name)
677 
678  def __delitem__(self, name):
679  if name in self:
680  self.remove(name)
681  else:
682  raise KeyError(f"{name} not present in dict")
683 
684  def __str__(self):
685  return self.toString()
686 
687  def __len__(self):
688  return self.nameCount(topLevelOnly=True)
689 
690  def __iter__(self):
691  for n in self.names(topLevelOnly=True):
692  yield n
693 
694  def keys(self):
695  return KeysView(self)
696 
697  def items(self):
698  return ItemsView(self)
699 
700  def values(self):
701  return ValuesView(self)
702 
703  def __reduce__(self):
704  # It would be a bit simpler to use __setstate__ and __getstate__.
705  # However, implementing __setstate__ in Python causes segfaults
706  # because pickle creates a new instance by calling
707  # object.__new__(PropertyList, *args) which bypasses
708  # the pybind11 memory allocation step.
709  return (_makePropertySet, (getPropertySetState(self),))
710 
711 
712 @continueClass
714  # Mapping of type to method names
715  _typeMenu = {bool: "Bool",
716  int: "Int",
717  float: "Double",
718  str: "String",
719  DateTime: "DateTime",
720  PropertySet: "PropertySet",
721  PropertyList: "PropertySet",
722  None: "Undef",
723  }
724 
725  COMMENTSUFFIX = "#COMMENT"
726  """Special suffix used to indicate that a named item being assigned
727  using dict syntax is referring to a comment, not value."""
728 
729  def get(self, name, default=None):
730  """Return an item as a scalar, else default.
731 
732  Identical to `getScalar` except that a default value is returned
733  if the requested key is not present. If an array item is requested
734  the final value in the array will be returned.
735 
736  Parameters
737  ----------
738  name : ``str``
739  Name of item
740  default : `object`, optional
741  Default value to use if the named item is not present.
742 
743  Returns
744  -------
745  value : any type supported by container
746  Single value of any type supported by the container, else the
747  default value if the requested item is not present in the
748  container. For array items the most recently added value is
749  returned.
750  """
751  try:
752  return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR)
753  except KeyError:
754  return default
755 
756  def getArray(self, name):
757  """Return an item as a list.
758 
759  Parameters
760  ----------
761  name : `str`
762  Name of item
763 
764  Returns
765  -------
766  values : `list` of values
767  The contents of the item, guaranteed to be returned as a `list.`
768 
769  Raises
770  ------
771  KeyError
772  Raised if the item does not exist.
773  """
774  return _propertyContainerGet(self, name, returnStyle=ReturnStyle.ARRAY)
775 
776  def getScalar(self, name):
777  """Return an item as a scalar
778 
779  If the item has more than one value then the last value is returned.
780 
781  Parameters
782  ----------
783  name : `str`
784  Name of item.
785 
786  Returns
787  -------
788  value : scalar item
789  Value stored in the item. If the item refers to an array the
790  most recently added value is returned.
791 
792  Raises
793  ------
794  KeyError
795  Raised if the item does not exist.
796  """
797  return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR)
798 
799  def set(self, name, value, comment=None):
800  """Set the value of an item
801 
802  If the item already exists it is silently replaced; the types
803  need not match.
804 
805  Parameters
806  ----------
807  name : `str`
808  Name of item
809  value : any supported type
810  Value of item; may be a scalar or array
811  """
812  args = []
813  if comment is not None:
814  args.append(comment)
815  return _propertyContainerSet(self, name, value, self._typeMenu_typeMenu, *args)
816 
817  def add(self, name, value, comment=None):
818  """Append one or more values to a given item, which need not exist
819 
820  If the item exists then the new value(s) are appended;
821  otherwise it is like calling `set`
822 
823  Parameters
824  ----------
825  name : `str`
826  Name of item
827  value : any supported type
828  Value of item; may be a scalar or array
829 
830  Notes
831  -----
832  If `value` is an `lsst.daf.base.PropertySet` items are added
833  using dotted names (e.g. if name="a" and value contains
834  an item "b" which is another PropertySet and contains an
835  item "c" which is numeric or string, then the value of "c"
836  is added as "a.b.c", appended to the existing values of
837  "a.b.c" if any (in which case the types must be compatible).
838 
839  Raises
840  ------
841  lsst::pex::exceptions::TypeError
842  Raise if the type of ``value`` is incompatible with the existing
843  value of the item.
844  """
845  args = []
846  if comment is not None:
847  args.append(comment)
848  return _propertyContainerAdd(self, name, value, self._typeMenu_typeMenu, *args)
849 
850  def setComment(self, name, comment):
851  """Set the comment for an existing entry.
852 
853  Parameters
854  ----------
855  name : `str`
856  Name of the key to receive updated comment.
857  comment : `comment`
858  New comment string.
859  """
860  # The only way to do this is to replace the existing entry with
861  # one that has the new comment
862  containerType = _propertyContainerElementTypeName(self, name)
863  if self.isArray(name):
864  value = self.getArraygetArray(name)
865  else:
866  value = self.getScalargetScalar(name)
867  getattr(self, f"set{containerType}")(name, value, comment)
868 
869  def toList(self):
870  """Return a list of tuples of name, value, comment for each property
871  in the order that they were inserted.
872 
873  Returns
874  -------
875  ret : `list` of `tuple`
876  Tuples of name, value, comment for each property in the order
877  in which they were inserted.
878  """
879  orderedNames = self.getOrderedNames()
880  ret = []
881  for name in orderedNames:
882  if self.isArray(name):
883  values = _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO)
884  for v in values:
885  ret.append((name, v, self.getComment(name)))
886  else:
887  ret.append((name, _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO),
888  self.getComment(name)))
889  return ret
890 
891  def toOrderedDict(self):
892  """Return an ordered dictionary with all properties in the order that
893  they were inserted.
894 
895  Returns
896  -------
897  d : `dict`
898  Ordered dictionary with all properties in the order that they
899  were inserted. Comments are not included.
900 
901  Notes
902  -----
903  As of Python 3.6 dicts retain their insertion order.
904  """
905  d = {}
906  for name in self:
907  d[name] = _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO)
908  return d
909 
910  # For PropertyList the two are equivalent
911  toDict = toOrderedDict
912 
913  def __eq__(self, other):
914  # super() doesn't seem to work properly in @continueClass;
915  # note that super with arguments seems to work at first, but actually
916  # doesn't either.
917  if not PropertySet.__eq__(self, other):
918  return False
919 
920  for name in self:
921  if self.getComment(name) != other.getComment(name):
922  return False
923 
924  return True
925 
926  def __copy__(self):
927  # Copy without having to go through pickle state
928  pl = PropertyList()
929  for itemName in self:
930  pl.copy(itemName, self, itemName)
931  return pl
932 
933  def __deepcopy__(self, memo):
934  result = self.deepCopy()
935  memo[id(self)] = result
936  return result
937 
938  def __iter__(self):
939  for n in self.getOrderedNames():
940  yield n
941 
942  def __setitem__(self, name, value):
943  """Assigns the supplied value to the container.
944 
945  Parameters
946  ----------
947  name : `str`
948  Name of item to update. If the name ends with
949  `PropertyList.COMMENTSUFFIX`, the comment is updated rather
950  than the value.
951  value : Value to assign
952  Can be any value supported by the container's ``set()``
953  method. `~collections.abc.Mapping` are converted to
954  `PropertySet` before assignment.
955 
956  Notes
957  -----
958  Uses `PropertySet.set`, overwriting any previous value.
959  """
960  if name.endswith(self.COMMENTSUFFIXCOMMENTSUFFIX):
961  name = name[:-len(self.COMMENTSUFFIXCOMMENTSUFFIX)]
962  self.setCommentsetComment(name, value)
963  return
964  if isinstance(value, Mapping):
965  # Create a property set instead
966  ps = PropertySet()
967  for k, v in value.items():
968  ps[k] = v
969  value = ps
970  self.setset(name, value)
971 
972  def __reduce__(self):
973  # It would be a bit simpler to use __setstate__ and __getstate__.
974  # However, implementing __setstate__ in Python causes segfaults
975  # because pickle creates a new instance by calling
976  # object.__new__(PropertyList, *args) which bypasses
977  # the pybind11 memory allocation step.
978  return (_makePropertyList, (getPropertyListState(self),))