Coverage for python/lsst/daf/base/propertyContainer/propertyContainerContinued.py: 93%
398 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-29 01:45 -0700
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-29 01:45 -0700
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#
24__all__ = ["getPropertySetState", "getPropertyListState", "setPropertySetState", "setPropertyListState"]
26import enum
27import math
28import numbers
29import warnings
30import dataclasses
31from collections.abc import Mapping, KeysView, ValuesView, ItemsView
32from typing import TypeAlias, Union
34import astropy.io.fits
35import astropy.io.fits.verify
37# Ensure that C++ exceptions are properly translated to Python
38import lsst.pex.exceptions # noqa: F401
39from lsst.utils import continueClass
41from .._dafBaseLib import PropertySet, PropertyList
42from ..dateTime import DateTime
45# Note that '|' syntax for unions doesn't work when we have to use a string
46# literal (and we do since it's recursive and not an annotation).
47NestedMetadataDict: TypeAlias = Mapping[str, Union[str, float, int, bool, "NestedMetadataDict"]]
50# Map the type names to the internal type representation.
51_TYPE_MAP = {}
52for checkType in ("Bool", "Short", "Int", "Long", "LongLong", "UnsignedLongLong",
53 "Float", "Double", "String", "DateTime",
54 "PropertySet", "Undef"):
55 type_obj = getattr(PropertySet, "TYPE_" + checkType)
56 _TYPE_MAP[type_obj] = checkType
57 # Store both directions.
58 _TYPE_MAP[checkType] = type_obj
60# FITS headers cannot represent non-finite floats, so afw's FITS writer stores
61# them as these strings and its reader converts them back to floats.
62_NON_FINITE_FLOAT_STRINGS = {"NAN": math.nan, "+INFINITY": math.inf, "-INFINITY": -math.inf}
64# Keywords whose cards hold free text rather than a value.
65_COMMENTARY_KEYWORDS = frozenset({"COMMENT", "HISTORY"})
68def _nonFiniteFloatToString(value):
69 """Return the afw FITS string representation of a non-finite float,
70 or `None` if the value is finite.
71 """
72 if math.isfinite(value):
73 return None
74 if math.isnan(value):
75 return "NAN"
76 return "-INFINITY" if value < 0 else "+INFINITY"
79def getPropertySetState(container, asLists=False):
80 """Get the state of a PropertySet in a form that can be pickled.
82 Parameters
83 ----------
84 container : `PropertySet`
85 The property container.
86 asLists : `bool`, optional
87 If False, the default, `tuple` will be used for the contents. If true
88 a `list` will be used.
90 Returns
91 -------
92 state : `list` of `tuple` or `list` of `list`
93 The state, as a list of tuples (or lists), each of which contains
94 the following 3 items:
96 name (a `str`)
97 the name of the item
98 elementTypeName (a `str`)
99 the suffix of a ``setX`` method name
100 which is appropriate for the data type. For example integer
101 data has ``elementTypeName="Int"` which corresponds to
102 the ``setInt`` method.
103 value
104 the data for the item, in a form compatible
105 with the set method named by ``elementTypeName``
106 """
107 names = container.names(topLevelOnly=True)
108 sequence = list if asLists else tuple
109 return [sequence((name, _propertyContainerElementTypeName(container, name),
110 _propertyContainerGet(container, name, returnStyle=ReturnStyle.AUTO)))
111 for name in names]
114def getPropertyListState(container, asLists=False):
115 """Get the state of a PropertyList in a form that can be pickled.
117 Parameters
118 ----------
119 container : `PropertyList`
120 The property container.
121 asLists : `bool`, optional
122 If False, the default, `tuple` will be used for the contents. If true
123 a `list` will be used.
125 Returns
126 -------
127 state : `list` of `tuple` or `list` of `list`
128 The state, as a list of tuples (or lists), each of which contains
129 the following 4 items:
131 name (a `str`):
132 the name of the item
133 elementTypeName (a `str`):
134 the suffix of a ``setX`` method name
135 which is appropriate for the data type. For example integer
136 data has ``elementTypeName="Int"` which corresponds to
137 the ``setInt`` method.
138 value
139 the data for the item, in a form compatible
140 with the set method named by ``elementTypeName``
141 comment (a `str`): the comment. This item is only present
142 if ``container`` is a PropertyList.
143 """
144 sequence = list if asLists else tuple
145 return [sequence((name, _propertyContainerElementTypeName(container, name),
146 _propertyContainerGet(container, name, returnStyle=ReturnStyle.AUTO),
147 container.getComment(name)))
148 for name in container.getOrderedNames()]
151def setPropertySetState(container, state):
152 """Restore the state of a PropertySet, in place.
154 Parameters
155 ----------
156 container : `PropertySet`
157 The property container whose state is to be restored.
158 It should be empty to start with and is updated in place.
159 state : `list`
160 The state, as returned by `getPropertySetState`
161 """
162 for name, elemType, value in state:
163 if elemType is not None: 163 ↛ 166line 163 didn't jump to line 166 because the condition on line 163 was always true
164 getattr(container, "set" + elemType)(name, value)
165 else:
166 raise ValueError(f"Unrecognized values for state restoration: ({name}, {elemType}, {value})")
169def setPropertyListState(container, state):
170 """Restore the state of a PropertyList, in place.
172 Parameters
173 ----------
174 container : `PropertyList`
175 The property container whose state is to be restored.
176 It should be empty to start with and is updated in place.
177 state : `list`
178 The state, as returned by ``getPropertyListState``
179 """
180 for name, elemType, value, comment in state:
181 getattr(container, "set" + elemType)(name, value, comment)
184class ReturnStyle(enum.Enum):
185 ARRAY = enum.auto()
186 SCALAR = enum.auto()
187 AUTO = enum.auto()
190def _propertyContainerElementTypeName(container, name):
191 """Return name of the type of a particular element
193 Parameters
194 ----------
195 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
196 Container including the element
197 name : `str`
198 Name of element
199 """
200 try:
201 t = container.typeOf(name)
202 except LookupError as e:
203 # KeyError is more commonly expected when asking for an element
204 # from a mapping.
205 raise KeyError(str(e)) from None
207 return _TYPE_MAP.get(t, None)
210def _propertyContainerGet(container, name, returnStyle):
211 """Get a value of unknown type as a scalar or array
213 Parameters
214 ----------
215 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
216 Container from which to get the value
217 name : `str`
218 Name of item
219 returnStyle : `ReturnStyle`
220 Control whether numeric or string data is returned as an array
221 or scalar (the other types, ``PropertyList``, ``PropertySet``
222 and ``PersistablePtr``, are always returned as a scalar):
223 - ReturnStyle.ARRAY: return numeric or string data types
224 as an array of values.
225 - ReturnStyle.SCALAR: return numeric or string data types
226 as a single value; if the item has multiple values then
227 return the last value.
228 - ReturnStyle.AUTO: (deprecated) return numeric or string data
229 as a scalar if there is just one item, or as an array
230 otherwise.
232 Raises
233 ------
234 KeyError
235 Raised if the specified key does not exist in the container.
236 TypeError
237 Raised if the value retrieved is of an unexpected type.
238 ValueError
239 Raised if the value for ``returnStyle`` is not correct.
240 """
241 if not container.exists(name):
242 raise KeyError(name + " not found")
243 if returnStyle not in ReturnStyle: 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true
244 raise ValueError("returnStyle {} must be a ReturnStyle".format(returnStyle))
246 elemType = _propertyContainerElementTypeName(container, name)
247 if elemType and elemType != "PropertySet":
248 value = getattr(container, "getArray" + elemType)(name)
249 if returnStyle == ReturnStyle.ARRAY or (returnStyle == ReturnStyle.AUTO and len(value) > 1):
250 return value
251 return value[-1]
253 if container.isPropertySetPtr(name): 253 ↛ 258line 253 didn't jump to line 258 because the condition on line 253 was always true
254 try:
255 return container.getAsPropertyListPtr(name)
256 except Exception:
257 return container.getAsPropertySetPtr(name)
258 try:
259 return container.getAsPersistablePtr(name)
260 except Exception:
261 pass
262 raise TypeError('Unknown PropertySet value type for ' + name)
265def _iterable(a):
266 """Make input iterable.
268 Takes whatever is given to it and yields it back one element at a time.
269 If it is not an iterable or it is a string or PropertySet/List,
270 yields itself.
271 """
272 if isinstance(a, (str, PropertyList, PropertySet)):
273 yield a
274 return
275 try:
276 yield from a
277 except Exception:
278 yield a
281def _guessIntegerType(container, name, value):
282 """Given an existing container and name, determine the type
283 that should be used for the supplied value. The supplied value
284 is assumed to be a scalar.
286 On Python 3 all ints are LongLong but we need to be able to store them
287 in Int containers if that is what is being used (testing for truncation).
288 Int is assumed to mean 32bit integer (2147483647 to -2147483648).
290 If there is no pre-existing value we have to decide what to do. For now
291 we pick Int if the value is less than maxsize.
293 Parameters
294 ----------
295 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
296 Container from which to get the value
298 name : `str`
299 Name of item
301 value : `object`
302 Value to be assigned a type. Can be an iterable.
304 Returns
305 -------
306 useType : `str` or none
307 Type to use for the supplied value. `None` if the input is
308 `bool` or a non-integral value.
309 """
310 maxInt = 2147483647
311 minInt = -2147483648
312 maxLongLong = 2**63 - 1
313 minLongLong = -2**63
314 maxU64 = 2**64 - 1
315 minU64 = 0
317 # Go through the values to find the range of supplied integers,
318 # stopping early if we don't have an integer.
319 min = None
320 max = None
321 for v in _iterable(value):
322 # Do not try to convert a bool to an integer
323 if not isinstance(v, numbers.Integral) or isinstance(v, bool):
324 return None
326 if min is None:
327 min = v
328 max = v
329 elif v < min:
330 min = v
331 elif v > max:
332 max = v
334 # Safety net
335 if min is None or max is None: 335 ↛ 336line 335 didn't jump to line 336 because the condition on line 335 was never true
336 raise RuntimeError(f"Internal logic failure calculating integer range of {value}")
338 def _choose_int_from_range(int_value, current_type):
339 # If this is changing type from non-integer the current type
340 # does not matter.
341 if current_type not in {"Int", "LongLong", "UnsignedLongLong"}:
342 current_type = None
344 if int_value <= maxInt and int_value >= minInt and current_type in (None, "Int"):
345 # Use Int only if in range and either no current type or the
346 # current type is an Int.
347 use_type = "Int"
348 elif int_value >= minLongLong and int_value < 0:
349 # All large negatives must be LongLong if they did not fit
350 # in Int clause above.
351 use_type = "LongLong"
352 elif int_value >= 0 and int_value <= maxLongLong and current_type in (None, "Int", "LongLong"):
353 # Larger than Int or already a LongLong
354 use_type = "LongLong"
355 elif int_value <= maxU64 and int_value >= minU64:
356 use_type = "UnsignedLongLong"
357 else:
358 raise RuntimeError("Unable to guess integer type for storing out of "
359 f"range value: {int_value}")
360 return use_type
362 if container.exists(name):
363 containerType = _propertyContainerElementTypeName(container, name)
364 else:
365 containerType = None
367 useTypeMin = _choose_int_from_range(min, containerType)
368 useTypeMax = _choose_int_from_range(max, containerType)
370 if useTypeMin == useTypeMax:
371 return useTypeMin
373 # When different the combinations are:
374 # Int + LongLong
375 # Int + UnsignedLongLong
376 # LongLong + UnsignedLongLong
378 choices = {useTypeMin, useTypeMax}
379 if choices == {"Int", "LongLong"}:
380 return "LongLong"
382 # If UnsignedLongLong is required things will break if the min
383 # is negative. They will break whatever we choose if that is the case
384 # but we have no choice but to return the UnsignedLongLong regardless.
385 if "UnsignedLongLong" in choices: 385 ↛ 388line 385 didn't jump to line 388 because the condition on line 385 was always true
386 return "UnsignedLongLong"
388 raise RuntimeError(f"Logic error in guessing integer type from {min} and {max}")
391def _propertyContainerSet(container, name, value, typeMenu, *args):
392 """Set a single Python value of unknown type
393 """
394 try:
395 exemplar = next(_iterable(value))
396 except StopIteration:
397 # Do nothing if nothing provided. This matches the behavior
398 # of the explicit setX() methods.
399 return
400 t = type(exemplar)
401 setType = _guessIntegerType(container, name, value)
403 if setType is not None or t in typeMenu:
404 if setType is None:
405 setType = typeMenu[t]
406 return getattr(container, "set" + setType)(name, value, *args)
407 # Allow for subclasses
408 for checkType in typeMenu: 408 ↛ 412line 408 didn't jump to line 412 because the loop on line 408 didn't complete
409 if (checkType is None and exemplar is None) or \
410 (checkType is not None and isinstance(exemplar, checkType)):
411 return getattr(container, "set" + typeMenu[checkType])(name, value, *args)
412 raise TypeError("Unknown value type for key '%s': %s" % (name, t))
415def _propertyContainerAdd(container, name, value, typeMenu, *args):
416 """Add a single Python value of unknown type
417 """
418 try:
419 exemplar = next(_iterable(value))
420 except StopIteration:
421 # Adding an empty iterable to an existing entry is a no-op
422 # since there is nothing to add.
423 return
424 t = type(exemplar)
425 addType = _guessIntegerType(container, name, exemplar)
427 if addType is not None or t in typeMenu:
428 if addType is None:
429 addType = typeMenu[t]
430 return getattr(container, "add" + addType)(name, value, *args)
431 # Allow for subclasses
432 for checkType in typeMenu: 432 ↛ 436line 432 didn't jump to line 436 because the loop on line 432 didn't complete
433 if (checkType is None and exemplar is None) or \
434 (checkType is not None and isinstance(exemplar, checkType)):
435 return getattr(container, "add" + typeMenu[checkType])(name, value, *args)
436 raise TypeError("Unknown value type for key '%s': %s" % (name, t))
439def _makePropertySet(state):
440 """Make a `PropertySet` from the state returned by `getPropertySetState`
442 Parameters
443 ----------
444 state : `list`
445 The data returned by `getPropertySetState`.
446 """
447 ps = PropertySet()
448 setPropertySetState(ps, state)
449 return ps
452def _makePropertyList(state):
453 """Make a `PropertyList` from the state returned by
454 `getPropertyListState`
456 Parameters
457 ----------
458 state : `list`
459 The data returned by `getPropertySetState`.
460 """
461 pl = PropertyList()
462 setPropertyListState(pl, state)
463 return pl
466@continueClass
467class PropertySet:
468 # Mapping of type to method names;
469 # int types are omitted due to use of _guessIntegerType
470 _typeMenu = {bool: "Bool",
471 float: "Double",
472 str: "String",
473 DateTime: "DateTime",
474 PropertySet: "PropertySet",
475 PropertyList: "PropertySet",
476 None: "Undef",
477 }
479 @classmethod
480 def from_mapping(cls, metadata):
481 """Create a `PropertySet` from a mapping or dict-like object.
483 Parameters
484 ----------
485 metadata : `collections.abc.Mapping`
486 Metadata from which to create the `PropertySet`.
487 Can be a mapping, a `~dataclasses.dataclass` or anything that
488 supports ``toDict()``, ``to_dict()`` or ``dict()`` method.
489 It is assumed that the dictionary is expanded recursively by these
490 methods or that the Python type can be understood by `PropertySet`.
492 Returns
493 -------
494 ps : `PropertySet`
495 The new `PropertySet`.
496 """
497 ps = cls()
498 d = None
499 if isinstance(metadata, Mapping):
500 d = metadata
501 elif dataclasses.is_dataclass(metadata):
502 d = dataclasses.asdict(metadata)
503 else:
504 for attr in ("to_dict", "toDict", "dict"):
505 if hasattr(metadata, attr):
506 d = getattr(metadata, attr)()
507 break
508 if d is None:
509 raise ValueError("Unable to extract mappings from the supplied metadata of type"
510 f" {type(metadata)}")
511 ps.update(d)
512 return ps
514 def get(self, name, default=None):
515 """Return an item as a scalar, else default.
517 Identical to `getScalar` except that a default value is returned
518 if the requested key is not present. If an array item is requested
519 the final value in the array will be returned.
521 Parameters
522 ----------
523 name : `str`
524 Name of item
525 default : `object`, optional
526 Default value to use if the named item is not present.
528 Returns
529 -------
530 value : any type supported by container
531 Single value of any type supported by the container, else the
532 default value if the requested item is not present in the
533 container. For array items the most recently added value is
534 returned.
535 """
536 try:
537 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR)
538 except KeyError:
539 return default
541 def getArray(self, name):
542 """Return an item as an array if the item is numeric or string
544 If the item is a `PropertySet`, `PropertyList` or
545 `lsst.daf.base.PersistablePtr` then return the item as a scalar.
547 Parameters
548 ----------
549 name : `str`
550 Name of item
552 Returns
553 -------
554 values : `list` of any type supported by container
555 The contents of the item, guaranteed to be returned as a `list.`
557 Raises
558 ------
559 KeyError
560 Raised if the item does not exist.
561 """
562 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.ARRAY)
564 def getScalar(self, name):
565 """Return an item as a scalar
567 If the item has more than one value then the last value is returned.
569 Parameters
570 ----------
571 name : `str`
572 Name of item
574 Returns
575 -------
576 value : scalar item
577 Value stored in the item. If the item refers to an array the
578 most recently added value is returned.
580 Raises
581 ------
582 KeyError
583 Raised if the item does not exist.
584 """
585 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR)
587 def set(self, name, value):
588 """Set the value of an item
590 If the item already exists it is silently replaced; the types
591 need not match.
593 Parameters
594 ----------
595 name : `str`
596 Name of item
597 value : any supported type
598 Value of item; may be a scalar or array
599 """
600 return _propertyContainerSet(self, name, value, self._typeMenu)
602 def add(self, name, value):
603 """Append one or more values to a given item, which need not exist
605 If the item exists then the new value(s) are appended;
606 otherwise it is like calling `set`
608 Parameters
609 ----------
610 name : `str`
611 Name of item
612 value : any supported type
613 Value of item; may be a scalar or array
615 Notes
616 -----
617 If ``value`` is an `lsst.daf.base.PropertySet` or
618 `lsst.daf.base.PropertyList` then ``value`` replaces
619 the existing value. Also the item is added as a live
620 reference, so updating ``value`` will update this container
621 and vice-versa.
623 Raises
624 ------
625 lsst::pex::exceptions::TypeError
626 Raised if the type of `value` is incompatible with the existing
627 value of the item.
628 """
629 return _propertyContainerAdd(self, name, value, self._typeMenu)
631 def update(self, addition):
632 """Update the current container with the supplied additions.
634 Parameters
635 ----------
636 addition : `collections.abc.Mapping` or `PropertySet`
637 The content to merge into the current container.
639 Notes
640 -----
641 This is not the same as calling `PropertySet.combine` since the
642 behavior differs when both mappings contain the same key. This
643 method updates by overwriting existing values completely with
644 the new value.
645 """
646 if isinstance(addition, PropertySet):
647 # To support array values we can not use the dict interface
648 # and instead use the copy() method which overwrites
649 for k in addition:
650 self.copy(k, addition, k)
651 else:
652 for k, v in addition.items():
653 self[k] = v
655 def toDict(self):
656 """Returns a (possibly nested) dictionary with all properties.
658 Returns
659 -------
660 d : `dict`
661 Dictionary with all names and values (no comments).
662 """
664 d = {}
665 for name in self.names():
666 v = _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO)
668 if isinstance(v, PropertySet):
669 d[name] = PropertySet.toDict(v)
670 else:
671 d[name] = v
672 return d
674 def __eq__(self, other):
675 if type(self) is not type(other): 675 ↛ 676line 675 didn't jump to line 676 because the condition on line 675 was never true
676 return NotImplemented
678 if len(self) != len(other): 678 ↛ 679line 678 didn't jump to line 679 because the condition on line 678 was never true
679 return False
681 for name in self:
682 if (self_typeOf := self.typeOf(name)) != other.typeOf(name): 682 ↛ 683line 682 didn't jump to line 683 because the condition on line 682 was never true
683 return False
685 if (v1 := _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO)) != \
686 (v2 := _propertyContainerGet(other, name, returnStyle=ReturnStyle.AUTO)):
687 # It is possible that we have floats that are NaN. When
688 # equating two PropertySets if there are fields with NaN
689 # these should equate equal.
690 if self_typeOf in (_TYPE_MAP["Float"], _TYPE_MAP["Double"]) \ 690 ↛ 692line 690 didn't jump to line 692 because the condition on line 690 was never true
691 and math.isnan(v1) and math.isnan(v2):
692 pass
693 else:
694 return False
696 return True
698 def __copy__(self):
699 # Copy without having to go through pickle state
700 ps = PropertySet()
701 for itemName in self:
702 ps.copy(itemName, self, itemName)
703 return ps
705 def __deepcopy__(self, memo):
706 result = self.deepCopy()
707 memo[id(self)] = result
708 return result
710 def __contains__(self, name):
711 """Determines if the name is found at the top level hierarchy
712 of the container.
714 Notes
715 ------
716 Does not use `PropertySet.exists()`` because that includes support
717 for "."-delimited names. This method is consistent with the
718 items returned from ``__iter__``.
719 """
720 return name in self.names(topLevelOnly=True)
722 def __setitem__(self, name, value):
723 """Assigns the supplied value to the container.
725 Parameters
726 ----------
727 name : `str`
728 Name of item to update.
729 value : Value to assign
730 Can be any value supported by the container's ``set()``
731 method. `~collections.abc.Mapping` are converted to
732 `PropertySet` before assignment.
734 Notes
735 -----
736 Uses `PropertySet.set`, overwriting any previous value.
737 """
738 if isinstance(value, Mapping):
739 # Create a property set instead
740 ps = PropertySet()
741 for k, v in value.items():
742 ps[k] = v
743 value = ps
744 self.set(name, value)
746 def __getitem__(self, name):
747 """Returns a scalar item from the container.
749 Notes
750 -----
751 Uses `PropertySet.getScalar` to guarantee that a single value
752 will be returned.
753 """
754 return self.getScalar(name)
756 def __delitem__(self, name):
757 if self.exists(name): 757 ↛ 761line 757 didn't jump to line 761 because the condition on line 757 was always true
758 # dot-delimited names should work so cannot use "in".
759 self.remove(name)
760 else:
761 raise KeyError(f"{name} not present in dict")
763 def __str__(self):
764 return self.toString()
766 def __len__(self):
767 return self.nameCount(topLevelOnly=True)
769 def __iter__(self):
770 for n in self.names(topLevelOnly=True):
771 yield n
773 def keys(self):
774 return KeysView(self)
776 def items(self):
777 return ItemsView(self)
779 def values(self):
780 return ValuesView(self)
782 def pop(self, name, default=None):
783 """Remove the named key and return its value.
785 Parameters
786 ----------
787 name : `str`
788 Name of the key to remove. Can be hierarchical.
789 default : Any, optional
790 Value to return if the key is not present.
792 Returns
793 -------
794 value : Any
795 The value of the item as would be returned using `getScalar()`.
797 Raises
798 ------
799 KeyError
800 Raised if no default is given and the key is missing.
801 """
802 if self.exists(name):
803 value = self[name]
804 self.remove(name)
805 else:
806 if default is None:
807 raise KeyError(name)
808 value = default
809 return value
811 def __reduce__(self):
812 # It would be a bit simpler to use __setstate__ and __getstate__.
813 # However, implementing __setstate__ in Python causes segfaults
814 # because pickle creates a new instance by calling
815 # object.__new__(PropertyList, *args) which bypasses
816 # the pybind11 memory allocation step.
817 return (_makePropertySet, (getPropertySetState(self),))
819 def get_dict(self, key: str) -> NestedMetadataDict:
820 """Return a possibly-hierarchical nested `dict`.
822 This implements the `lsst.pipe.base.GetDictMetadata` protocol for
823 consistency with `lsst.pipe.base.TaskMetadata` and `PropertyList`.
825 Parameters
826 ----------
827 key : `str`
828 String key associated with the mapping. May not have a ``.``
829 character.
831 Returns
832 -------
833 value : `~collections.abc.Mapping`
834 Possibly-nested mapping, with `str` keys and values that are `int`,
835 `float`, `str`, `bool`, or another `dict` with the same key and
836 value types. Will be empty if ``key`` does not exist.
838 Raises
839 ------
840 TypeError
841 Raised if the value associated with this key is not a nested
842 dictionary, but does exist. Note that this behavior is not
843 consistent with `PropertyList` (which returns an empty `dict`).
844 """
845 if self.exists(key):
846 return self.getScalar(key).toDict()
847 else:
848 return {}
850 def set_dict(self, key: str, value: NestedMetadataDict) -> None:
851 """Assign a possibly-hierarchical nested `dict`.
853 This implements the `lsst.pipe.base.SetDictMetadata` protocol for
854 consistency with `lsst.pipe.base.TaskMetadata` and `PropertyList`.
856 Parameters
857 ----------
858 key : `str`
859 String key associated with the mapping. May not have a ``.``
860 character.
861 value : `~collections.abc.Mapping`
862 Possibly-nested mapping, with `str` keys and values that are `int`,
863 `float`, `str`, `bool`, or another `dict` with the same key and
864 value types. May not have a ``.``
865 character.
866 """
867 self.set(key, PropertySet.from_mapping(value))
870@continueClass
871class PropertyList:
872 # Mapping of type to method names
873 _typeMenu = {bool: "Bool",
874 int: "Int",
875 float: "Double",
876 str: "String",
877 DateTime: "DateTime",
878 PropertySet: "PropertySet",
879 PropertyList: "PropertySet",
880 None: "Undef",
881 }
883 COMMENTSUFFIX = "#COMMENT"
884 """Special suffix used to indicate that a named item being assigned
885 using dict syntax is referring to a comment, not value."""
887 def get(self, name, default=None):
888 """Return an item as a scalar, else default.
890 Identical to `getScalar` except that a default value is returned
891 if the requested key is not present. If an array item is requested
892 the final value in the array will be returned.
894 Parameters
895 ----------
896 name : ``str``
897 Name of item
898 default : `object`, optional
899 Default value to use if the named item is not present.
901 Returns
902 -------
903 value : any type supported by container
904 Single value of any type supported by the container, else the
905 default value if the requested item is not present in the
906 container. For array items the most recently added value is
907 returned.
908 """
909 try:
910 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR)
911 except KeyError:
912 return default
914 def getArray(self, name):
915 """Return an item as a list.
917 Parameters
918 ----------
919 name : `str`
920 Name of item
922 Returns
923 -------
924 values : `list` of values
925 The contents of the item, guaranteed to be returned as a `list.`
927 Raises
928 ------
929 KeyError
930 Raised if the item does not exist.
931 """
932 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.ARRAY)
934 def getScalar(self, name):
935 """Return an item as a scalar
937 If the item has more than one value then the last value is returned.
939 Parameters
940 ----------
941 name : `str`
942 Name of item.
944 Returns
945 -------
946 value : scalar item
947 Value stored in the item. If the item refers to an array the
948 most recently added value is returned.
950 Raises
951 ------
952 KeyError
953 Raised if the item does not exist.
954 """
955 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR)
957 def set(self, name, value, comment=None):
958 """Set the value of an item
960 If the item already exists it is silently replaced; the types
961 need not match.
963 Parameters
964 ----------
965 name : `str`
966 Name of item
967 value : any supported type
968 Value of item; may be a scalar or array
969 """
970 args = []
971 if comment is not None:
972 args.append(comment)
973 return _propertyContainerSet(self, name, value, self._typeMenu, *args)
975 def add(self, name, value, comment=None):
976 """Append one or more values to a given item, which need not exist
978 If the item exists then the new value(s) are appended;
979 otherwise it is like calling `set`
981 Parameters
982 ----------
983 name : `str`
984 Name of item
985 value : any supported type
986 Value of item; may be a scalar or array
988 Notes
989 -----
990 If `value` is an `lsst.daf.base.PropertySet` items are added
991 using dotted names (e.g. if name="a" and value contains
992 an item "b" which is another PropertySet and contains an
993 item "c" which is numeric or string, then the value of "c"
994 is added as "a.b.c", appended to the existing values of
995 "a.b.c" if any (in which case the types must be compatible).
997 Raises
998 ------
999 lsst::pex::exceptions::TypeError
1000 Raise if the type of ``value`` is incompatible with the existing
1001 value of the item.
1002 """
1003 args = []
1004 if comment is not None:
1005 args.append(comment)
1006 return _propertyContainerAdd(self, name, value, self._typeMenu, *args)
1008 def setComment(self, name, comment):
1009 """Set the comment for an existing entry.
1011 Parameters
1012 ----------
1013 name : `str`
1014 Name of the key to receive updated comment.
1015 comment : `comment`
1016 New comment string.
1017 """
1018 # The only way to do this is to replace the existing entry with
1019 # one that has the new comment
1020 containerType = _propertyContainerElementTypeName(self, name)
1021 if self.isArray(name): 1021 ↛ 1022line 1021 didn't jump to line 1022 because the condition on line 1021 was never true
1022 value = self.getArray(name)
1023 else:
1024 value = self.getScalar(name)
1025 getattr(self, f"set{containerType}")(name, value, comment)
1027 @classmethod
1028 def from_astropy_header(cls, header: astropy.io.fits.Header) -> "PropertyList":
1029 """Convert an `astropy.io.fits.Header` into a `PropertyList`.
1031 Parameters
1032 ----------
1033 header : `astropy.io.fits.Header`
1034 FITS header to convert.
1036 Returns
1037 -------
1038 propertyList : `PropertyList`
1039 Equivalent legacy metadata. Blank (keyword-less) cards and
1040 cards whose values Astropy cannot parse (e.g. from unterminated
1041 quoted strings in real raw headers) are skipped. Cards with
1042 `astropy.io.fits.card.Undefined` values (e.g. ``SEEING =``) are
1043 stored as undefined properties (``TYPE_Undef``), preserving any
1044 comment text. COMMENT and HISTORY cards hold their text as
1045 string values, as they do both in FITS convention and when read
1046 back by `lsst.afw.fits.readMetadata`. The string values
1047 ``NAN``, ``+INFINITY`` and ``-INFINITY`` are converted to the
1048 corresponding non-finite floats, matching
1049 `lsst.afw.fits.readMetadata`.
1051 Notes
1052 -----
1053 Comments on cards are preserved via `PropertyList.add`, and so are
1054 keyed by name: cards that repeat a keyword share that keyword's
1055 comment slot.
1056 """
1057 result = cls()
1058 for card in header.cards:
1059 if not card.keyword: 1059 ↛ 1061line 1059 didn't jump to line 1061 because the condition on line 1059 was never true
1060 # Truly blank cards have no name to key on.
1061 continue
1062 try:
1063 value = card.value
1064 except astropy.io.fits.verify.VerifyError:
1065 # Values astropy cannot parse (e.g. from unterminated quoted
1066 # strings) have no representation in legacy metadata.
1067 continue
1068 comment = card.comment or None
1069 if isinstance(value, astropy.io.fits.card.Undefined):
1070 value = None
1071 elif isinstance(value, str) and card.keyword not in _COMMENTARY_KEYWORDS:
1072 value = _NON_FINITE_FLOAT_STRINGS.get(value, value)
1073 result.add(card.keyword, value, comment)
1074 return result
1076 def to_astropy_header(self) -> astropy.io.fits.Header:
1077 """Convert this `PropertyList` into an `astropy.io.fits.Header`.
1079 Returns
1080 -------
1081 header : `astropy.io.fits.Header`
1082 FITS header with one card per stored value, in insertion order.
1083 Undefined properties become cards with no value
1084 (`astropy.io.fits.card.Undefined`); string values stored under
1085 the COMMENT and HISTORY keywords render as bare comment cards
1086 (``COMMENT text``) rather than ``COMMENT = 'text'``. Comments
1087 set via `PropertyList.add` or `setComment` are restored.
1088 Non-finite floats, which FITS headers cannot represent, are
1089 stored as the strings ``NAN``, ``+INFINITY`` and ``-INFINITY``,
1090 matching the afw FITS writer.
1091 """
1092 header = astropy.io.fits.Header()
1093 with warnings.catch_warnings():
1094 # Silence warnings about long keys becoming HIERARCH and
1095 # comments being truncated; both are unavoidable here.
1096 warnings.simplefilter("ignore", category=astropy.io.fits.verify.VerifyWarning)
1097 for name in self.getOrderedNames():
1098 values = self.getArray(name)
1099 comment = self.getComment(name) or None
1100 for value in values:
1101 if isinstance(value, float) and (nonFinite := _nonFiniteFloatToString(value)):
1102 value = nonFinite
1103 header.append(astropy.io.fits.Card(name, value, comment), end=True)
1104 return header
1106 def toList(self):
1107 """Return a list of tuples of name, value, comment for each property
1108 in the order that they were inserted.
1110 Returns
1111 -------
1112 ret : `list` of `tuple`
1113 Tuples of name, value, comment for each property in the order
1114 in which they were inserted.
1115 """
1116 orderedNames = self.getOrderedNames()
1117 ret = []
1118 for name in orderedNames:
1119 if self.isArray(name):
1120 values = _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO)
1121 for v in values:
1122 ret.append((name, v, self.getComment(name)))
1123 else:
1124 ret.append((name, _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO),
1125 self.getComment(name)))
1126 return ret
1128 def toOrderedDict(self):
1129 """Return an ordered dictionary with all properties in the order that
1130 they were inserted.
1132 Returns
1133 -------
1134 d : `dict`
1135 Ordered dictionary with all properties in the order that they
1136 were inserted. Comments are not included.
1138 Notes
1139 -----
1140 As of Python 3.6 dicts retain their insertion order.
1141 """
1142 d = {}
1143 for name in self:
1144 d[name] = _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO)
1145 return d
1147 # For PropertyList the two are equivalent
1148 toDict = toOrderedDict
1150 def __eq__(self, other):
1151 # super() doesn't seem to work properly in @continueClass;
1152 # note that super with arguments seems to work at first, but actually
1153 # doesn't either.
1154 if not PropertySet.__eq__(self, other): 1154 ↛ 1155line 1154 didn't jump to line 1155 because the condition on line 1154 was never true
1155 return False
1157 for name in self:
1158 if self.getComment(name) != other.getComment(name): 1158 ↛ 1159line 1158 didn't jump to line 1159 because the condition on line 1158 was never true
1159 return False
1161 return True
1163 def __copy__(self):
1164 # Copy without having to go through pickle state
1165 pl = PropertyList()
1166 for itemName in self:
1167 pl.copy(itemName, self, itemName)
1168 return pl
1170 def __deepcopy__(self, memo):
1171 result = self.deepCopy()
1172 memo[id(self)] = result
1173 return result
1175 def __iter__(self):
1176 for n in self.getOrderedNames():
1177 yield n
1179 def __setitem__(self, name, value):
1180 """Assigns the supplied value to the container.
1182 Parameters
1183 ----------
1184 name : `str`
1185 Name of item to update. If the name ends with
1186 `PropertyList.COMMENTSUFFIX`, the comment is updated rather
1187 than the value.
1188 value : Value to assign
1189 Can be any value supported by the container's ``set()``
1190 method. `~collections.abc.Mapping` are converted to
1191 `PropertySet` before assignment.
1193 Notes
1194 -----
1195 Uses `PropertySet.set`, overwriting any previous value.
1196 """
1197 if name.endswith(self.COMMENTSUFFIX):
1198 name = name[:-len(self.COMMENTSUFFIX)]
1199 self.setComment(name, value)
1200 return
1201 if isinstance(value, Mapping):
1202 # Create a property set instead
1203 ps = PropertySet()
1204 for k, v in value.items():
1205 ps[k] = v
1206 value = ps
1207 self.set(name, value)
1209 def __reduce__(self):
1210 # It would be a bit simpler to use __setstate__ and __getstate__.
1211 # However, implementing __setstate__ in Python causes segfaults
1212 # because pickle creates a new instance by calling
1213 # object.__new__(PropertyList, *args) which bypasses
1214 # the pybind11 memory allocation step.
1215 return (_makePropertyList, (getPropertyListState(self),))
1217 def get_dict(self, key: str) -> NestedMetadataDict:
1218 """Return a possibly-hierarchical nested `dict`.
1220 This implements the `lsst.pipe.base.GetDictMetadata` protocol for
1221 consistency with `lsst.pipe.base.TaskMetadata` and `PropertySet`.
1223 Parameters
1224 ----------
1225 key : `str`
1226 String key associated with the mapping. May not have a ``.``
1227 character.
1229 Returns
1230 -------
1231 value : `~collections.abc.Mapping`
1232 Possibly-nested mapping, with `str` keys and values that are `int`,
1233 `float`, `str`, `bool`, or another `dict` with the same key and
1234 value types. Will be empty if ``key`` does not exist.
1235 """
1236 result: NestedMetadataDict = {}
1237 name: str
1238 for name in self.getOrderedNames():
1239 levels = name.split(".")
1240 if levels[0] == key:
1241 nested = result
1242 for level_key in levels[1:-1]:
1243 nested = result.setdefault(level_key, {})
1244 nested[levels[-1]] = self[name]
1245 return result
1247 def set_dict(self, key: str, value: NestedMetadataDict) -> None:
1248 """Assign a possibly-hierarchical nested `dict`.
1250 This implements the `lsst.pipe.base.SetDictMetadata` protocol for
1251 consistency with `lsst.pipe.base.TaskMetadata` and `PropertySet`.
1253 Parameters
1254 ----------
1255 key : `str`
1256 String key associated with the mapping. May not have a ``.``
1257 character.
1258 value : `~collections.abc.Mapping`
1259 Possibly-nested mapping, with `str` keys and values that are `int`,
1260 `float`, `str`, `bool`, or another `dict` with the same key and
1261 value types. Nested keys may not have a ``.`` character.
1262 """
1263 self.set(key, PropertySet.from_mapping(value))