Repository: pydata/xarray

Issue:
`dim` vs `dims`
### What is your issue?

I've recently been hit with this when experimenting with `xr.dot` and `xr.corr` — `xr.dot` takes `dims`, and `xr.cov` takes `dim`. Because they each take multiple arrays as positional args, kwargs are more conventional.

Should we standardize on one of these?

Discussion:

Patch:
diff --git a/.github/workflows/ci-additional.yaml b/.github/workflows/ci-additional.yaml
index 0fcba9d5..7b248b14 100644
--- a/.github/workflows/ci-additional.yaml
+++ b/.github/workflows/ci-additional.yaml
@@ -41,7 +41,7 @@ jobs:
         shell: bash -l {0}
     env:
       CONDA_ENV_FILE: ci/requirements/environment.yml
-      PYTHON_VERSION: "3.11"
+      PYTHON_VERSION: "3.12"
     steps:
       - uses: actions/checkout@v4
         with:
@@ -205,7 +205,7 @@ jobs:
         shell: bash -l {0}
     env:
       CONDA_ENV_FILE: ci/requirements/environment.yml
-      PYTHON_VERSION: "3.10"
+      PYTHON_VERSION: "3.12"
 
     steps:
       - uses: actions/checkout@v4
@@ -330,7 +330,7 @@ jobs:
         with:
           environment-name: xarray-tests
           create-args: >-
-            python=3.11
+            python=3.12
             pyyaml
             conda
             python-dateutil
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index e970bc8e..a577312a 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -44,7 +44,7 @@ jobs:
       matrix:
         os: ["ubuntu-latest", "macos-latest", "windows-latest"]
         # Bookend python versions
-        python-version: ["3.9", "3.11", "3.12"]
+        python-version: ["3.9",  "3.12"]
         env: [""]
         include:
           # Minimum python version:
@@ -56,10 +56,11 @@ jobs:
             os: ubuntu-latest
           # Latest python version:
           - env: "all-but-dask"
-            python-version: "3.10"
+            # Not 3.12 because of pint
+            python-version: "3.11"
             os: ubuntu-latest
           - env: "flaky"
-            python-version: "3.10"
+            python-version: "3.12"
             os: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
@@ -71,30 +72,30 @@ jobs:
 
           if [[ ${{ matrix.os }} == windows* ]] ;
           then
-            if [[ ${{ matrix.python-version }} != "3.12" ]]; then
+            if [[ ${{ matrix.python-version }} != "3.13" ]]; then
               echo "CONDA_ENV_FILE=ci/requirements/environment-windows.yml" >> $GITHUB_ENV
             else
-              echo "CONDA_ENV_FILE=ci/requirements/environment-windows-3.12.yml" >> $GITHUB_ENV
+              echo "CONDA_ENV_FILE=ci/requirements/environment-windows-3.13.yml" >> $GITHUB_ENV
             fi
           elif [[ "${{ matrix.env }}" != "" ]] ;
           then
             if [[ "${{ matrix.env }}" == "flaky" ]] ;
             then
               echo "CONDA_ENV_FILE=ci/requirements/environment.yml" >> $GITHUB_ENV
-              echo "PYTEST_EXTRA_FLAGS=--run-flaky --run-network-tests -W default" >> $GITHUB_ENV
+              echo "PYTEST_ADDOPTS=-m 'flaky or network' --run-flaky --run-network-tests -W default" >> $GITHUB_ENV
             else
               echo "CONDA_ENV_FILE=ci/requirements/${{ matrix.env }}.yml" >> $GITHUB_ENV
             fi
             if [[ "${{ matrix.env }}" == "min-all-deps" ]] ;
             then
               # Don't raise on warnings
-              echo "PYTEST_EXTRA_FLAGS=-W default" >> $GITHUB_ENV
+              echo "PYTEST_ADDOPTS=-W default" >> $GITHUB_ENV
             fi
           else
-            if [[ ${{ matrix.python-version }} != "3.12" ]]; then
+            if [[ ${{ matrix.python-version }} != "3.13" ]]; then
               echo "CONDA_ENV_FILE=ci/requirements/environment.yml" >> $GITHUB_ENV
             else
-              echo "CONDA_ENV_FILE=ci/requirements/environment-3.12.yml" >> $GITHUB_ENV
+              echo "CONDA_ENV_FILE=ci/requirements/environment-3.13.yml" >> $GITHUB_ENV
             fi
           fi
 
@@ -114,7 +115,7 @@ jobs:
       # We only want to install this on one run, because otherwise we'll have
       # duplicate annotations.
       - name: Install error reporter
-        if: ${{ matrix.os }} == 'ubuntu-latest' and ${{ matrix.python-version }} == '3.10'
+        if: ${{ matrix.os }} == 'ubuntu-latest' and ${{ matrix.python-version }} == '3.12'
         run: |
           python -m pip install pytest-github-actions-annotate-failures
 
@@ -146,7 +147,6 @@ jobs:
           --cov=xarray
           --cov-report=xml
           --junitxml=pytest.xml
-          $PYTEST_EXTRA_FLAGS
 
       - name: Upload test results
         if: always()
diff --git a/.github/workflows/pypi-release.yaml b/.github/workflows/pypi-release.yaml
index 354a8b59..e7aec6e8 100644
--- a/.github/workflows/pypi-release.yaml
+++ b/.github/workflows/pypi-release.yaml
@@ -18,7 +18,7 @@ jobs:
       - uses: actions/setup-python@v5
         name: Install Python
         with:
-          python-version: "3.11"
+          python-version: "3.12"
 
       - name: Install dependencies
         run: |
@@ -53,7 +53,7 @@ jobs:
       - uses: actions/setup-python@v5
         name: Install Python
         with:
-          python-version: "3.11"
+          python-version: "3.12"
       - uses: actions/download-artifact@v4
         with:
           name: releases
diff --git a/ci/install-upstream-wheels.sh b/ci/install-upstream-wheels.sh
index d9c797e2..af4b68b0 100755
--- a/ci/install-upstream-wheels.sh
+++ b/ci/install-upstream-wheels.sh
@@ -1,13 +1,13 @@
 #!/usr/bin/env bash
 
 # install cython for building cftime without build isolation
-micromamba install "cython>=0.29.20" py-cpuinfo
+micromamba install "cython>=0.29.20" py-cpuinfo setuptools-scm
 # temporarily (?) remove numbagg and numba
 micromamba remove -y numba numbagg sparse
 # temporarily remove numexpr
 micromamba remove -y numexpr
 # temporarily remove backends
-micromamba remove -y cf_units hdf5 h5py netcdf4
+micromamba remove -y cf_units hdf5 h5py netcdf4 pydap
 # forcibly remove packages to avoid artifacts
 micromamba remove -y --force \
     numpy \
@@ -31,7 +31,8 @@ python -m pip install \
     numpy \
     scipy \
     matplotlib \
-    pandas
+    pandas \
+    h5py
 # for some reason pandas depends on pyarrow already.
 # Remove once a `pyarrow` version compiled with `numpy>=2.0` is on `conda-forge`
 python -m pip install \
@@ -70,6 +71,6 @@ python -m pip install \
     git+https://github.com/intake/filesystem_spec \
     git+https://github.com/SciTools/nc-time-axis \
     git+https://github.com/xarray-contrib/flox \
+    git+https://github.com/h5netcdf/h5netcdf \
     git+https://github.com/dgasmith/opt_einsum
     # git+https://github.com/pydata/sparse
-    # git+https://github.com/h5netcdf/h5netcdf
diff --git a/ci/requirements/environment-3.12.yml b/ci/requirements/environment-3.13.yml
similarity index 100%
rename from ci/requirements/environment-3.12.yml
rename to ci/requirements/environment-3.13.yml
diff --git a/ci/requirements/environment-windows-3.12.yml b/ci/requirements/environment-windows-3.13.yml
similarity index 100%
rename from ci/requirements/environment-windows-3.12.yml
rename to ci/requirements/environment-windows-3.13.yml
diff --git a/ci/requirements/environment-windows.yml b/ci/requirements/environment-windows.yml
index c1027b52..3b2e6dc6 100644
--- a/ci/requirements/environment-windows.yml
+++ b/ci/requirements/environment-windows.yml
@@ -26,7 +26,7 @@ dependencies:
   - numpy
   - packaging
   - pandas
-  - pint>=0.22
+  # - pint>=0.22
   - pip
   - pre-commit
   - pydap
diff --git a/ci/requirements/environment.yml b/ci/requirements/environment.yml
index d3dbc088..01521e95 100644
--- a/ci/requirements/environment.yml
+++ b/ci/requirements/environment.yml
@@ -30,7 +30,7 @@ dependencies:
   - opt_einsum
   - packaging
   - pandas
-  - pint>=0.22
+  # - pint>=0.22
   - pip
   - pooch
   - pre-commit
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 46f9d3bb..184168e5 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 <https://github.com/owenlittlejohns>`_ and
   `Tom Nicholas <https://github.com/TomNicholas>`_.
+- ``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 <https://github.com/max-sixty>`_
 
 
 .. _whats-new.2024.03.0:
diff --git a/pyproject.toml b/pyproject.toml
index 00348ba7..d2b4b57e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -117,13 +117,13 @@ module = [
   "iris.*",
   "matplotlib.*",
   "mpl_toolkits.*",
-  "Nio.*",
   "nc_time_axis.*",
   "numbagg.*",
   "netCDF4.*",
   "netcdftime.*",
   "opt_einsum.*",
   "pandas.*",
+  "pint.*",
   "pooch.*",
   "pyarrow.*",
   "pydap.*",
diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py
index 41c9af1b..d5aa4688 100644
--- a/xarray/core/dataarray.py
+++ b/xarray/core/dataarray.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 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 @@ class DataArray(
         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 @@ class DataArray(
 
         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 @@ class DataArray(
         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 @@ class DataArray(
         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 @@ class DataArray(
         # 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 @@ class DataArray(
 
         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 @@ class DataArray(
         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 4f9125a1..ffed9da1 100644
--- a/xarray/core/dataset.py
+++ b/xarray/core/dataset.py
@@ -17,6 +17,7 @@ from collections.abc import (
     MutableMapping,
     Sequence,
 )
+from functools import partial
 from html import escape
 from numbers import Number
 from operator import methodcaller
@@ -124,7 +125,7 @@ from xarray.core.variable import (
 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 @@ class Dataset(
             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 @@ class Dataset(
 
         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 @@ class Dataset(
         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 @@ class Dataset(
         --------
         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 @@ class Dataset(
         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 @@ class Dataset(
 
         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 @@ class Dataset(
         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 2229eaa2..2bcee559 100644
--- a/xarray/core/variable.py
+++ b/xarray/core/variable.py
@@ -45,6 +45,7 @@ from xarray.core.utils import (
 )
 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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
             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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
         --------
         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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
 
         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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
         -------
         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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
         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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
                 "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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
             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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
         --------
         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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
 
         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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
 
         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 @@ class Variable(NamedArray, AbstractArray, VariableArithmetic):
         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/util/deprecation_helpers.py b/xarray/util/deprecation_helpers.py
index c620e455..3d52253e 100644
--- a/xarray/util/deprecation_helpers.py
+++ b/xarray/util/deprecation_helpers.py
@@ -119,7 +119,7 @@ def _deprecate_positional_args(version) -> Callable[[T], T]:
     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
diff --git a/xarray/util/print_versions.py b/xarray/util/print_versions.py
index 4c715437..0b2e2b05 100755
--- a/xarray/util/print_versions.py
+++ b/xarray/util/print_versions.py
@@ -104,7 +104,6 @@ def show_versions(file=sys.stdout):
         ("pydap", lambda mod: mod.__version__),
         ("h5netcdf", lambda mod: mod.__version__),
         ("h5py", lambda mod: mod.__version__),
-        ("Nio", lambda mod: mod.__version__),
         ("zarr", lambda mod: mod.__version__),
         ("cftime", lambda mod: mod.__version__),
         ("nc_time_axis", lambda mod: mod.__version__),


Test patch:
diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py
index 7ea02eaf..c202e191 100644
--- a/xarray/tests/__init__.py
+++ b/xarray/tests/__init__.py
@@ -142,8 +142,36 @@ requires_numbagg_or_bottleneck = pytest.mark.skipif(
     not has_scipy_or_netCDF4, reason="requires scipy or netCDF4"
 )
 has_numpy_array_api, requires_numpy_array_api = _importorskip("numpy", "1.26.0")
-has_h5netcdf_ros3, requires_h5netcdf_ros3 = _importorskip("h5netcdf", "1.3.0")
 
+
+def _importorskip_h5netcdf_ros3():
+    try:
+        import h5netcdf
+
+        has_h5netcdf = True
+    except ImportError:
+        has_h5netcdf = False
+
+    if not has_h5netcdf:
+        return has_h5netcdf, pytest.mark.skipif(
+            not has_h5netcdf, reason="requires h5netcdf"
+        )
+
+    h5netcdf_with_ros3 = Version(h5netcdf.__version__) >= Version("1.3.0")
+
+    import h5py
+
+    h5py_with_ros3 = h5py.get_config().ros3
+
+    has_h5netcdf_ros3 = h5netcdf_with_ros3 and h5py_with_ros3
+
+    return has_h5netcdf_ros3, pytest.mark.skipif(
+        not has_h5netcdf_ros3,
+        reason="requires h5netcdf>=1.3.0 and h5py with ros3 support",
+    )
+
+
+has_h5netcdf_ros3, requires_h5netcdf_ros3 = _importorskip_h5netcdf_ros3()
 has_netCDF4_1_6_2_or_above, requires_netCDF4_1_6_2_or_above = _importorskip(
     "netCDF4", "1.6.2"
 )
diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py
index bfa26025..fdf181b5 100644
--- a/xarray/tests/test_backends.py
+++ b/xarray/tests/test_backends.py
@@ -2988,14 +2988,6 @@ class ZarrBase(CFEncodedBase):
                 assert original[name].chunks == actual_var.chunks
             assert original.chunks == actual.chunks
 
-    def test_vectorized_indexing_negative_step(self) -> None:
-        if not has_dask:
-            pytest.xfail(
-                reason="zarr without dask handles negative steps in slices incorrectly"
-            )
-
-        super().test_vectorized_indexing_negative_step()
-
 
 @requires_zarr
 class TestZarrDictStore(ZarrBase):
@@ -3821,6 +3813,7 @@ def skip_if_not_engine(engine):
         pytest.importorskip(engine)
 
 
+# Flaky test. Very open to contributions on fixing this
 @requires_dask
 @pytest.mark.filterwarnings("ignore:use make_scale(name) instead")
 @pytest.mark.xfail(reason="Flaky test. Very open to contributions on fixing this")
@@ -4523,7 +4516,8 @@ class TestDask(DatasetIOBase):
             ) as actual:
                 assert_identical(expected, actual)
 
-    @pytest.mark.xfail(reason="Flaky test. Very open to contributions on fixing this")
+    # Flaky test. Very open to contributions on fixing this
+    @pytest.mark.flaky
     def test_dask_roundtrip(self) -> None:
         with create_tmp_file() as tmp:
             data = create_test_data()
diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py
index 26a4268c..4e916d62 100644
--- a/xarray/tests/test_dataarray.py
+++ b/xarray/tests/test_dataarray.py
@@ -2484,7 +2484,7 @@ class TestDataArray:
         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 @@ class TestStackEllipsis:
     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 40cf8548..301596e0 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_formatting.py b/xarray/tests/test_formatting.py
index 256a02d4..cb765ce9 100644
--- a/xarray/tests/test_formatting.py
+++ b/xarray/tests/test_formatting.py
@@ -6,7 +6,6 @@ from textwrap import dedent
 import numpy as np
 import pandas as pd
 import pytest
-from numpy.core import defchararray
 
 import xarray as xr
 from xarray.core import formatting
@@ -770,9 +769,9 @@ def test_repr_file_collapsed(tmp_path) -> None:
 )
 def test__mapping_repr(display_max_rows, n_vars, n_attr) -> None:
     long_name = "long_name"
-    a = defchararray.add(long_name, np.arange(0, n_vars).astype(str))
-    b = defchararray.add("attr_", np.arange(0, n_attr).astype(str))
-    c = defchararray.add("coord", np.arange(0, n_vars).astype(str))
+    a = np.char.add(long_name, np.arange(0, n_vars).astype(str))
+    b = np.char.add("attr_", np.arange(0, n_attr).astype(str))
+    c = np.char.add("coord", np.arange(0, n_vars).astype(str))
     attrs = {k: 2 for k in b}
     coords = {_c: np.array([0, 1], dtype=np.uint64) for _c in c}
     data_vars = dict()
diff --git a/xarray/tests/test_plugins.py b/xarray/tests/test_plugins.py
index 8e1eb616..46051f04 100644
--- a/xarray/tests/test_plugins.py
+++ b/xarray/tests/test_plugins.py
@@ -228,7 +228,6 @@ def test_lazy_import() -> None:
         "matplotlib",
         "nc_time_axis",
         "netCDF4",
-        "Nio",
         "numbagg",
         "pint",
         "pydap",
diff --git a/xarray/tests/test_sparse.py b/xarray/tests/test_sparse.py
index 09c12818..f0a97fc7 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),