diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 0e74ca1e2fc..2df83a0c4fc 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -69,6 +69,11 @@ Bug Fixes for zarr writes. Existing zarr stores written with the old ``int8`` encoding are still read correctly. (:issue:`2937`, :pull:`11318`) By `Evan Lyall `_. +- Raise an informative ``TypeError`` when a :py:class:`~xarray.Coordinates` object is + passed as a coordinate value, e.g. ``ds.assign_coords({"x": coords})``, instead + of silently creating a broken coordinate. Pass the object directly with + ``ds.assign_coords(coords)`` (:issue:`10194`). + By `NoiceHex `_. Documentation diff --git a/pyproject.toml b/pyproject.toml index 828bee18fa5..6fad5adacff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -424,6 +424,7 @@ Claus = "Claus" Celles = "Celles" slowy = "slowy" Commun = "Commun" +Noice = "Noice" # Tests Ome = "Ome" diff --git a/xarray/core/variable.py b/xarray/core/variable.py index ed262fe49e0..0ece498c8ce 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -127,6 +127,7 @@ def as_variable( The newly created variable. """ + from xarray.core.coordinates import Coordinates from xarray.core.dataarray import DataArray # TODO: consider extending this method to automatically handle Iris and @@ -160,6 +161,12 @@ def as_variable( obj = Variable([], obj) elif isinstance(obj, pd.Index | IndexVariable) and obj.name is not None: obj = Variable(obj.name, obj) + elif isinstance(obj, Coordinates): + raise TypeError( + f"Variable {name!r}: Using a Coordinates object to construct a variable is " + "ambiguous, please pass the Coordinates object directly instead, e.g., " + "`obj.assign_coords(coords)` instead of `obj.assign_coords({name: coords})`." + ) elif isinstance(obj, set | dict): raise TypeError(f"variable {name!r} has invalid type {type(obj)!r}") elif name is not None: diff --git a/xarray/tests/test_coordinates.py b/xarray/tests/test_coordinates.py index ba3f366ba77..478ef0a303b 100644 --- a/xarray/tests/test_coordinates.py +++ b/xarray/tests/test_coordinates.py @@ -289,6 +289,26 @@ def test_rename_vars(self) -> None: assert set(actual.dims) == {"x", "y"} assert set(actual.variables) == {"a", "u", "v"} + def test_coordinates_as_coord_value(self) -> None: + # a Coordinates object must be passed directly, not as a value of a + # mapping of coordinate names (GH10194) + ds = Dataset({"foo": ("x", [1, 2, 3])}) + coords = Coordinates({"x": [4, 5, 6]}) + + with pytest.raises(TypeError, match=r"Using a Coordinates object"): + ds.assign_coords({"x": coords}) + with pytest.raises(TypeError, match=r"Using a Coordinates object"): + ds.foo.assign_coords({"x": coords}) + with pytest.raises(TypeError, match=r"Using a Coordinates object"): + Dataset({"foo": ("x", [1, 2, 3])}, coords={"x": coords}) + with pytest.raises(TypeError, match=r"Using a Coordinates object"): + DataArray([1, 2, 3], dims="x", coords={"x": coords}) + + # passing it directly still works and keeps the index + actual = ds.assign_coords(coords) + assert_identical(actual, Dataset({"foo": ("x", [1, 2, 3])}, coords=coords)) + assert "x" in actual.xindexes + def test_operator_merge(self) -> None: coords1 = Coordinates({"x": ("x", [0, 1, 2])}) coords2 = Coordinates({"y": ("y", [3, 4, 5])}) diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index a7178bba512..08ebfef5301 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -5021,7 +5021,11 @@ def test_drop_attrs(self) -> None: mx = xr.Coordinates.from_pandas_multiindex( pd.MultiIndex.from_tuples([(1, 2), (3, 4)], names=["d", "e"]), "z" ) - ds = Dataset(dict(var1=var), coords=dict(y=idx, z=mx)).assign_attrs(a=1, b=2) + ds = ( + Dataset(dict(var1=var), coords=dict(y=idx)) + .assign_coords(mx) + .assign_attrs(a=1, b=2) + ) assert ds.attrs != {} assert ds["var1"].attrs != {} assert ds["y"].attrs != {} diff --git a/xarray/tests/test_variable.py b/xarray/tests/test_variable.py index c2bc73f70b9..1190d35613b 100644 --- a/xarray/tests/test_variable.py +++ b/xarray/tests/test_variable.py @@ -12,7 +12,14 @@ import pytest import pytz -from xarray import DataArray, Dataset, IndexVariable, Variable, set_options +from xarray import ( + Coordinates, + DataArray, + Dataset, + IndexVariable, + Variable, + set_options, +) from xarray.core import dtypes, duck_array_ops, indexing from xarray.core.common import full_like, ones_like, zeros_like from xarray.core.extension_array import PandasExtensionArray @@ -1273,6 +1280,10 @@ def test_as_variable(self): with pytest.raises(TypeError): as_variable(("x", DataArray([]))) + # GH10194 + with pytest.raises(TypeError, match=r"Using a Coordinates object"): + as_variable(Coordinates({"x": [1, 2, 3]}), name="x") + def test_repr(self): v = Variable(["time", "x"], [[1, 2, 3], [4, 5, 6]], {"foo": "bar"}) v = v.astype(np.uint64)