diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 46f9d3bbfc9..184168e551a 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -73,6 +73,10 @@ Internal Changes ``xarray/testing/assertions`` for ``DataTree``. (:pull:`8967`) By `Owen Littlejohns `_ and `Tom Nicholas `_. +- ``transpose``, ``set_dims``, ``stack`` & ``unstack`` now use a ``dim`` kwarg + rather than ``dims`` or ``dimensions``. This is the final change to make xarray methods + consistent with their use of ``dim``. Using the existing kwarg will raise a + warning. By `Maximilian Roos `_ .. _whats-new.2024.03.0: diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py index 41c9af1bb10..d5aa4688ce1 100644 --- a/xarray/core/dataarray.py +++ b/xarray/core/dataarray.py @@ -3,6 +3,7 @@ import datetime import warnings from collections.abc import Hashable, Iterable, Mapping, MutableMapping, Sequence +from functools import partial from os import PathLike from typing import ( TYPE_CHECKING, @@ -2808,12 +2809,13 @@ def reorder_levels( ds = self._to_temp_dataset().reorder_levels(dim_order, **dim_order_kwargs) return self._from_temp_dataset(ds) + @partial(deprecate_dims, old_name="dimensions") def stack( self, - dimensions: Mapping[Any, Sequence[Hashable]] | None = None, + dim: Mapping[Any, Sequence[Hashable]] | None = None, create_index: bool | None = True, index_cls: type[Index] = PandasMultiIndex, - **dimensions_kwargs: Sequence[Hashable], + **dim_kwargs: Sequence[Hashable], ) -> Self: """ Stack any number of existing dimensions into a single new dimension. @@ -2823,7 +2825,7 @@ def stack( Parameters ---------- - dimensions : mapping of Hashable to sequence of Hashable + dim : mapping of Hashable to sequence of Hashable Mapping of the form `new_name=(dim1, dim2, ...)`. Names of new dimensions, and the existing dimensions that they replace. An ellipsis (`...`) will be replaced by all unlisted dimensions. @@ -2837,9 +2839,9 @@ def stack( index_cls: class, optional Can be used to pass a custom multi-index type. Must be an Xarray index that implements `.stack()`. By default, a pandas multi-index wrapper is used. - **dimensions_kwargs - The keyword arguments form of ``dimensions``. - One of dimensions or dimensions_kwargs must be provided. + **dim_kwargs + The keyword arguments form of ``dim``. + One of dim or dim_kwargs must be provided. Returns ------- @@ -2874,10 +2876,10 @@ def stack( DataArray.unstack """ ds = self._to_temp_dataset().stack( - dimensions, + dim, create_index=create_index, index_cls=index_cls, - **dimensions_kwargs, + **dim_kwargs, ) return self._from_temp_dataset(ds) @@ -3011,9 +3013,10 @@ def to_unstacked_dataset(self, dim: Hashable, level: int | Hashable = 0) -> Data # unstacked dataset return Dataset(data_dict) + @deprecate_dims def transpose( self, - *dims: Hashable, + *dim: Hashable, transpose_coords: bool = True, missing_dims: ErrorOptionsWithWarn = "raise", ) -> Self: @@ -3021,7 +3024,7 @@ def transpose( Parameters ---------- - *dims : Hashable, optional + *dim : Hashable, optional By default, reverse the dimensions. Otherwise, reorder the dimensions to this order. transpose_coords : bool, default: True @@ -3049,13 +3052,13 @@ def transpose( numpy.transpose Dataset.transpose """ - if dims: - dims = tuple(infix_dims(dims, self.dims, missing_dims)) - variable = self.variable.transpose(*dims) + if dim: + dim = tuple(infix_dims(dim, self.dims, missing_dims)) + variable = self.variable.transpose(*dim) if transpose_coords: coords: dict[Hashable, Variable] = {} for name, coord in self.coords.items(): - coord_dims = tuple(dim for dim in dims if dim in coord.dims) + coord_dims = tuple(d for d in dim if d in coord.dims) coords[name] = coord.variable.transpose(*coord_dims) return self._replace(variable, coords) else: diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 4f9125a1ab0..ffed9da1069 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -17,6 +17,7 @@ MutableMapping, Sequence, ) +from functools import partial from html import escape from numbers import Number from operator import methodcaller @@ -124,7 +125,7 @@ from xarray.namedarray.parallelcompat import get_chunked_array_type, guess_chunkmanager from xarray.namedarray.pycompat import array_type, is_chunked_array from xarray.plot.accessor import DatasetPlotAccessor -from xarray.util.deprecation_helpers import _deprecate_positional_args +from xarray.util.deprecation_helpers import _deprecate_positional_args, deprecate_dims if TYPE_CHECKING: from dask.dataframe import DataFrame as DaskDataFrame @@ -5264,12 +5265,13 @@ def _stack_once( new_variables, coord_names=new_coord_names, indexes=indexes ) + @partial(deprecate_dims, old_name="dimensions") def stack( self, - dimensions: Mapping[Any, Sequence[Hashable | ellipsis]] | None = None, + dim: Mapping[Any, Sequence[Hashable | ellipsis]] | None = None, create_index: bool | None = True, index_cls: type[Index] = PandasMultiIndex, - **dimensions_kwargs: Sequence[Hashable | ellipsis], + **dim_kwargs: Sequence[Hashable | ellipsis], ) -> Self: """ Stack any number of existing dimensions into a single new dimension. @@ -5279,7 +5281,7 @@ def stack( Parameters ---------- - dimensions : mapping of hashable to sequence of hashable + dim : mapping of hashable to sequence of hashable Mapping of the form `new_name=(dim1, dim2, ...)`. Names of new dimensions, and the existing dimensions that they replace. An ellipsis (`...`) will be replaced by all unlisted dimensions. @@ -5295,9 +5297,9 @@ def stack( index_cls: Index-class, default: PandasMultiIndex Can be used to pass a custom multi-index type (must be an Xarray index that implements `.stack()`). By default, a pandas multi-index wrapper is used. - **dimensions_kwargs - The keyword arguments form of ``dimensions``. - One of dimensions or dimensions_kwargs must be provided. + **dim_kwargs + The keyword arguments form of ``dim``. + One of dim or dim_kwargs must be provided. Returns ------- @@ -5308,9 +5310,9 @@ def stack( -------- Dataset.unstack """ - dimensions = either_dict_or_kwargs(dimensions, dimensions_kwargs, "stack") + dim = either_dict_or_kwargs(dim, dim_kwargs, "stack") result = self - for new_dim, dims in dimensions.items(): + for new_dim, dims in dim.items(): result = result._stack_once(dims, new_dim, index_cls, create_index) return result @@ -6218,9 +6220,10 @@ def drop_dims( drop_vars = {k for k, v in self._variables.items() if set(v.dims) & drop_dims} return self.drop_vars(drop_vars) + @deprecate_dims def transpose( self, - *dims: Hashable, + *dim: Hashable, missing_dims: ErrorOptionsWithWarn = "raise", ) -> Self: """Return a new Dataset object with all array dimensions transposed. @@ -6230,7 +6233,7 @@ def transpose( Parameters ---------- - *dims : hashable, optional + *dim : hashable, optional By default, reverse the dimensions on each array. Otherwise, reorder the dimensions to this order. missing_dims : {"raise", "warn", "ignore"}, default: "raise" @@ -6257,20 +6260,20 @@ def transpose( numpy.transpose DataArray.transpose """ - # Raise error if list is passed as dims - if (len(dims) > 0) and (isinstance(dims[0], list)): - list_fix = [f"{repr(x)}" if isinstance(x, str) else f"{x}" for x in dims[0]] + # Raise error if list is passed as dim + if (len(dim) > 0) and (isinstance(dim[0], list)): + list_fix = [f"{repr(x)}" if isinstance(x, str) else f"{x}" for x in dim[0]] raise TypeError( - f'transpose requires dims to be passed as multiple arguments. Expected `{", ".join(list_fix)}`. Received `{dims[0]}` instead' + f'transpose requires dim to be passed as multiple arguments. Expected `{", ".join(list_fix)}`. Received `{dim[0]}` instead' ) # Use infix_dims to check once for missing dimensions - if len(dims) != 0: - _ = list(infix_dims(dims, self.dims, missing_dims)) + if len(dim) != 0: + _ = list(infix_dims(dim, self.dims, missing_dims)) ds = self.copy() for name, var in self._variables.items(): - var_dims = tuple(dim for dim in dims if dim in (var.dims + (...,))) + var_dims = tuple(d for d in dim if d in (var.dims + (...,))) ds._variables[name] = var.transpose(*var_dims) return ds diff --git a/xarray/core/variable.py b/xarray/core/variable.py index 2229eaa2d24..2bcee5590f8 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -45,6 +45,7 @@ ) from xarray.namedarray.core import NamedArray, _raise_if_any_duplicate_dimensions from xarray.namedarray.pycompat import integer_types, is_0d_dask_array, to_duck_array +from xarray.util.deprecation_helpers import deprecate_dims NON_NUMPY_SUPPORTED_ARRAY_TYPES = ( indexing.ExplicitlyIndexed, @@ -1283,16 +1284,17 @@ def roll(self, shifts=None, **shifts_kwargs): result = result._roll_one_dim(dim, count) return result + @deprecate_dims def transpose( self, - *dims: Hashable | ellipsis, + *dim: Hashable | ellipsis, missing_dims: ErrorOptionsWithWarn = "raise", ) -> Self: """Return a new Variable object with transposed dimensions. Parameters ---------- - *dims : Hashable, optional + *dim : Hashable, optional By default, reverse the dimensions. Otherwise, reorder the dimensions to this order. missing_dims : {"raise", "warn", "ignore"}, default: "raise" @@ -1317,25 +1319,26 @@ def transpose( -------- numpy.transpose """ - if len(dims) == 0: - dims = self.dims[::-1] + if len(dim) == 0: + dim = self.dims[::-1] else: - dims = tuple(infix_dims(dims, self.dims, missing_dims)) + dim = tuple(infix_dims(dim, self.dims, missing_dims)) - if len(dims) < 2 or dims == self.dims: + if len(dim) < 2 or dim == self.dims: # no need to transpose if only one dimension # or dims are in same order return self.copy(deep=False) - axes = self.get_axis_num(dims) + axes = self.get_axis_num(dim) data = as_indexable(self._data).transpose(axes) - return self._replace(dims=dims, data=data) + return self._replace(dims=dim, data=data) @property def T(self) -> Self: return self.transpose() - def set_dims(self, dims, shape=None): + @deprecate_dims + def set_dims(self, dim, shape=None): """Return a new variable with given set of dimensions. This method might be used to attach new dimension(s) to variable. @@ -1343,7 +1346,7 @@ def set_dims(self, dims, shape=None): Parameters ---------- - dims : str or sequence of str or dict + dim : str or sequence of str or dict Dimensions to include on the new variable. If a dict, values are used to provide the sizes of new dimensions; otherwise, new dimensions are inserted with length 1. @@ -1352,28 +1355,28 @@ def set_dims(self, dims, shape=None): ------- Variable """ - if isinstance(dims, str): - dims = [dims] + if isinstance(dim, str): + dim = [dim] - if shape is None and is_dict_like(dims): - shape = dims.values() + if shape is None and is_dict_like(dim): + shape = dim.values() - missing_dims = set(self.dims) - set(dims) + missing_dims = set(self.dims) - set(dim) if missing_dims: raise ValueError( - f"new dimensions {dims!r} must be a superset of " + f"new dimensions {dim!r} must be a superset of " f"existing dimensions {self.dims!r}" ) self_dims = set(self.dims) - expanded_dims = tuple(d for d in dims if d not in self_dims) + self.dims + expanded_dims = tuple(d for d in dim if d not in self_dims) + self.dims if self.dims == expanded_dims: # don't use broadcast_to unless necessary so the result remains # writeable if possible expanded_data = self.data elif shape is not None: - dims_map = dict(zip(dims, shape)) + dims_map = dict(zip(dim, shape)) tmp_shape = tuple(dims_map[d] for d in expanded_dims) expanded_data = duck_array_ops.broadcast_to(self.data, tmp_shape) else: @@ -1383,11 +1386,11 @@ def set_dims(self, dims, shape=None): expanded_var = Variable( expanded_dims, expanded_data, self._attrs, self._encoding, fastpath=True ) - return expanded_var.transpose(*dims) + return expanded_var.transpose(*dim) - def _stack_once(self, dims: list[Hashable], new_dim: Hashable): - if not set(dims) <= set(self.dims): - raise ValueError(f"invalid existing dimensions: {dims}") + def _stack_once(self, dim: list[Hashable], new_dim: Hashable): + if not set(dim) <= set(self.dims): + raise ValueError(f"invalid existing dimensions: {dim}") if new_dim in self.dims: raise ValueError( @@ -1395,12 +1398,12 @@ def _stack_once(self, dims: list[Hashable], new_dim: Hashable): "name as an existing dimension" ) - if len(dims) == 0: + if len(dim) == 0: # don't stack return self.copy(deep=False) - other_dims = [d for d in self.dims if d not in dims] - dim_order = other_dims + list(dims) + other_dims = [d for d in self.dims if d not in dim] + dim_order = other_dims + list(dim) reordered = self.transpose(*dim_order) new_shape = reordered.shape[: len(other_dims)] + (-1,) @@ -1411,22 +1414,23 @@ def _stack_once(self, dims: list[Hashable], new_dim: Hashable): new_dims, new_data, self._attrs, self._encoding, fastpath=True ) - def stack(self, dimensions=None, **dimensions_kwargs): + @partial(deprecate_dims, old_name="dimensions") + def stack(self, dim=None, **dim_kwargs): """ - Stack any number of existing dimensions into a single new dimension. + Stack any number of existing dim into a single new dimension. - New dimensions will be added at the end, and the order of the data + New dim will be added at the end, and the order of the data along each new dimension will be in contiguous (C) order. Parameters ---------- - dimensions : mapping of hashable to tuple of hashable + dim : mapping of hashable to tuple of hashable Mapping of form new_name=(dim1, dim2, ...) describing the - names of new dimensions, and the existing dimensions that + names of new dim, and the existing dim that they replace. - **dimensions_kwargs - The keyword arguments form of ``dimensions``. - One of dimensions or dimensions_kwargs must be provided. + **dim_kwargs + The keyword arguments form of ``dim``. + One of dim or dim_kwargs must be provided. Returns ------- @@ -1437,9 +1441,9 @@ def stack(self, dimensions=None, **dimensions_kwargs): -------- Variable.unstack """ - dimensions = either_dict_or_kwargs(dimensions, dimensions_kwargs, "stack") + dim = either_dict_or_kwargs(dim, dim_kwargs, "stack") result = self - for new_dim, dims in dimensions.items(): + for new_dim, dims in dim.items(): result = result._stack_once(dims, new_dim) return result @@ -1548,7 +1552,8 @@ def _unstack_once( return self._replace(dims=new_dims, data=data) - def unstack(self, dimensions=None, **dimensions_kwargs): + @partial(deprecate_dims, old_name="dimensions") + def unstack(self, dim=None, **dim_kwargs): """ Unstack an existing dimension into multiple new dimensions. @@ -1561,13 +1566,13 @@ def unstack(self, dimensions=None, **dimensions_kwargs): Parameters ---------- - dimensions : mapping of hashable to mapping of hashable to int + dim : mapping of hashable to mapping of hashable to int Mapping of the form old_dim={dim1: size1, ...} describing the names of existing dimensions, and the new dimensions and sizes that they map to. - **dimensions_kwargs - The keyword arguments form of ``dimensions``. - One of dimensions or dimensions_kwargs must be provided. + **dim_kwargs + The keyword arguments form of ``dim``. + One of dim or dim_kwargs must be provided. Returns ------- @@ -1580,9 +1585,9 @@ def unstack(self, dimensions=None, **dimensions_kwargs): DataArray.unstack Dataset.unstack """ - dimensions = either_dict_or_kwargs(dimensions, dimensions_kwargs, "unstack") + dim = either_dict_or_kwargs(dim, dim_kwargs, "unstack") result = self - for old_dim, dims in dimensions.items(): + for old_dim, dims in dim.items(): result = result._unstack_once_full(dims, old_dim) return result diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py index 26a4268c8b7..4e916d62155 100644 --- a/xarray/tests/test_dataarray.py +++ b/xarray/tests/test_dataarray.py @@ -2484,7 +2484,7 @@ def test_stack_unstack(self) -> None: assert_identical(orig, orig.unstack()) # test GH3000 - a = orig[:0, :1].stack(dim=("x", "y")).indexes["dim"] + a = orig[:0, :1].stack(new_dim=("x", "y")).indexes["new_dim"] b = pd.MultiIndex( levels=[pd.Index([], np.int64), pd.Index([0], np.int64)], codes=[[], []], @@ -7135,7 +7135,7 @@ def test_result_as_expected(self) -> None: def test_error_on_ellipsis_without_list(self) -> None: da = DataArray([[1, 2], [1, 2]], dims=("x", "y")) with pytest.raises(ValueError): - da.stack(flat=...) # type: ignore + da.stack(flat=...) def test_nD_coord_dataarray() -> None: diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index 40cf85484da..301596e032f 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -7403,7 +7403,7 @@ def test_transpose_error() -> None: with pytest.raises( TypeError, match=re.escape( - "transpose requires dims to be passed as multiple arguments. Expected `'y', 'x'`. Received `['y', 'x']` instead" + "transpose requires dim to be passed as multiple arguments. Expected `'y', 'x'`. Received `['y', 'x']` instead" ), ): ds.transpose(["y", "x"]) # type: ignore diff --git a/xarray/tests/test_sparse.py b/xarray/tests/test_sparse.py index 09c12818754..f0a97fc7e69 100644 --- a/xarray/tests/test_sparse.py +++ b/xarray/tests/test_sparse.py @@ -109,11 +109,11 @@ def test_variable_property(prop): (do("notnull"), True), (do("roll"), True), (do("round"), True), - (do("set_dims", dims=("x", "y", "z")), True), - (do("stack", dimensions={"flat": ("x", "y")}), True), + (do("set_dims", dim=("x", "y", "z")), True), + (do("stack", dim={"flat": ("x", "y")}), True), (do("to_base_variable"), True), (do("transpose"), True), - (do("unstack", dimensions={"x": {"x1": 5, "x2": 2}}), True), + (do("unstack", dim={"x": {"x1": 5, "x2": 2}}), True), (do("broadcast_equals", make_xrvar({"x": 10, "y": 5})), False), (do("equals", make_xrvar({"x": 10, "y": 5})), False), (do("identical", make_xrvar({"x": 10, "y": 5})), False), diff --git a/xarray/util/deprecation_helpers.py b/xarray/util/deprecation_helpers.py index c620e45574e..3d52253ed6e 100644 --- a/xarray/util/deprecation_helpers.py +++ b/xarray/util/deprecation_helpers.py @@ -119,7 +119,7 @@ def inner(*args, **kwargs): return _decorator -def deprecate_dims(func: T) -> T: +def deprecate_dims(func: T, old_name="dims") -> T: """ For functions that previously took `dims` as a kwarg, and have now transitioned to `dim`. This decorator will issue a warning if `dims` is passed while forwarding it @@ -128,15 +128,15 @@ def deprecate_dims(func: T) -> T: @wraps(func) def wrapper(*args, **kwargs): - if "dims" in kwargs: + if old_name in kwargs: emit_user_level_warning( - "The `dims` argument has been renamed to `dim`, and will be removed " + f"The `{old_name}` argument has been renamed to `dim`, and will be removed " "in the future. This renaming is taking place throughout xarray over the " "next few releases.", # Upgrade to `DeprecationWarning` in the future, when the renaming is complete. PendingDeprecationWarning, ) - kwargs["dim"] = kwargs.pop("dims") + kwargs["dim"] = kwargs.pop(old_name) return func(*args, **kwargs) # We're quite confident we're just returning `T` from this function, so it's fine to ignore typing