Skip to content

Commit

Permalink
Add methods to get and set nested dictionaries consistently.
Browse files Browse the repository at this point in the history
These conform to a new typing.Protocol being added to lsst.pipe.base.
  • Loading branch information
TallJimbo committed Feb 20, 2024
1 parent f041782 commit d7875cd
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@
# see <http://www.lsstcorp.org/LegalNotices/>.
#


__all__ = ["getPropertySetState", "getPropertyListState", "setPropertySetState", "setPropertyListState"]

import enum
import math
import numbers
import dataclasses
from collections.abc import Mapping, KeysView, ValuesView, ItemsView
from typing import TypeAlias, Union

# Ensure that C++ exceptions are properly translated to Python
import lsst.pex.exceptions # noqa: F401
Expand All @@ -37,6 +37,12 @@
from .._dafBaseLib import PropertySet, PropertyList
from ..dateTime import DateTime


# Note that '|' syntax for unions doesn't work when we have to use a string
# literal (and we do since it's recursive and not an annotation).
NestedMetadataDict: TypeAlias = Mapping[str, Union[str, float, int, bool, "NestedMetadataDict"]]


# Map the type names to the internal type representation.
_TYPE_MAP = {}
for checkType in ("Bool", "Short", "Int", "Long", "LongLong", "UnsignedLongLong",
Expand Down Expand Up @@ -788,6 +794,47 @@ def __reduce__(self):
# the pybind11 memory allocation step.
return (_makePropertySet, (getPropertySetState(self),))

def get_dict(self, key: str) -> NestedMetadataDict:
"""Return a possibly-hierarchical nested `dict`.
This implements the `lsst.pipe.base.GetDictMetadata` protocol for
consistency with `lsst.pipe.base.TaskMetadata` and `PropertyList`.
Parameters
----------
key : `str`
String key associated with the mapping.
Returns
-------
value : `~collections.abc.Mapping`
Possibly-nested mapping, with `str` keys and values that are `int`,
`float`, `str`, `bool`, or another `dict` with the same key and
value types. Will be empty if ``key`` does not exist.
"""
try:
value = self.getScalar(key)
except KeyError:
return {}
return value.toDict()

def set_dict(self, key: str, value: NestedMetadataDict) -> None:
"""Assign a possibly-hierarchical nested `dict`.
This implements the `lsst.pipe.base.SetDictMetadata` protocol for
consistency with `lsst.pipe.base.TaskMetadata` and `PropertyList`.
Parameters
----------
key : `str`
String key associated with the mapping.
value : `~collections.abc.Mapping`
Possibly-nested mapping, with `str` keys and values that are `int`,
`float`, `str`, `bool`, or another `dict` with the same key and
value types.
"""
self.set(key, PropertySet.from_mapping(value))


@continueClass
class PropertyList:
Expand Down Expand Up @@ -1056,3 +1103,49 @@ def __reduce__(self):
# object.__new__(PropertyList, *args) which bypasses
# the pybind11 memory allocation step.
return (_makePropertyList, (getPropertyListState(self),))

def get_dict(self, key: str) -> NestedMetadataDict:
"""Return a possibly-hierarchical nested `dict`.
This implements the `lsst.pipe.base.GetDictMetadata` protocol for
consistency with `lsst.pipe.base.TaskMetadata` and `PropertySet`.
Parameters
----------
key : `str`
String key associated with the mapping.
Returns
-------
value : `~collections.abc.Mapping`
Possibly-nested mapping, with `str` keys and values that are `int`,
`float`, `str`, `bool`, or another `dict` with the same key and
value types. Will be empty if ``key`` does not exist.
"""
result: NestedMetadataDict = {}
name: str
for name in self.getOrderedNames():
levels = name.split(".")
if levels[0] == key:
nested = result
for level_key in levels[1:-1]:
nested = result.setdefault(level_key, {})
nested[levels[-1]] = self[name]
return result

def set_dict(self, key: str, value: NestedMetadataDict) -> None:
"""Assign a possibly-hierarchical nested `dict`.
This implements the `lsst.pipe.base.SetDictMetadata` protocol for
consistency with `lsst.pipe.base.TaskMetadata` and `PropertySet`.
Parameters
----------
key : `str`
String key associated with the mapping.
value : `~collections.abc.Mapping`
Possibly-nested mapping, with `str` keys and values that are `int`,
`float`, `str`, `bool`, or another `dict` with the same key and
value types.
"""
self.set(key, PropertySet.from_mapping(value))
26 changes: 26 additions & 0 deletions tests/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,32 @@ def testCopyPropertyList(self):
self.assertNotIn("dt", deep)
self.assertIn("dt", self.pl)

def test_get_set_dict(self):
"""Test the get_dict and set_dict methods of both `PropertyList` and
`PropertySet`.
"""

def check(obj):
d1 = {"one": 1, "two": 2.0, "three": True, "four": {"a": 4, "b": "B"}, "five": {}}
obj.set_dict("d", d1)
obj.set_dict("e", {})
d2 = obj.get_dict("d")
# Keys with empty-dict values may or may not be round-tripped.
self.assertGreaterEqual(d2.keys(), {"one", "two", "three", "four"})
self.assertLessEqual(d2.keys(), {"one", "two", "three", "four", "five"})
self.assertEqual(d2["one"], d1["one"])
self.assertEqual(d2["two"], d1["two"])
self.assertEqual(d2["three"], d1["three"])
self.assertEqual(d2["four"], d1["four"])
self.assertEqual(d2.get("five", {}), d1["five"])
# Empty dict may or may not have been added, and retrieving it or
# a key that was never added yields an empty dict.
self.assertEqual(obj.get_dict("e"), {})
self.assertEqual(obj.get_dict("f"), {})

check(lsst.daf.base.PropertySet())
check(lsst.daf.base.PropertyList())


if __name__ == '__main__':
unittest.main()

0 comments on commit d7875cd

Please sign in to comment.