24__all__ = [
"getPropertySetState",
"getPropertyListState",
"setPropertySetState",
"setPropertyListState"]
31from collections.abc
import Mapping, KeysView, ValuesView, ItemsView
32from typing
import TypeAlias, Union
35import astropy.io.fits.verify
39from lsst.utils
import continueClass
41from .._dafBaseLib
import PropertySet, PropertyList
42from ..dateTime
import DateTime
47NestedMetadataDict: TypeAlias = Mapping[str, Union[str, float, int, bool,
"NestedMetadataDict"]]
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
58 _TYPE_MAP[checkType] = type_obj
62 """Get the state of a PropertySet in a form that can be pickled.
66 container : `PropertySet`
67 The property container.
68 asLists : `bool`, optional
69 If False, the default, `tuple` will be used for the contents. If true
70 a `list` will be used.
74 state : `list` of `tuple` or `list` of `list`
75 The state, as a list of tuples (or lists), each of which contains
76 the following 3 items:
80 elementTypeName (a `str`)
81 the suffix of a ``setX`` method name
82 which is appropriate for the data type. For example integer
83 data has ``elementTypeName="Int"` which corresponds to
84 the ``setInt`` method.
86 the data for the item, in a form compatible
87 with the set method named by ``elementTypeName``
89 names = container.names(topLevelOnly=
True)
90 sequence = list
if asLists
else tuple
97 """Get the state of a PropertyList in a form that can be pickled.
101 container : `PropertyList`
102 The property container.
103 asLists : `bool`, optional
104 If False, the default, `tuple` will be used for the contents. If true
105 a `list` will be used.
109 state : `list` of `tuple` or `list` of `list`
110 The state, as a list of tuples (or lists), each of which contains
111 the following 4 items:
115 elementTypeName (a `str`):
116 the suffix of a ``setX`` method name
117 which is appropriate for the data type. For example integer
118 data has ``elementTypeName="Int"` which corresponds to
119 the ``setInt`` method.
121 the data for the item, in a form compatible
122 with the set method named by ``elementTypeName``
123 comment (a `str`): the comment. This item is only present
124 if ``container`` is a PropertyList.
126 sequence = list
if asLists
else tuple
129 container.getComment(name)))
130 for name
in container.getOrderedNames()]
134 """Restore the state of a PropertySet, in place.
138 container : `PropertySet`
139 The property container whose state is to be restored.
140 It should be empty to start with and is updated in place.
142 The state, as returned by `getPropertySetState`
144 for name, elemType, value
in state:
145 if elemType
is not None:
146 getattr(container,
"set" + elemType)(name, value)
148 raise ValueError(f
"Unrecognized values for state restoration: ({name}, {elemType}, {value})")
152 """Restore the state of a PropertyList, in place.
156 container : `PropertyList`
157 The property container whose state is to be restored.
158 It should be empty to start with and is updated in place.
160 The state, as returned by ``getPropertyListState``
162 for name, elemType, value, comment
in state:
163 getattr(container,
"set" + elemType)(name, value, comment)
173 """Return name of the type of a particular element
177 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
178 Container including the element
183 t = container.typeOf(name)
184 except LookupError
as e:
187 raise KeyError(str(e))
from None
189 return _TYPE_MAP.get(t,
None)
193 """Get a value of unknown type as a scalar or array
197 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
198 Container from which to get the value
201 returnStyle : `ReturnStyle`
202 Control whether numeric or string data is returned as an array
203 or scalar (the other types, ``PropertyList``, ``PropertySet``
204 and ``PersistablePtr``, are always returned as a scalar):
205 - ReturnStyle.ARRAY: return numeric or string data types
206 as an array of values.
207 - ReturnStyle.SCALAR: return numeric or string data types
208 as a single value; if the item has multiple values then
209 return the last value.
210 - ReturnStyle.AUTO: (deprecated) return numeric or string data
211 as a scalar if there is just one item, or as an array
217 Raised if the specified key does not exist in the container.
219 Raised if the value retrieved is of an unexpected type.
221 Raised if the value for ``returnStyle`` is not correct.
223 if not container.exists(name):
224 raise KeyError(name +
" not found")
225 if returnStyle
not in ReturnStyle:
226 raise ValueError(
"returnStyle {} must be a ReturnStyle".format(returnStyle))
229 if elemType
and elemType !=
"PropertySet":
230 value = getattr(container,
"getArray" + elemType)(name)
231 if returnStyle == ReturnStyle.ARRAY
or (returnStyle == ReturnStyle.AUTO
and len(value) > 1):
235 if container.isPropertySetPtr(name):
237 return container.getAsPropertyListPtr(name)
239 return container.getAsPropertySetPtr(name)
241 return container.getAsPersistablePtr(name)
244 raise TypeError(
'Unknown PropertySet value type for ' + name)
248 """Make input iterable.
250 Takes whatever is given to it and yields it back one element at a time.
251 If it is not an iterable or it is a string or PropertySet/List,
254 if isinstance(a, (str, PropertyList, PropertySet)):
264 """Given an existing container and name, determine the type
265 that should be used for the supplied value. The supplied value
266 is assumed to be a scalar.
268 On Python 3 all ints are LongLong but we need to be able to store them
269 in Int containers if that is what is being used (testing for truncation).
270 Int is assumed to mean 32bit integer (2147483647 to -2147483648).
272 If there is no pre-existing value we have to decide what to do. For now
273 we pick Int if the value is less than maxsize.
277 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
278 Container from which to get the value
284 Value to be assigned a type. Can be an iterable.
288 useType : `str` or none
289 Type to use for the supplied value. `None` if the input is
290 `bool` or a non-integral value.
294 maxLongLong = 2**63 - 1
305 if not isinstance(v, numbers.Integral)
or isinstance(v, bool):
317 if min
is None or max
is None:
318 raise RuntimeError(f
"Internal logic failure calculating integer range of {value}")
320 def _choose_int_from_range(int_value, current_type):
323 if current_type
not in {
"Int",
"LongLong",
"UnsignedLongLong"}:
326 if int_value <= maxInt
and int_value >= minInt
and current_type
in (
None,
"Int"):
330 elif int_value >= minLongLong
and int_value < 0:
333 use_type =
"LongLong"
334 elif int_value >= 0
and int_value <= maxLongLong
and current_type
in (
None,
"Int",
"LongLong"):
336 use_type =
"LongLong"
337 elif int_value <= maxU64
and int_value >= minU64:
338 use_type =
"UnsignedLongLong"
340 raise RuntimeError(
"Unable to guess integer type for storing out of "
341 f
"range value: {int_value}")
344 if container.exists(name):
349 useTypeMin = _choose_int_from_range(min, containerType)
350 useTypeMax = _choose_int_from_range(max, containerType)
352 if useTypeMin == useTypeMax:
360 choices = {useTypeMin, useTypeMax}
361 if choices == {
"Int",
"LongLong"}:
367 if "UnsignedLongLong" in choices:
368 return "UnsignedLongLong"
370 raise RuntimeError(f
"Logic error in guessing integer type from {min} and {max}")
374 """Set a single Python value of unknown type
378 except StopIteration:
385 if setType
is not None or t
in typeMenu:
387 setType = typeMenu[t]
388 return getattr(container,
"set" + setType)(name, value, *args)
390 for checkType
in typeMenu:
391 if (checkType
is None and exemplar
is None)
or \
392 (checkType
is not None and isinstance(exemplar, checkType)):
393 return getattr(container,
"set" + typeMenu[checkType])(name, value, *args)
394 raise TypeError(
"Unknown value type for key '%s': %s" % (name, t))
398 """Add a single Python value of unknown type
402 except StopIteration:
409 if addType
is not None or t
in typeMenu:
411 addType = typeMenu[t]
412 return getattr(container,
"add" + addType)(name, value, *args)
414 for checkType
in typeMenu:
415 if (checkType
is None and exemplar
is None)
or \
416 (checkType
is not None and isinstance(exemplar, checkType)):
417 return getattr(container,
"add" + typeMenu[checkType])(name, value, *args)
418 raise TypeError(
"Unknown value type for key '%s': %s" % (name, t))
422 """Make a `PropertySet` from the state returned by `getPropertySetState`
427 The data returned by `getPropertySetState`.
435 """Make a `PropertyList` from the state returned by
436 `getPropertyListState`
441 The data returned by `getPropertySetState`.
452 _typeMenu = {bool:
"Bool",
455 DateTime:
"DateTime",
456 PropertySet:
"PropertySet",
457 PropertyList:
"PropertySet",
463 """Create a `PropertySet` from a mapping or dict-like object.
467 metadata : `collections.abc.Mapping`
468 Metadata from which to create the `PropertySet`.
469 Can be a mapping, a `~dataclasses.dataclass` or anything that
470 supports ``toDict()``, ``to_dict()`` or ``dict()`` method.
471 It is assumed that the dictionary is expanded recursively by these
472 methods or that the Python type can be understood by `PropertySet`.
477 The new `PropertySet`.
481 if isinstance(metadata, Mapping):
483 elif dataclasses.is_dataclass(metadata):
484 d = dataclasses.asdict(metadata)
486 for attr
in (
"to_dict",
"toDict",
"dict"):
487 if hasattr(metadata, attr):
488 d = getattr(metadata, attr)()
491 raise ValueError(
"Unable to extract mappings from the supplied metadata of type"
492 f
" {type(metadata)}")
496 def get(self, name, default=None):
497 """Return an item as a scalar, else default.
499 Identical to `getScalar` except that a default value is returned
500 if the requested key is not present. If an array item is requested
501 the final value in the array will be returned.
507 default : `object`, optional
508 Default value to use if the named item is not present.
512 value : any type supported by container
513 Single value of any type supported by the container, else the
514 default value if the requested item is not present in the
515 container. For array items the most recently added value is
524 """Return an item as an array if the item is numeric or string
526 If the item is a `PropertySet`, `PropertyList` or
527 `lsst.daf.base.PersistablePtr` then return the item as a scalar.
536 values : `list` of any type supported by container
537 The contents of the item, guaranteed to be returned as a `list.`
542 Raised if the item does not exist.
547 """Return an item as a scalar
549 If the item has more than one value then the last value is returned.
559 Value stored in the item. If the item refers to an array the
560 most recently added value is returned.
565 Raised if the item does not exist.
569 def set(self, name, value):
570 """Set the value of an item
572 If the item already exists it is silently replaced; the types
579 value : any supported type
580 Value of item; may be a scalar or array
584 def add(self, name, value):
585 """Append one or more values to a given item, which need not exist
587 If the item exists then the new value(s) are appended;
588 otherwise it is like calling `set`
594 value : any supported type
595 Value of item; may be a scalar or array
599 If ``value`` is an `lsst.daf.base.PropertySet` or
600 `lsst.daf.base.PropertyList` then ``value`` replaces
601 the existing value. Also the item is added as a live
602 reference, so updating ``value`` will update this container
607 lsst::pex::exceptions::TypeError
608 Raised if the type of `value` is incompatible with the existing
614 """Update the current container with the supplied additions.
618 addition : `collections.abc.Mapping` or `PropertySet`
619 The content to merge into the current container.
623 This is not the same as calling `PropertySet.combine` since the
624 behavior differs when both mappings contain the same key. This
625 method updates by overwriting existing values completely with
628 if isinstance(addition, PropertySet):
632 self.copy(k, addition, k)
634 for k, v
in addition.items():
638 """Returns a (possibly nested) dictionary with all properties.
643 Dictionary with all names and values (no comments).
647 for name
in self.names():
650 if isinstance(v, PropertySet):
651 d[name] = PropertySet.toDict(v)
657 if type(self)
is not type(other):
658 return NotImplemented
660 if len(self) != len(other):
664 if (self_typeOf := self.typeOf(name)) != other.typeOf(name):
672 if self_typeOf
in (_TYPE_MAP[
"Float"], _TYPE_MAP[
"Double"]) \
673 and math.isnan(v1)
and math.isnan(v2):
683 for itemName
in self:
684 ps.copy(itemName, self, itemName)
688 result = self.deepCopy()
689 memo[id(self)] = result
693 """Determines if the name is found at the top level hierarchy
698 Does not use `PropertySet.exists()`` because that includes support
699 for "."-delimited names. This method is consistent with the
700 items returned from ``__iter__``.
702 return name
in self.names(topLevelOnly=
True)
705 """Assigns the supplied value to the container.
710 Name of item to update.
711 value : Value to assign
712 Can be any value supported by the container's ``set()``
713 method. `~collections.abc.Mapping` are converted to
714 `PropertySet` before assignment.
718 Uses `PropertySet.set`, overwriting any previous value.
720 if isinstance(value, Mapping):
723 for k, v
in value.items():
726 self.
set(name, value)
729 """Returns a scalar item from the container.
733 Uses `PropertySet.getScalar` to guarantee that a single value
739 if self.exists(name):
743 raise KeyError(f
"{name} not present in dict")
746 return self.toString()
749 return self.nameCount(topLevelOnly=
True)
752 for n
in self.names(topLevelOnly=
True):
756 return KeysView(self)
759 return ItemsView(self)
762 return ValuesView(self)
764 def pop(self, name, default=None):
765 """Remove the named key and return its value.
770 Name of the key to remove. Can be hierarchical.
771 default : Any, optional
772 Value to return if the key is not present.
777 The value of the item as would be returned using `getScalar()`.
782 Raised if no default is given and the key is missing.
784 if self.exists(name):
801 def get_dict(self, key: str) -> NestedMetadataDict:
802 """Return a possibly-hierarchical nested `dict`.
804 This implements the `lsst.pipe.base.GetDictMetadata` protocol for
805 consistency with `lsst.pipe.base.TaskMetadata` and `PropertyList`.
810 String key associated with the mapping. May not have a ``.``
815 value : `~collections.abc.Mapping`
816 Possibly-nested mapping, with `str` keys and values that are `int`,
817 `float`, `str`, `bool`, or another `dict` with the same key and
818 value types. Will be empty if ``key`` does not exist.
823 Raised if the value associated with this key is not a nested
824 dictionary, but does exist. Note that this behavior is not
825 consistent with `PropertyList` (which returns an empty `dict`).
832 def set_dict(self, key: str, value: NestedMetadataDict) ->
None:
833 """Assign a possibly-hierarchical nested `dict`.
835 This implements the `lsst.pipe.base.SetDictMetadata` protocol for
836 consistency with `lsst.pipe.base.TaskMetadata` and `PropertyList`.
841 String key associated with the mapping. May not have a ``.``
843 value : `~collections.abc.Mapping`
844 Possibly-nested mapping, with `str` keys and values that are `int`,
845 `float`, `str`, `bool`, or another `dict` with the same key and
846 value types. May not have a ``.``
849 self.
set(key, PropertySet.from_mapping(value))
855 _typeMenu = {bool:
"Bool",
859 DateTime:
"DateTime",
860 PropertySet:
"PropertySet",
861 PropertyList:
"PropertySet",
865 COMMENTSUFFIX =
"#COMMENT"
866 """Special suffix used to indicate that a named item being assigned
867 using dict syntax is referring to a comment, not value."""
869 def get(self, name, default=None):
870 """Return an item as a scalar, else default.
872 Identical to `getScalar` except that a default value is returned
873 if the requested key is not present. If an array item is requested
874 the final value in the array will be returned.
880 default : `object`, optional
881 Default value to use if the named item is not present.
885 value : any type supported by container
886 Single value of any type supported by the container, else the
887 default value if the requested item is not present in the
888 container. For array items the most recently added value is
897 """Return an item as a list.
906 values : `list` of values
907 The contents of the item, guaranteed to be returned as a `list.`
912 Raised if the item does not exist.
917 """Return an item as a scalar
919 If the item has more than one value then the last value is returned.
929 Value stored in the item. If the item refers to an array the
930 most recently added value is returned.
935 Raised if the item does not exist.
939 def set(self, name, value, comment=None):
940 """Set the value of an item
942 If the item already exists it is silently replaced; the types
949 value : any supported type
950 Value of item; may be a scalar or array
953 if comment
is not None:
957 def add(self, name, value, comment=None):
958 """Append one or more values to a given item, which need not exist
960 If the item exists then the new value(s) are appended;
961 otherwise it is like calling `set`
967 value : any supported type
968 Value of item; may be a scalar or array
972 If `value` is an `lsst.daf.base.PropertySet` items are added
973 using dotted names (e.g. if name="a" and value contains
974 an item "b" which is another PropertySet and contains an
975 item "c" which is numeric or string, then the value of "c"
976 is added as "a.b.c", appended to the existing values of
977 "a.b.c" if any (in which case the types must be compatible).
981 lsst::pex::exceptions::TypeError
982 Raise if the type of ``value`` is incompatible with the existing
986 if comment
is not None:
991 """Set the comment for an existing entry.
996 Name of the key to receive updated comment.
1003 if self.isArray(name):
1007 getattr(self, f
"set{containerType}")(name, value, comment)
1011 """Convert an `astropy.io.fits.Header` into a `PropertyList`.
1015 header : `astropy.io.fits.Header`
1016 FITS header to convert.
1020 propertyList : `PropertyList`
1021 Equivalent legacy metadata. Blank (keyword-less) cards and
1022 cards whose values Astropy cannot parse (e.g. from unterminated
1023 quoted strings in real raw headers) are skipped. Cards with
1024 `astropy.io.fits.card.Undefined` values (e.g. ``SEEING =``) are
1025 stored as undefined properties (``TYPE_Undef``), preserving any
1026 comment text. COMMENT and HISTORY cards hold their text as
1027 string values, as they do both in FITS convention and when read
1028 back by `lsst.afw.fits.readMetadata`.
1032 Comments on cards are preserved via `PropertyList.add`, and so are
1033 keyed by name: cards that repeat a keyword share that keyword's
1037 for card
in header.cards:
1038 if not card.keyword:
1043 except astropy.io.fits.verify.VerifyError:
1047 comment = card.comment
or None
1048 if isinstance(value, astropy.io.fits.card.Undefined):
1050 result.add(card.keyword, value, comment)
1054 """Convert this `PropertyList` into an `astropy.io.fits.Header`.
1058 header : `astropy.io.fits.Header`
1059 FITS header with one card per stored value, in insertion order.
1060 Undefined properties become cards with no value
1061 (`astropy.io.fits.card.Undefined`); string values stored under
1062 the COMMENT and HISTORY keywords render as bare comment cards
1063 (``COMMENT text``) rather than ``COMMENT = 'text'``. Comments
1064 set via `PropertyList.add` or `setComment` are restored.
1066 header = astropy.io.fits.Header()
1067 with warnings.catch_warnings():
1070 warnings.simplefilter(
"ignore", category=astropy.io.fits.verify.VerifyWarning)
1071 for name
in self.getOrderedNames():
1073 comment = self.getComment(name)
or None
1074 for value
in values:
1075 header.append(astropy.io.fits.Card(name, value, comment), end=
True)
1079 """Return a list of tuples of name, value, comment for each property
1080 in the order that they were inserted.
1084 ret : `list` of `tuple`
1085 Tuples of name, value, comment for each property in the order
1086 in which they were inserted.
1088 orderedNames = self.getOrderedNames()
1090 for name
in orderedNames:
1091 if self.isArray(name):
1094 ret.append((name, v, self.getComment(name)))
1097 self.getComment(name)))
1101 """Return an ordered dictionary with all properties in the order that
1107 Ordered dictionary with all properties in the order that they
1108 were inserted. Comments are not included.
1112 As of Python 3.6 dicts retain their insertion order.
1120 toDict = toOrderedDict
1126 if not PropertySet.__eq__(self, other):
1130 if self.getComment(name) != other.getComment(name):
1138 for itemName
in self:
1139 pl.copy(itemName, self, itemName)
1143 result = self.deepCopy()
1144 memo[id(self)] = result
1148 for n
in self.getOrderedNames():
1152 """Assigns the supplied value to the container.
1157 Name of item to update. If the name ends with
1158 `PropertyList.COMMENTSUFFIX`, the comment is updated rather
1160 value : Value to assign
1161 Can be any value supported by the container's ``set()``
1162 method. `~collections.abc.Mapping` are converted to
1163 `PropertySet` before assignment.
1167 Uses `PropertySet.set`, overwriting any previous value.
1173 if isinstance(value, Mapping):
1176 for k, v
in value.items():
1179 self.
set(name, value)
1190 """Return a possibly-hierarchical nested `dict`.
1192 This implements the `lsst.pipe.base.GetDictMetadata` protocol for
1193 consistency with `lsst.pipe.base.TaskMetadata` and `PropertySet`.
1198 String key associated with the mapping. May not have a ``.``
1203 value : `~collections.abc.Mapping`
1204 Possibly-nested mapping, with `str` keys and values that are `int`,
1205 `float`, `str`, `bool`, or another `dict` with the same key and
1206 value types. Will be empty if ``key`` does not exist.
1208 result: NestedMetadataDict = {}
1210 for name
in self.getOrderedNames():
1211 levels = name.split(
".")
1212 if levels[0] == key:
1214 for level_key
in levels[1:-1]:
1215 nested = result.setdefault(level_key, {})
1216 nested[levels[-1]] = self[name]
1219 def set_dict(self, key: str, value: NestedMetadataDict) ->
None:
1220 """Assign a possibly-hierarchical nested `dict`.
1222 This implements the `lsst.pipe.base.SetDictMetadata` protocol for
1223 consistency with `lsst.pipe.base.TaskMetadata` and `PropertySet`.
1228 String key associated with the mapping. May not have a ``.``
1230 value : `~collections.abc.Mapping`
1231 Possibly-nested mapping, with `str` keys and values that are `int`,
1232 `float`, `str`, `bool`, or another `dict` with the same key and
1233 value types. Nested keys may not have a ``.`` character.
1235 self.
set(key, PropertySet.from_mapping(value))
set(self, name, value, comment=None)
astropy.io.fits.Header to_astropy_header(self)
setComment(self, name, comment)
add(self, name, value, comment=None)
NestedMetadataDict get_dict(self, str key)
None set_dict(self, str key, NestedMetadataDict value)
"PropertyList" from_astropy_header(cls, astropy.io.fits.Header header)
get(self, name, default=None)
__setitem__(self, name, value)
pop(self, name, default=None)
None set_dict(self, str key, NestedMetadataDict value)
NestedMetadataDict get_dict(self, str key)
from_mapping(cls, metadata)
__setitem__(self, name, value)
get(self, name, default=None)
_propertyContainerAdd(container, name, value, typeMenu, *args)
setPropertySetState(container, state)
_propertyContainerSet(container, name, value, typeMenu, *args)
getPropertyListState(container, asLists=False)
setPropertyListState(container, state)
getPropertySetState(container, asLists=False)
_propertyContainerElementTypeName(container, name)
_propertyContainerGet(container, name, returnStyle)
_guessIntegerType(container, name, value)