Repository: pydata/xarray

Issue:
Coordinates object permits invalid state
### What happened?

It is currently possible to create a `Coordinates` object where a variable shares a name with a dimension, but the variable is not 1D. This is explicitly forbidden by the xarray data model.



### What did you expect to happen?

If you try to pass the resulting object into the `Dataset` constructor you get the expected error telling you that this is forbidden, but that error should have been raised by `Coordinates.__init__`.

### Minimal Complete Verifiable Example

```Python
In [1]: from xarray.core.coordinates import Coordinates

In [2]: from xarray.core.variable import Variable

In [4]: import numpy as np

In [5]: var = Variable(data=np.arange(6).reshape(2, 3), dims=['x', 'y'])

In [6]: var
Out[6]: 
<xarray.Variable (x: 2, y: 3)> Size: 48B
array([[0, 1, 2],
       [3, 4, 5]])

In [7]: coords = Coordinates(coords={'x': var}, indexes={})

In [8]: coords
Out[8]: 
Coordinates:
    x        (x, y) int64 48B 0 1 2 3 4 5

In [10]: import xarray as xr

In [11]: ds = xr.Dataset(coords=coords)
---------------------------------------------------------------------------
MergeError                                Traceback (most recent call last)
Cell In[11], line 1
----> 1 ds = xr.Dataset(coords=coords)

File ~/Documents/Work/Code/xarray/xarray/core/dataset.py:693, in Dataset.__init__(self, data_vars, coords, attrs)
    690 if isinstance(coords, Dataset):
    691     coords = coords._variables
--> 693 variables, coord_names, dims, indexes, _ = merge_data_and_coords(
    694     data_vars, coords
    695 )
    697 self._attrs = dict(attrs) if attrs else None
    698 self._close = None

File ~/Documents/Work/Code/xarray/xarray/core/dataset.py:422, in merge_data_and_coords(data_vars, coords)
    418     coords = create_coords_with_default_indexes(coords, data_vars)
    420 # exclude coords from alignment (all variables in a Coordinates object should
    421 # already be aligned together) and use coordinates' indexes to align data_vars
--> 422 return merge_core(
    423     [data_vars, coords],
    424     compat="broadcast_equals",
    425     join="outer",
    426     explicit_coords=tuple(coords),
    427     indexes=coords.xindexes,
    428     priority_arg=1,
    429     skip_align_args=[1],
    430 )

File ~/Documents/Work/Code/xarray/xarray/core/merge.py:731, in merge_core(objects, compat, join, combine_attrs, priority_arg, explicit_coords, indexes, fill_value, skip_align_args)
    729     coord_names.intersection_update(variables)
    730 if explicit_coords is not None:
--> 731     assert_valid_explicit_coords(variables, dims, explicit_coords)
    732     coord_names.update(explicit_coords)
    733 for dim, size in dims.items():

File ~/Documents/Work/Code/xarray/xarray/core/merge.py:577, in assert_valid_explicit_coords(variables, dims, explicit_coords)
    575 for coord_name in explicit_coords:
    576     if coord_name in dims and variables[coord_name].dims != (coord_name,):
--> 577         raise MergeError(
    578             f"coordinate {coord_name} shares a name with a dataset dimension, but is "
    579             "not a 1D variable along that dimension. This is disallowed "
    580             "by the xarray data model."
    581         )

MergeError: coordinate x shares a name with a dataset dimension, but is not a 1D variable along that dimension. This is disallowed by the xarray data model.
```


### MVCE confirmation

- [X] Minimal example — the example is as focused as reasonably possible to demonstrate the underlying issue in xarray.
- [X] Complete example — the example is self-contained, including all data and the text of any traceback.
- [X] Verifiable example — the example copy & pastes into an IPython prompt or [Binder notebook](https://mybinder.org/v2/gh/pydata/xarray/main?urlpath=lab/tree/doc/examples/blank_template.ipynb), returning the result.
- [x] New issue — a search of GitHub Issues suggests this is not a duplicate.
- [X] Recent environment — the issue occurs with the latest version of xarray and its dependencies.

### Relevant log output

_No response_

### Anything else we need to know?

I noticed this whilst working on #8872

### Environment

`main`

Discussion:
dcherian: nice find!

Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index e8ce0cff..e3581876 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -73,6 +73,9 @@ Bug fixes
 - Warn and return bytes undecoded in case of UnicodeDecodeError in h5netcdf-backend
   (:issue:`5563`, :pull:`8874`).
   By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
+- Fix bug incorrectly disallowing creation of a dataset with a multidimensional coordinate variable with the same name as one of its dims.
+  (:issue:`8884`, :pull:`8886`)
+  By `Tom Nicholas <https://github.com/TomNicholas>`_.
 
 
 Documentation
diff --git a/xarray/core/merge.py b/xarray/core/merge.py
index cbd06c8f..a90e59e7 100644
--- a/xarray/core/merge.py
+++ b/xarray/core/merge.py
@@ -562,25 +562,6 @@ def merge_coords(
     return variables, out_indexes
 
 
-def assert_valid_explicit_coords(
-    variables: Mapping[Any, Any],
-    dims: Mapping[Any, int],
-    explicit_coords: Iterable[Hashable],
-) -> None:
-    """Validate explicit coordinate names/dims.
-
-    Raise a MergeError if an explicit coord shares a name with a dimension
-    but is comprised of arbitrary dimensions.
-    """
-    for coord_name in explicit_coords:
-        if coord_name in dims and variables[coord_name].dims != (coord_name,):
-            raise MergeError(
-                f"coordinate {coord_name} shares a name with a dataset dimension, but is "
-                "not a 1D variable along that dimension. This is disallowed "
-                "by the xarray data model."
-            )
-
-
 def merge_attrs(variable_attrs, combine_attrs, context=None):
     """Combine attributes from different variables according to combine_attrs"""
     if not variable_attrs:
@@ -728,7 +709,6 @@ def merge_core(
         # coordinates may be dropped in merged results
         coord_names.intersection_update(variables)
     if explicit_coords is not None:
-        assert_valid_explicit_coords(variables, dims, explicit_coords)
         coord_names.update(explicit_coords)
     for dim, size in dims.items():
         if dim in variables:


Test patch:
diff --git a/xarray/tests/test_coordinates.py b/xarray/tests/test_coordinates.py
index 40743194..f88e554d 100644
--- a/xarray/tests/test_coordinates.py
+++ b/xarray/tests/test_coordinates.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import numpy as np
 import pandas as pd
 import pytest
 
@@ -8,7 +9,7 @@ from xarray.core.coordinates import Coordinates
 from xarray.core.dataarray import DataArray
 from xarray.core.dataset import Dataset
 from xarray.core.indexes import PandasIndex, PandasMultiIndex
-from xarray.core.variable import IndexVariable
+from xarray.core.variable import IndexVariable, Variable
 from xarray.tests import assert_identical, source_ndarray
 
 
@@ -174,3 +175,10 @@ class TestCoordinates:
         left2, right2 = align(left, right, join="override")
         assert_identical(left2, left)
         assert_identical(left2, right2)
+
+    def test_dataset_from_coords_with_multidim_var_same_name(self):
+        # regression test for GH #8883
+        var = Variable(data=np.arange(6).reshape(2, 3), dims=["x", "y"])
+        coords = Coordinates(coords={"x": var}, indexes={})
+        ds = Dataset(coords=coords)
+        assert ds.coords["x"].dims == ("x", "y")
diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py
index 39c404d0..e2a64964 100644
--- a/xarray/tests/test_dataset.py
+++ b/xarray/tests/test_dataset.py
@@ -486,14 +486,6 @@ class TestDataset:
         actual = Dataset({"z": expected["z"]})
         assert_identical(expected, actual)
 
-    def test_constructor_invalid_dims(self) -> None:
-        # regression for GH1120
-        with pytest.raises(MergeError):
-            Dataset(
-                data_vars=dict(v=("y", [1, 2, 3, 4])),
-                coords=dict(y=DataArray([0.1, 0.2, 0.3, 0.4], dims="x")),
-            )
-
     def test_constructor_1d(self) -> None:
         expected = Dataset({"x": (["x"], 5.0 + np.arange(5))})
         actual = Dataset({"x": 5.0 + np.arange(5)})