Repository: pydata/xarray

Issue:
arguments not passed to `open_groups_as_dict` function when opening a datatree
### What is your issue?

Hi everyone, 

I am trying to open a datatree I created, which looks as follows.

```python
Group: /
├── Group: /group_0
│   │   Dimensions:  (x: 4, y: 5)
│   │   Coordinates:
│   │     * x        (x) int64 32B 10 20 30 40
│   │     * y        (y) datetime64[ns] 40B 2000-01-01 2000-01-02 ... 2000-01-05
│   │   Data variables:
│   │       foo      (x, y) float64 160B ...
│   ├── Group: /group_0/subgroup_1
│   └── Group: /group_0/subgroup_2
│       │   Dimensions:  (x: 4, y: 5, dim_0: 9)
│       │   Coordinates:
│       │     * x        (x) int64 32B 10 20 30 40
│       │     * y        (y) datetime64[ns] 40B 2000-01-01 2000-01-02 ... 2000-01-05
│       │   Dimensions without coordinates: dim_0
│       │   Data variables:
│       │       a        (dim_0) int64 72B ...
│       └── Group: /group_0/subgroup_2/subsubgroup_1
│           │   Dimensions:  (x: 4, y: 5, dim_0: 9)
│           │   Coordinates:
│           │     * x        (x) int64 32B 10 20 30 40
│           │     * y        (y) datetime64[ns] 40B 2000-01-01 2000-01-02 ... 2000-01-05
│           │   Dimensions without coordinates: dim_0
│           │   Data variables:
│           │       a        (dim_0) int64 72B ...
│           └── Group: /group_0/subgroup_2/subsubgroup_1/subsubsubn
│                   Dimensions:  (x: 4, y: 5, dim_0: 9)
│                   Coordinates:
│                     * x        (x) int64 32B 10 20 30 40
│                     * y        (y) datetime64[ns] 40B 2000-01-01 2000-01-02 ... 2000-01-05
```

However, if I wanted to select and specific groups as follows 

```python
path = "/path/to/datatree/hdf_dtree_10.h5"
dtree = open_datatree(path, engine='h5netcdf', group="subgroup_1")

```
The output is the whole `datatree`. Digging around. I found out that some parameters passed to https://github.com/pydata/xarray/blob/a8c989673d0ea0cd099ede136efa117eaeb7ae1d/xarray/backends/h5netcdf_.py#L454 are missing, including the `group` parameter. A similar situation happens with netcdf4 datatrees 



Discussion:
kmuehlbauer: @TomNicholas Yes, I think this is correct. I'm not sure if this can be restructured and would rely on @keewis expertise.
keewis: sorry for the delay, I've been far away from any computer for the past two weeks.

I'd say short of adding the arguments to `**kwargs` that's the only thing we can do, as really we're just forwarding to `open_groups_as_dict` (which by the way I thought we wanted to call just `open_groups`? Maybe I missed some discussion on that).

Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index f6ee951a..2242f452 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -155,6 +155,9 @@ Bug fixes
 - Fix deprecation warning that was raised when calling ``np.array`` on an ``xr.DataArray``
   in NumPy 2.0 (:issue:`9312`, :pull:`9393`)
   By `Andrew Scherer <https://github.com/andrew-s28>`_.
+- Fix passing missing arguments to when opening hdf5 and netCDF4 datatrees
+  (:issue:`9427`, :pull: `9428`).
+  By `Alfonso Ladino <https://github.com/aladinor>`_.
 - Fix support for using ``pandas.DateOffset``, ``pandas.Timedelta``, and
   ``datetime.timedelta`` objects as ``resample`` frequencies
   (:issue:`9408`, :pull:`9413`).
diff --git a/pyproject.toml b/pyproject.toml
index 79ebfb52..c23d12ff 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -214,13 +214,16 @@ warn_return_any = true
 
 module = ["xarray.namedarray.*", "xarray.tests.test_namedarray"]
 
-[tool.pyright]
-defineConstant = {DEBUG = true}
-
-# Enabling this means that developers who have disabled the warning locally —
-# because not all dependencies are installable — are overridden
-# reportMissingImports = true
-reportMissingTypeStubs = false
+# We disable pyright here for now, since including it means that all errors show
+# up in devs' VS Code, which then makes it more difficult to work with actual
+# errors. It overrides local VS Code settings so isn't escapable.
+
+# [tool.pyright]
+# defineConstant = {DEBUG = true}
+# # Enabling this means that developers who have disabled the warning locally —
+# # because not all dependencies are installable — are overridden
+# # reportMissingImports = true
+# reportMissingTypeStubs = false
 
 [tool.ruff]
 extend-exclude = [
diff --git a/xarray/backends/h5netcdf_.py b/xarray/backends/h5netcdf_.py
index f7a4fff2..f1ab57ba 100644
--- a/xarray/backends/h5netcdf_.py
+++ b/xarray/backends/h5netcdf_.py
@@ -477,7 +477,25 @@ class H5netcdfBackendEntrypoint(BackendEntrypoint):
 
         from xarray.core.datatree import DataTree
 
-        groups_dict = self.open_groups_as_dict(filename_or_obj, **kwargs)
+        groups_dict = self.open_groups_as_dict(
+            filename_or_obj,
+            mask_and_scale=mask_and_scale,
+            decode_times=decode_times,
+            concat_characters=concat_characters,
+            decode_coords=decode_coords,
+            drop_variables=drop_variables,
+            use_cftime=use_cftime,
+            decode_timedelta=decode_timedelta,
+            format=format,
+            group=group,
+            lock=lock,
+            invalid_netcdf=invalid_netcdf,
+            phony_dims=phony_dims,
+            decode_vlen_strings=decode_vlen_strings,
+            driver=driver,
+            driver_kwds=driver_kwds,
+            **kwargs,
+        )
 
         return DataTree.from_dict(groups_dict)
 
diff --git a/xarray/backends/netCDF4_.py b/xarray/backends/netCDF4_.py
index 32f6abeb..99ca263f 100644
--- a/xarray/backends/netCDF4_.py
+++ b/xarray/backends/netCDF4_.py
@@ -713,7 +713,24 @@ class NetCDF4BackendEntrypoint(BackendEntrypoint):
 
         from xarray.core.datatree import DataTree
 
-        groups_dict = self.open_groups_as_dict(filename_or_obj, **kwargs)
+        groups_dict = self.open_groups_as_dict(
+            filename_or_obj,
+            mask_and_scale=mask_and_scale,
+            decode_times=decode_times,
+            concat_characters=concat_characters,
+            decode_coords=decode_coords,
+            drop_variables=drop_variables,
+            use_cftime=use_cftime,
+            decode_timedelta=decode_timedelta,
+            group=group,
+            format=format,
+            clobber=clobber,
+            diskless=diskless,
+            persist=persist,
+            lock=lock,
+            autoclose=autoclose,
+            **kwargs,
+        )
 
         return DataTree.from_dict(groups_dict)
 
diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py
index 45dc3545..def932bd 100644
--- a/xarray/backends/zarr.py
+++ b/xarray/backends/zarr.py
@@ -1295,7 +1295,25 @@ class ZarrBackendEntrypoint(BackendEntrypoint):
         from xarray.core.datatree import DataTree
 
         filename_or_obj = _normalize_path(filename_or_obj)
-        groups_dict = self.open_groups_as_dict(filename_or_obj, **kwargs)
+        groups_dict = self.open_groups_as_dict(
+            filename_or_obj=filename_or_obj,
+            mask_and_scale=mask_and_scale,
+            decode_times=decode_times,
+            concat_characters=concat_characters,
+            decode_coords=decode_coords,
+            drop_variables=drop_variables,
+            use_cftime=use_cftime,
+            decode_timedelta=decode_timedelta,
+            group=group,
+            mode=mode,
+            synchronizer=synchronizer,
+            consolidated=consolidated,
+            chunk_store=chunk_store,
+            storage_options=storage_options,
+            stacklevel=stacklevel,
+            zarr_version=zarr_version,
+            **kwargs,
+        )
 
         return DataTree.from_dict(groups_dict)
 
diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py
index b8da7ab8..8d460e49 100644
--- a/xarray/core/dataarray.py
+++ b/xarray/core/dataarray.py
@@ -69,7 +69,7 @@ from xarray.core.types import (
 )
 from xarray.core.utils import (
     Default,
-    HybridMappingProxy,
+    FilteredMapping,
     ReprObject,
     _default,
     either_dict_or_kwargs,
@@ -929,11 +929,10 @@ class DataArray(
     @property
     def _item_sources(self) -> Iterable[Mapping[Hashable, Any]]:
         """Places to look-up items for key-completion"""
-        yield HybridMappingProxy(keys=self._coords, mapping=self.coords)
+        yield FilteredMapping(keys=self._coords, mapping=self.coords)
 
         # virtual coordinates
-        # uses empty dict -- everything here can already be found in self.coords.
-        yield HybridMappingProxy(keys=self.dims, mapping={})
+        yield FilteredMapping(keys=self.dims, mapping=self.coords)
 
     def __contains__(self, key: Any) -> bool:
         return key in self.data
diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py
index 0311a9c2..d57a6957 100644
--- a/xarray/core/dataset.py
+++ b/xarray/core/dataset.py
@@ -103,9 +103,9 @@ from xarray.core.types import (
 )
 from xarray.core.utils import (
     Default,
+    FilteredMapping,
     Frozen,
     FrozenMappingWarningOnValuesAccess,
-    HybridMappingProxy,
     OrderedSet,
     _default,
     decode_numpy_dict_values,
@@ -1507,10 +1507,10 @@ class Dataset(
     def _item_sources(self) -> Iterable[Mapping[Hashable, Any]]:
         """Places to look-up items for key-completion"""
         yield self.data_vars
-        yield HybridMappingProxy(keys=self._coord_names, mapping=self.coords)
+        yield FilteredMapping(keys=self._coord_names, mapping=self.coords)
 
         # virtual coordinates
-        yield HybridMappingProxy(keys=self.sizes, mapping=self)
+        yield FilteredMapping(keys=self.sizes, mapping=self)
 
     def __contains__(self, key: object) -> bool:
         """The 'in' operator will return true or false depending on whether
diff --git a/xarray/core/datatree.py b/xarray/core/datatree.py
index 52d44bec..82769fcd 100644
--- a/xarray/core/datatree.py
+++ b/xarray/core/datatree.py
@@ -40,8 +40,8 @@ from xarray.core.options import OPTIONS as XR_OPTS
 from xarray.core.treenode import NamedNode, NodePath
 from xarray.core.utils import (
     Default,
+    FilteredMapping,
     Frozen,
-    HybridMappingProxy,
     _default,
     either_dict_or_kwargs,
     maybe_wrap_array,
@@ -516,10 +516,17 @@ class DataTree(
         check_alignment(path, node_ds, parent_ds, self.children)
         _deduplicate_inherited_coordinates(self, parent)
 
+    @property
+    def _node_coord_variables_with_index(self) -> Mapping[Hashable, Variable]:
+        return FilteredMapping(
+            keys=self._node_indexes, mapping=self._node_coord_variables
+        )
+
     @property
     def _coord_variables(self) -> ChainMap[Hashable, Variable]:
         return ChainMap(
-            self._node_coord_variables, *(p._node_coord_variables for p in self.parents)
+            self._node_coord_variables,
+            *(p._node_coord_variables_with_index for p in self.parents),
         )
 
     @property
@@ -720,10 +727,10 @@ class DataTree(
     def _item_sources(self) -> Iterable[Mapping[Any, Any]]:
         """Places to look-up items for key-completion"""
         yield self.data_vars
-        yield HybridMappingProxy(keys=self._coord_variables, mapping=self.coords)
+        yield FilteredMapping(keys=self._coord_variables, mapping=self.coords)
 
         # virtual coordinates
-        yield HybridMappingProxy(keys=self.dims, mapping=self)
+        yield FilteredMapping(keys=self.dims, mapping=self)
 
         # immediate child nodes
         yield self.children
diff --git a/xarray/core/utils.py b/xarray/core/utils.py
index 3c1dee7a..e5168342 100644
--- a/xarray/core/utils.py
+++ b/xarray/core/utils.py
@@ -465,7 +465,7 @@ class FrozenMappingWarningOnValuesAccess(Frozen[K, V]):
         return super().values()
 
 
-class HybridMappingProxy(Mapping[K, V]):
+class FilteredMapping(Mapping[K, V]):
     """Implements the Mapping interface. Uses the wrapped mapping for item lookup
     and a separate wrapped keys collection for iteration.
 
@@ -473,25 +473,31 @@ class HybridMappingProxy(Mapping[K, V]):
     eagerly accessing its items or when a mapping object is expected but only
     iteration over keys is actually used.
 
-    Note: HybridMappingProxy does not validate consistency of the provided `keys`
-    and `mapping`. It is the caller's responsibility to ensure that they are
-    suitable for the task at hand.
+    Note: keys should be a subset of mapping, but FilteredMapping does not
+    validate consistency of the provided `keys` and `mapping`. It is the
+    caller's responsibility to ensure that they are suitable for the task at
+    hand.
     """
 
-    __slots__ = ("_keys", "mapping")
+    __slots__ = ("keys_", "mapping")
 
     def __init__(self, keys: Collection[K], mapping: Mapping[K, V]):
-        self._keys = keys
+        self.keys_ = keys  # .keys is already a property on Mapping
         self.mapping = mapping
 
     def __getitem__(self, key: K) -> V:
+        if key not in self.keys_:
+            raise KeyError(key)
         return self.mapping[key]
 
     def __iter__(self) -> Iterator[K]:
-        return iter(self._keys)
+        return iter(self.keys_)
 
     def __len__(self) -> int:
-        return len(self._keys)
+        return len(self.keys_)
+
+    def __repr__(self) -> str:
+        return f"{type(self).__name__}(keys={self.keys_!r}, mapping={self.mapping!r})"
 
 
 class OrderedSet(MutableSet[T]):


Test patch:
diff --git a/xarray/tests/test_datatree.py b/xarray/tests/test_datatree.py
index 30934f83..3365e493 100644
--- a/xarray/tests/test_datatree.py
+++ b/xarray/tests/test_datatree.py
@@ -216,16 +216,16 @@ class TestStoreDatasets:
 
 
 class TestToDataset:
-    def test_to_dataset(self):
-        base = xr.Dataset(coords={"a": 1})
-        sub = xr.Dataset(coords={"b": 2})
+    def test_to_dataset_inherited(self):
+        base = xr.Dataset(coords={"a": [1], "b": 2})
+        sub = xr.Dataset(coords={"c": [3]})
         tree = DataTree.from_dict({"/": base, "/sub": sub})
         subtree = typing.cast(DataTree, tree["sub"])
 
         assert_identical(tree.to_dataset(inherited=False), base)
         assert_identical(subtree.to_dataset(inherited=False), sub)
 
-        sub_and_base = xr.Dataset(coords={"a": 1, "b": 2})
+        sub_and_base = xr.Dataset(coords={"a": [1], "c": [3]})  # no "b"
         assert_identical(tree.to_dataset(inherited=True), base)
         assert_identical(subtree.to_dataset(inherited=True), sub_and_base)
 
@@ -714,7 +714,8 @@ class TestCoords:
         dt["child"] = DataTree()
         child = dt["child"]
 
-        assert set(child.coords) == {"x", "y", "a", "b"}
+        assert set(dt.coords) == {"x", "y", "a", "b"}
+        assert set(child.coords) == {"x", "y"}
 
         actual = child.copy(deep=True)
         actual.coords["x"] = ("x", ["a", "b"])
@@ -729,7 +730,7 @@ class TestCoords:
 
         with pytest.raises(KeyError):
             # cannot delete inherited coordinate from child node
-            del child["b"]
+            del child["x"]
 
         # TODO requires a fix for #9472
         # actual = child.copy(deep=True)
@@ -1278,22 +1279,23 @@ class TestInheritance:
         assert "x" in dt["/b"].coords
         xr.testing.assert_identical(dt["/x"], dt["/b/x"])
 
-    def test_inherited_coords_override(self):
+    def test_inherit_only_index_coords(self):
         dt = DataTree.from_dict(
             {
-                "/": xr.Dataset(coords={"x": 1, "y": 2}),
-                "/b": xr.Dataset(coords={"x": 4, "z": 3}),
+                "/": xr.Dataset(coords={"x": [1], "y": 2}),
+                "/b": xr.Dataset(coords={"z": 3}),
             }
         )
         assert dt.coords.keys() == {"x", "y"}
-        root_coords = {"x": 1, "y": 2}
-        sub_coords = {"x": 4, "y": 2, "z": 3}
-        xr.testing.assert_equal(dt["/x"], xr.DataArray(1, coords=root_coords))
-        xr.testing.assert_equal(dt["/y"], xr.DataArray(2, coords=root_coords))
-        assert dt["/b"].coords.keys() == {"x", "y", "z"}
-        xr.testing.assert_equal(dt["/b/x"], xr.DataArray(4, coords=sub_coords))
-        xr.testing.assert_equal(dt["/b/y"], xr.DataArray(2, coords=sub_coords))
-        xr.testing.assert_equal(dt["/b/z"], xr.DataArray(3, coords=sub_coords))
+        xr.testing.assert_equal(
+            dt["/x"], xr.DataArray([1], dims=["x"], coords={"x": [1], "y": 2})
+        )
+        xr.testing.assert_equal(dt["/y"], xr.DataArray(2, coords={"y": 2}))
+        assert dt["/b"].coords.keys() == {"x", "z"}
+        xr.testing.assert_equal(
+            dt["/b/x"], xr.DataArray([1], dims=["x"], coords={"x": [1], "z": 3})
+        )
+        xr.testing.assert_equal(dt["/b/z"], xr.DataArray(3, coords={"z": 3}))
 
     def test_inherited_coords_with_index_are_deduplicated(self):
         dt = DataTree.from_dict(
diff --git a/xarray/tests/test_utils.py b/xarray/tests/test_utils.py
index 86e34d15..9ef4a688 100644
--- a/xarray/tests/test_utils.py
+++ b/xarray/tests/test_utils.py
@@ -139,6 +139,16 @@ class TestDictionaries:
             "Frozen({'b': 'B', 'a': 'A'})",
         )
 
+    def test_filtered(self):
+        x = utils.FilteredMapping(keys={"a"}, mapping={"a": 1, "b": 2})
+        assert "a" in x
+        assert "b" not in x
+        assert x["a"] == 1
+        assert list(x) == ["a"]
+        assert len(x) == 1
+        assert repr(x) == "FilteredMapping(keys={'a'}, mapping={'a': 1, 'b': 2})"
+        assert dict(x) == {"a": 1}
+
 
 def test_repr_object():
     obj = utils.ReprObject("foo")