diff --git a/doc/whats-new.rst b/doc/whats-new.rst index e959ec111f5..0d146a7fd0d 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -35,6 +35,8 @@ Deprecations Bug fixes ~~~~~~~~~ +- Fix bug causing `DataTree.from_dict` to be sensitive to insertion order (:issue:`9276`, :pull:`9292`). + By `Tom Nicholas `_. Documentation ~~~~~~~~~~~~~ diff --git a/xarray/core/datatree.py b/xarray/core/datatree.py index 65ff8667cb7..f8a8a5fbc18 100644 --- a/xarray/core/datatree.py +++ b/xarray/core/datatree.py @@ -1102,9 +1102,14 @@ def from_dict( else: obj = cls(name=name, data=root_data, parent=None, children=None) + def depth(item) -> int: + pathstr, _ = item + return len(NodePath(pathstr).parts) + if d: # Populate tree with children determined from data_objects mapping - for path, data in d.items(): + # Sort keys by depth so as to insert nodes from root first (see GH issue #9276) + for path, data in sorted(d.items(), key=depth): # Create and set new node node_name = NodePath(path).name if isinstance(data, DataTree): diff --git a/xarray/tests/test_datatree.py b/xarray/tests/test_datatree.py index 31d77ca17e7..c875322b9c5 100644 --- a/xarray/tests/test_datatree.py +++ b/xarray/tests/test_datatree.py @@ -561,6 +561,30 @@ def test_roundtrip_unnamed_root(self, simple_datatree): roundtrip = DataTree.from_dict(dt.to_dict()) assert roundtrip.equals(dt) + def test_insertion_order(self): + # regression test for GH issue #9276 + reversed = DataTree.from_dict( + { + "/Homer/Lisa": xr.Dataset({"age": 8}), + "/Homer/Bart": xr.Dataset({"age": 10}), + "/Homer": xr.Dataset({"age": 39}), + "/": xr.Dataset({"age": 83}), + } + ) + expected = DataTree.from_dict( + { + "/": xr.Dataset({"age": 83}), + "/Homer": xr.Dataset({"age": 39}), + "/Homer/Lisa": xr.Dataset({"age": 8}), + "/Homer/Bart": xr.Dataset({"age": 10}), + } + ) + assert reversed.equals(expected) + + # Check that Bart and Lisa's order is still preserved within the group, + # despite 'Bart' coming before 'Lisa' when sorted alphabetically + assert list(reversed["Homer"].children.keys()) == ["Lisa", "Bart"] + class TestDatasetView: def test_view_contents(self):