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_NON_FINITE_FLOAT_STRINGS = {
"NAN": math.nan,
"+INFINITY": math.inf,
"-INFINITY": -math.inf}
65_COMMENTARY_KEYWORDS = frozenset({
"COMMENT",
"HISTORY"})
69 """Return the afw FITS string representation of a non-finite float,
70 or `None` if the value is finite.
72 if math.isfinite(value):
76 return "-INFINITY" if value < 0
else "+INFINITY"
80 """Get the state of a PropertySet in a form that can be pickled.
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.
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:
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.
104 the data for the item, in a form compatible
105 with the set method named by ``elementTypeName``
107 names = container.names(topLevelOnly=
True)
108 sequence = list
if asLists
else tuple
115 """Get the state of a PropertyList in a form that can be pickled.
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.
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:
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.
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.
144 sequence = list
if asLists
else tuple
147 container.getComment(name)))
148 for name
in container.getOrderedNames()]
152 """Restore the state of a PropertySet, in place.
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.
160 The state, as returned by `getPropertySetState`
162 for name, elemType, value
in state:
163 if elemType
is not None:
164 getattr(container,
"set" + elemType)(name, value)
166 raise ValueError(f
"Unrecognized values for state restoration: ({name}, {elemType}, {value})")
170 """Restore the state of a PropertyList, in place.
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.
178 The state, as returned by ``getPropertyListState``
180 for name, elemType, value, comment
in state:
181 getattr(container,
"set" + elemType)(name, value, comment)
191 """Return name of the type of a particular element
195 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
196 Container including the element
201 t = container.typeOf(name)
202 except LookupError
as e:
205 raise KeyError(str(e))
from None
207 return _TYPE_MAP.get(t,
None)
211 """Get a value of unknown type as a scalar or array
215 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
216 Container from which to get the value
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
235 Raised if the specified key does not exist in the container.
237 Raised if the value retrieved is of an unexpected type.
239 Raised if the value for ``returnStyle`` is not correct.
241 if not container.exists(name):
242 raise KeyError(name +
" not found")
243 if returnStyle
not in ReturnStyle:
244 raise ValueError(
"returnStyle {} must be a ReturnStyle".format(returnStyle))
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):
253 if container.isPropertySetPtr(name):
255 return container.getAsPropertyListPtr(name)
257 return container.getAsPropertySetPtr(name)
259 return container.getAsPersistablePtr(name)
262 raise TypeError(
'Unknown PropertySet value type for ' + name)
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,
272 if isinstance(a, (str, PropertyList, PropertySet)):
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.
295 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList`
296 Container from which to get the value
302 Value to be assigned a type. Can be an iterable.
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.
312 maxLongLong = 2**63 - 1
323 if not isinstance(v, numbers.Integral)
or isinstance(v, bool):
335 if min
is None or max
is None:
336 raise RuntimeError(f
"Internal logic failure calculating integer range of {value}")
338 def _choose_int_from_range(int_value, current_type):
341 if current_type
not in {
"Int",
"LongLong",
"UnsignedLongLong"}:
344 if int_value <= maxInt
and int_value >= minInt
and current_type
in (
None,
"Int"):
348 elif int_value >= minLongLong
and int_value < 0:
351 use_type =
"LongLong"
352 elif int_value >= 0
and int_value <= maxLongLong
and current_type
in (
None,
"Int",
"LongLong"):
354 use_type =
"LongLong"
355 elif int_value <= maxU64
and int_value >= minU64:
356 use_type =
"UnsignedLongLong"
358 raise RuntimeError(
"Unable to guess integer type for storing out of "
359 f
"range value: {int_value}")
362 if container.exists(name):
367 useTypeMin = _choose_int_from_range(min, containerType)
368 useTypeMax = _choose_int_from_range(max, containerType)
370 if useTypeMin == useTypeMax:
378 choices = {useTypeMin, useTypeMax}
379 if choices == {
"Int",
"LongLong"}:
385 if "UnsignedLongLong" in choices:
386 return "UnsignedLongLong"
388 raise RuntimeError(f
"Logic error in guessing integer type from {min} and {max}")
392 """Set a single Python value of unknown type
396 except StopIteration:
403 if setType
is not None or t
in typeMenu:
405 setType = typeMenu[t]
406 return getattr(container,
"set" + setType)(name, value, *args)
408 for checkType
in typeMenu:
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))
416 """Add a single Python value of unknown type
420 except StopIteration:
427 if addType
is not None or t
in typeMenu:
429 addType = typeMenu[t]
430 return getattr(container,
"add" + addType)(name, value, *args)
432 for checkType
in typeMenu:
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))
440 """Make a `PropertySet` from the state returned by `getPropertySetState`
445 The data returned by `getPropertySetState`.
453 """Make a `PropertyList` from the state returned by
454 `getPropertyListState`
459 The data returned by `getPropertySetState`.
470 _typeMenu = {bool:
"Bool",
473 DateTime:
"DateTime",
474 PropertySet:
"PropertySet",
475 PropertyList:
"PropertySet",
481 """Create a `PropertySet` from a mapping or dict-like object.
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`.
495 The new `PropertySet`.
499 if isinstance(metadata, Mapping):
501 elif dataclasses.is_dataclass(metadata):
502 d = dataclasses.asdict(metadata)
504 for attr
in (
"to_dict",
"toDict",
"dict"):
505 if hasattr(metadata, attr):
506 d = getattr(metadata, attr)()
509 raise ValueError(
"Unable to extract mappings from the supplied metadata of type"
510 f
" {type(metadata)}")
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.
525 default : `object`, optional
526 Default value to use if the named item is not present.
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
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.
554 values : `list` of any type supported by container
555 The contents of the item, guaranteed to be returned as a `list.`
560 Raised if the item does not exist.
565 """Return an item as a scalar
567 If the item has more than one value then the last value is returned.
577 Value stored in the item. If the item refers to an array the
578 most recently added value is returned.
583 Raised if the item does not exist.
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
597 value : any supported type
598 Value of item; may be a scalar or array
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`
612 value : any supported type
613 Value of item; may be a scalar or array
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
625 lsst::pex::exceptions::TypeError
626 Raised if the type of `value` is incompatible with the existing
632 """Update the current container with the supplied additions.
636 addition : `collections.abc.Mapping` or `PropertySet`
637 The content to merge into the current container.
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
646 if isinstance(addition, PropertySet):
650 self.copy(k, addition, k)
652 for k, v
in addition.items():
656 """Returns a (possibly nested) dictionary with all properties.
661 Dictionary with all names and values (no comments).
665 for name
in self.names():
668 if isinstance(v, PropertySet):
669 d[name] = PropertySet.toDict(v)
675 if type(self)
is not type(other):
676 return NotImplemented
678 if len(self) != len(other):
682 if (self_typeOf := self.typeOf(name)) != other.typeOf(name):
690 if self_typeOf
in (_TYPE_MAP[
"Float"], _TYPE_MAP[
"Double"]) \
691 and math.isnan(v1)
and math.isnan(v2):
701 for itemName
in self:
702 ps.copy(itemName, self, itemName)
706 result = self.deepCopy()
707 memo[id(self)] = result
711 """Determines if the name is found at the top level hierarchy
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__``.
720 return name
in self.names(topLevelOnly=
True)
723 """Assigns the supplied value to the container.
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.
736 Uses `PropertySet.set`, overwriting any previous value.
738 if isinstance(value, Mapping):
741 for k, v
in value.items():
744 self.
set(name, value)
747 """Returns a scalar item from the container.
751 Uses `PropertySet.getScalar` to guarantee that a single value
757 if self.exists(name):
761 raise KeyError(f
"{name} not present in dict")
764 return self.toString()
767 return self.nameCount(topLevelOnly=
True)
770 for n
in self.names(topLevelOnly=
True):
774 return KeysView(self)
777 return ItemsView(self)
780 return ValuesView(self)
782 def pop(self, name, default=None):
783 """Remove the named key and return its value.
788 Name of the key to remove. Can be hierarchical.
789 default : Any, optional
790 Value to return if the key is not present.
795 The value of the item as would be returned using `getScalar()`.
800 Raised if no default is given and the key is missing.
802 if self.exists(name):
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`.
828 String key associated with the mapping. May not have a ``.``
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.
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`).
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`.
859 String key associated with the mapping. May not have a ``.``
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 ``.``
867 self.
set(key, PropertySet.from_mapping(value))
873 _typeMenu = {bool:
"Bool",
877 DateTime:
"DateTime",
878 PropertySet:
"PropertySet",
879 PropertyList:
"PropertySet",
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.
898 default : `object`, optional
899 Default value to use if the named item is not present.
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
915 """Return an item as a list.
924 values : `list` of values
925 The contents of the item, guaranteed to be returned as a `list.`
930 Raised if the item does not exist.
935 """Return an item as a scalar
937 If the item has more than one value then the last value is returned.
947 Value stored in the item. If the item refers to an array the
948 most recently added value is returned.
953 Raised if the item does not exist.
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
967 value : any supported type
968 Value of item; may be a scalar or array
971 if comment
is not None:
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`
985 value : any supported type
986 Value of item; may be a scalar or array
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).
999 lsst::pex::exceptions::TypeError
1000 Raise if the type of ``value`` is incompatible with the existing
1004 if comment
is not None:
1005 args.append(comment)
1009 """Set the comment for an existing entry.
1014 Name of the key to receive updated comment.
1021 if self.isArray(name):
1025 getattr(self, f
"set{containerType}")(name, value, comment)
1029 """Convert an `astropy.io.fits.Header` into a `PropertyList`.
1033 header : `astropy.io.fits.Header`
1034 FITS header to convert.
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`.
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
1058 for card
in header.cards:
1059 if not card.keyword:
1064 except astropy.io.fits.verify.VerifyError:
1068 comment = card.comment
or None
1069 if isinstance(value, astropy.io.fits.card.Undefined):
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)
1077 """Convert this `PropertyList` into an `astropy.io.fits.Header`.
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.
1092 header = astropy.io.fits.Header()
1093 with warnings.catch_warnings():
1096 warnings.simplefilter(
"ignore", category=astropy.io.fits.verify.VerifyWarning)
1097 for name
in self.getOrderedNames():
1099 comment = self.getComment(name)
or None
1100 for value
in values:
1103 header.append(astropy.io.fits.Card(name, value, comment), end=
True)
1107 """Return a list of tuples of name, value, comment for each property
1108 in the order that they were inserted.
1112 ret : `list` of `tuple`
1113 Tuples of name, value, comment for each property in the order
1114 in which they were inserted.
1116 orderedNames = self.getOrderedNames()
1118 for name
in orderedNames:
1119 if self.isArray(name):
1122 ret.append((name, v, self.getComment(name)))
1125 self.getComment(name)))
1129 """Return an ordered dictionary with all properties in the order that
1135 Ordered dictionary with all properties in the order that they
1136 were inserted. Comments are not included.
1140 As of Python 3.6 dicts retain their insertion order.
1148 toDict = toOrderedDict
1154 if not PropertySet.__eq__(self, other):
1158 if self.getComment(name) != other.getComment(name):
1166 for itemName
in self:
1167 pl.copy(itemName, self, itemName)
1171 result = self.deepCopy()
1172 memo[id(self)] = result
1176 for n
in self.getOrderedNames():
1180 """Assigns the supplied value to the container.
1185 Name of item to update. If the name ends with
1186 `PropertyList.COMMENTSUFFIX`, the comment is updated rather
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.
1195 Uses `PropertySet.set`, overwriting any previous value.
1201 if isinstance(value, Mapping):
1204 for k, v
in value.items():
1207 self.
set(name, value)
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`.
1226 String key associated with the mapping. May not have a ``.``
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.
1236 result: NestedMetadataDict = {}
1238 for name
in self.getOrderedNames():
1239 levels = name.split(
".")
1240 if levels[0] == key:
1242 for level_key
in levels[1:-1]:
1243 nested = result.setdefault(level_key, {})
1244 nested[levels[-1]] = self[name]
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`.
1256 String key associated with the mapping. May not have a ``.``
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.
1263 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)
_nonFiniteFloatToString(value)
setPropertyListState(container, state)
getPropertySetState(container, asLists=False)
_propertyContainerElementTypeName(container, name)
_propertyContainerGet(container, name, returnStyle)
_guessIntegerType(container, name, value)