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