diff --git a/xarray/core/merge.py b/xarray/core/merge.py index 6426f741750..db03e7423b0 100644 --- a/xarray/core/merge.py +++ b/xarray/core/merge.py @@ -333,6 +333,12 @@ def collect_variables_and_indexes( indexes = {} grouped: dict[Hashable, list[MergeElement]] = defaultdict(list) + sizes: dict[Hashable, int] = { + k: v + for i in list_of_mappings + for j in i.values() + for k, v in getattr(j, "sizes", {}).items() + } def append(name, variable, index): grouped[name].append((variable, index)) @@ -355,7 +361,7 @@ def append_all(variables, indexes): indexes_.pop(name, None) append_all(coords_, indexes_) - variable = as_variable(variable, name=name, auto_convert=False) + variable = as_variable(variable, name=name, auto_convert=False, sizes=sizes) if name in indexes: append(name, variable, indexes[name]) elif variable.dims == (name,): diff --git a/xarray/core/variable.py b/xarray/core/variable.py index 088c5f405ef..e8f68de74a5 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -86,7 +86,10 @@ class MissingDimensionsError(ValueError): def as_variable( - obj: T_DuckArray | Any, name=None, auto_convert: bool = True + obj: T_DuckArray | Any, + name=None, + auto_convert: bool = True, + sizes: Mapping | None = None, ) -> Variable | IndexVariable: """Convert an object into a Variable. @@ -127,11 +130,13 @@ def as_variable( if isinstance(obj, Variable): obj = obj.copy(deep=False) elif isinstance(obj, tuple): + if len(obj) < 2: + obj += (np.nan,) try: - dims_, data_, *attrs = obj + dims_, data_, *attrs_ = obj except ValueError as err: raise ValueError( - f"Tuple {obj} is not in the form (dims, data[, attrs])" + f"Tuple {obj} is not in the form (dims, [data[, attrs[, encoding]]])" ) from err if isinstance(data_, DataArray): @@ -139,12 +144,30 @@ def as_variable( f"Variable {name!r}: Using a DataArray object to construct a variable is" " ambiguous, please extract the data using the .data property." ) + + if utils.is_scalar(data_, include_0d=True): + try: + shape_ = tuple(sizes[i] for i in dims_) + except TypeError as err: + message = ( + f"Variable {name!r}: Could not convert tuple of form " + f"(dims, [data, [attrs, [encoding]]]): {obj} to Variable." + ) + raise ValueError(message) from err + except KeyError as err: + message = ( + f"Variable {name!r}: Provide `coords` with dimension(s) {dims_} to " + f"initialize with `np.full({dims_}, {data_!r})`." + ) + raise ValueError(message) from err + data_ = np.full(shape_, data_) + try: - obj = Variable(dims_, data_, *attrs) + obj = Variable(dims_, data_, *attrs_) except (TypeError, ValueError) as error: raise error.__class__( f"Variable {name!r}: Could not convert tuple of form " - f"(dims, data[, attrs, encoding]): {obj} to Variable." + f"(dims, [data, [attrs, [encoding]]]): {obj} to Variable." ) from error elif utils.is_scalar(obj): obj = Variable([], obj) diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index 8a90a05a4e3..6ccb15bcf7b 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -476,7 +476,7 @@ def test_constructor(self) -> None: with pytest.raises(ValueError, match=r"conflicting sizes"): Dataset({"a": x1, "b": x2}) - with pytest.raises(TypeError, match=r"tuple of form"): + with pytest.raises(ValueError, match=r"tuple of form"): Dataset({"x": (1, 2, 3, 4, 5, 6, 7)}) with pytest.raises(ValueError, match=r"already exists as a scalar"): Dataset({"x": 0, "y": ("x", [1, 2, 3])}) @@ -527,6 +527,51 @@ class Arbitrary: actual = Dataset({"x": arg}) assert_identical(expected, actual) + def test_constructor_scalar(self) -> None: + fill_value = np.nan + x = np.arange(2) + a = {"foo": "bar"} + + # a suitable `coords`` argument is required + with pytest.raises(ValueError): + Dataset({"f": (["x"], fill_value), "x": x}) + + # 1d coordinates + expected = Dataset( + { + "f": DataArray(fill_value, dims=["x"], coords={"x": x}), + }, + ) + for actual in ( + Dataset({"f": (["x"], fill_value)}, coords=expected.coords), + Dataset({"f": (["x"], fill_value)}, coords={"x": x}), + Dataset({"f": (["x"],)}, coords=expected.coords), + Dataset({"f": (["x"],)}, coords={"x": x}), + ): + assert_identical(expected, actual) + expected["f"].attrs.update(a) + actual = Dataset({"f": (["x"], fill_value, a)}, coords={"x": x}) + assert_identical(expected, actual) + + # 2d coordinates + yx = np.arange(6).reshape(2, -1) + try: + # TODO(itcarroll): aux coords broken in DataArray from scalar + array = DataArray( + fill_value, dims=["y", "x"], coords={"lat": (["y", "x"], yx)} + ) + expected = Dataset({"f": array}) + except ValueError: + expected = Dataset( + data_vars={"f": (["y", "x"], np.full(yx.shape, fill_value))}, + coords={"lat": (["y", "x"], yx)}, + ) + actual = Dataset( + {"f": (["y", "x"], fill_value)}, + coords=expected.coords, + ) + assert_identical(expected, actual) + def test_constructor_auto_align(self) -> None: a = DataArray([1, 2], [("x", [0, 1])]) b = DataArray([3, 4], [("x", [1, 2])]) diff --git a/xarray/tests/test_variable.py b/xarray/tests/test_variable.py index c283797bd08..889819d1d70 100644 --- a/xarray/tests/test_variable.py +++ b/xarray/tests/test_variable.py @@ -1220,7 +1220,7 @@ def test_as_variable(self): ) assert_identical(expected_extra, as_variable(xarray_tuple)) - with pytest.raises(TypeError, match=r"tuple of form"): + with pytest.raises(ValueError, match=r"tuple of form"): as_variable(tuple(data)) with pytest.raises(ValueError, match=r"tuple of form"): # GH1016 as_variable(("five", "six", "seven"))