Repository: pydata/xarray

Issue:
Can't select time with str after converting the calendar to noleap
### What happened?

I am trying to select a time period after converting the calendar to `noleap` and I receive the following error:

`TypeError: '<' not supported between instances of 'cftime._cftime.DatetimeNoLeap' and 'str'`

### What did you expect to happen?

I expected the slicing to work as normal.

### Minimal Complete Verifiable Example

```Python
import xarray as xr
import pandas as pd
import numpy as np
time=xr.cftime_range('2000-01-01','2006-01-01', freq='D', calendar='noleap')
temperature =  np.ones(len(time))
da = xr.DataArray(
    data=temperature,
    dims=["time"],
    coords=dict(time=time,),
)
out=da.sel(time=slice('2001','2002'))  # works if array is built with cftime
da1=da.convert_calendar('noleap')
out1=da1.sel(time=slice('2001','2002'))  # works if array is built with cftime and converted

time=pd.date_range('2000-01-01','2006-01-01', freq='D',)
temperature =  np.ones(len(time))
da2 = xr.DataArray(
    data=temperature,
    dims=["time"],
    coords=dict(time=time,),
)
out2=da2.sel(time=slice('2001','2002')) #  # works if array is built with pandas time
da3=da2.convert_calendar('noleap')
out3=da3.sel(time=slice('2001','2002')) # fails if array is built with pandas time and convert to noleap
```


### 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

```Python
KeyError                                  Traceback (most recent call last)
File /srv/conda/envs/notebook/lib/python3.10/site-packages/pandas/core/indexes/base.py:3805, in Index.get_loc(self, key)
   3804 try:
-> 3805     return self._engine.get_loc(casted_key)
   3806 except KeyError as err:

File index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7081, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7089, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: '2001'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
File /srv/conda/envs/notebook/lib/python3.10/site-packages/pandas/core/indexes/base.py:6798, in Index.get_slice_bound(self, label, side)
   6797 try:
-> 6798     slc = self.get_loc(label)
   6799 except KeyError as err:

File /srv/conda/envs/notebook/lib/python3.10/site-packages/pandas/core/indexes/base.py:3812, in Index.get_loc(self, key)
   3811         raise InvalidIndexError(key)
-> 3812     raise KeyError(key) from err
   3813 except TypeError:
   3814     # If we have a listlike key, _check_indexing_error will raise
   3815     #  InvalidIndexError. Otherwise we fall through and re-raise
   3816     #  the TypeError.

KeyError: '2001'

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
Cell In[1], line 24
     22 out2=da2.sel(time=slice('2001','2002')) #  # works if array is built with pandas time
     23 da3=da2.convert_calendar('noleap')
---> 24 out3=da3.sel(time=slice('2001','2002')) # fails if array is built with pandas time and convert to noleap

File /srv/conda/envs/notebook/lib/python3.10/site-packages/xarray/core/dataarray.py:1644, in DataArray.sel(self, indexers, method, tolerance, drop, **indexers_kwargs)
   1528 def sel(
   1529     self,
   1530     indexers: Mapping[Any, Any] | None = None,
   (...)
   1534     **indexers_kwargs: Any,
   1535 ) -> Self:
   1536     """Return a new DataArray whose data is given by selecting index
   1537     labels along the specified dimension(s).
   1538 
   (...)
   1642     Dimensions without coordinates: points
   1643     """
-> 1644     ds = self._to_temp_dataset().sel(
   1645         indexers=indexers,
   1646         drop=drop,
   1647         method=method,
   1648         tolerance=tolerance,
   1649         **indexers_kwargs,
   1650     )
   1651     return self._from_temp_dataset(ds)

File /srv/conda/envs/notebook/lib/python3.10/site-packages/xarray/core/dataset.py:3126, in Dataset.sel(self, indexers, method, tolerance, drop, **indexers_kwargs)
   3058 """Returns a new dataset with each array indexed by tick labels
   3059 along the specified dimension(s).
   3060 
   (...)
   3123 
   3124 """
   3125 indexers = either_dict_or_kwargs(indexers, indexers_kwargs, "sel")
-> 3126 query_results = map_index_queries(
   3127     self, indexers=indexers, method=method, tolerance=tolerance
   3128 )
   3130 if drop:
   3131     no_scalar_variables = {}

File /srv/conda/envs/notebook/lib/python3.10/site-packages/xarray/core/indexing.py:192, in map_index_queries(obj, indexers, method, tolerance, **indexers_kwargs)
    190         results.append(IndexSelResult(labels))
    191     else:
--> 192         results.append(index.sel(labels, **options))
    194 merged = merge_sel_results(results)
    196 # drop dimension coordinates found in dimension indexers
    197 # (also drop multi-index if any)
    198 # (.sel() already ensures alignment)

File /srv/conda/envs/notebook/lib/python3.10/site-packages/xarray/core/indexes.py:758, in PandasIndex.sel(self, labels, method, tolerance)
    755 coord_name, label = next(iter(labels.items()))
    757 if isinstance(label, slice):
--> 758     indexer = _query_slice(self.index, label, coord_name, method, tolerance)
    759 elif is_dict_like(label):
    760     raise ValueError(
    761         "cannot use a dict-like object for selection on "
    762         "a dimension that does not have a MultiIndex"
    763     )

File /srv/conda/envs/notebook/lib/python3.10/site-packages/xarray/core/indexes.py:497, in _query_slice(index, label, coord_name, method, tolerance)
    493 if method is not None or tolerance is not None:
    494     raise NotImplementedError(
    495         "cannot use ``method`` argument if any indexers are slice objects"
    496     )
--> 497 indexer = index.slice_indexer(
    498     _sanitize_slice_element(label.start),
    499     _sanitize_slice_element(label.stop),
    500     _sanitize_slice_element(label.step),
    501 )
    502 if not isinstance(indexer, slice):
    503     # unlike pandas, in xarray we never want to silently convert a
    504     # slice indexer into an array indexer
    505     raise KeyError(
    506         "cannot represent labeled-based slice indexer for coordinate "
    507         f"{coord_name!r} with a slice over integer positions; the index is "
    508         "unsorted or non-unique"
    509     )

File /srv/conda/envs/notebook/lib/python3.10/site-packages/pandas/core/indexes/base.py:6662, in Index.slice_indexer(self, start, end, step)
   6618 def slice_indexer(
   6619     self,
   6620     start: Hashable | None = None,
   6621     end: Hashable | None = None,
   6622     step: int | None = None,
   6623 ) -> slice:
   6624     """
   6625     Compute the slice indexer for input labels and step.
   6626 
   (...)
   6660     slice(1, 3, None)
   6661     """
-> 6662     start_slice, end_slice = self.slice_locs(start, end, step=step)
   6664     # return a slice
   6665     if not is_scalar(start_slice):

File /srv/conda/envs/notebook/lib/python3.10/site-packages/pandas/core/indexes/base.py:6879, in Index.slice_locs(self, start, end, step)
   6877 start_slice = None
   6878 if start is not None:
-> 6879     start_slice = self.get_slice_bound(start, "left")
   6880 if start_slice is None:
   6881     start_slice = 0

File /srv/conda/envs/notebook/lib/python3.10/site-packages/pandas/core/indexes/base.py:6801, in Index.get_slice_bound(self, label, side)
   6799 except KeyError as err:
   6800     try:
-> 6801         return self._searchsorted_monotonic(label, side)
   6802     except ValueError:
   6803         # raise the original KeyError
   6804         raise err

File /srv/conda/envs/notebook/lib/python3.10/site-packages/pandas/core/indexes/base.py:6733, in Index._searchsorted_monotonic(self, label, side)
   6731 def _searchsorted_monotonic(self, label, side: Literal["left", "right"] = "left"):
   6732     if self.is_monotonic_increasing:
-> 6733         return self.searchsorted(label, side=side)
   6734     elif self.is_monotonic_decreasing:
   6735         # np.searchsorted expects ascending sort order, have to reverse
   6736         # everything for it to work (element ordering, search side and
   6737         # resulting value).
   6738         pos = self[::-1].searchsorted(
   6739             label, side="right" if side == "left" else "left"
   6740         )

File /srv/conda/envs/notebook/lib/python3.10/site-packages/pandas/core/base.py:1352, in IndexOpsMixin.searchsorted(self, value, side, sorter)
   1348 if not isinstance(values, np.ndarray):
   1349     # Going through EA.searchsorted directly improves performance GH#38083
   1350     return values.searchsorted(value, side=side, sorter=sorter)
-> 1352 return algorithms.searchsorted(
   1353     values,
   1354     value,
   1355     side=side,
   1356     sorter=sorter,
   1357 )

File /srv/conda/envs/notebook/lib/python3.10/site-packages/pandas/core/algorithms.py:1329, in searchsorted(arr, value, side, sorter)
   1325     arr = ensure_wrapped_if_datetimelike(arr)
   1327 # Argument 1 to "searchsorted" of "ndarray" has incompatible type
   1328 # "Union[NumpyValueArrayLike, ExtensionArray]"; expected "NumpyValueArrayLike"
-> 1329 return arr.searchsorted(value, side=side, sorter=sorter)

TypeError: '<' not supported between instances of 'cftime._cftime.DatetimeNoLeap' and 'str'
```


### Anything else we need to know?

It was working with version 2024.05.0

### Environment

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.11.5 | packaged by conda-forge | (main, Aug 27 2023, 03:34:09) [GCC 12.3.0]
python-bits: 64
OS: Linux
OS-release: 3.10.0-1160.105.1.el7.x86_64
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_CA.UTF-8
LOCALE: ('fr_CA', 'UTF-8')
libhdf5: 1.14.1
libnetcdf: 4.9.2
xarray: 2024.6.0
pandas: 2.2.1
numpy: 1.24.4
scipy: 1.11.2
netCDF4: 1.6.4
pydap: None
h5netcdf: 1.2.0
h5py: 3.9.0
zarr: 2.16.1
cftime: 1.6.3
nc_time_axis: 1.4.1
iris: None
bottleneck: 1.3.7
dask: 2023.9.2
distributed: 2023.9.2
matplotlib: 3.8.0
cartopy: 0.22.0
seaborn: 0.13.1
numbagg: None
fsspec: 2023.9.2
cupy: None
pint: 0.22
sparse: 0.14.0
flox: 0.7.2
numpy_groupies: 0.10.1
setuptools: 68.2.2
pip: 23.2.1
conda: installed
pytest: 7.4.2
mypy: None
IPython: 8.15.0
sphinx: 7.2.6

</details>


Discussion:
hmaarrfk: The comment https://github.com/pydata/xarray/pull/9002/files#r1591214299 had the right intuition. The fastpath was the source of problems.

Empirically, i tested to see if it had any effects on the internal benchmarks that brought me to introducing it and it seems it does not (we avoid pandas quite heavily due to slowdowns it introduces, our data access is quite regular).
hmaarrfk: ps. i have visitors this week, so feel free to push to adjust anything you feel the need to change.
hmaarrfk: ```
asv continuous main calendar_noleap --bench indexing


| Change   | Before [42ed6d30] <main>   | After [822c0b8e] <calendar_noleap>   |   Ratio | Benchmark (Parameter)                                      |
|----------|----------------------------|--------------------------------------|---------|------------------------------------------------------------|
| +        | 337±1μs                    | 618±1μs                              |    1.83 | indexing.AssignmentOptimized.time_assign_identical_indexes |
| +        | 67.4±0.6μs                 | 77.7±0.5μs                           |    1.15 | indexing.HugeAxisSmallSliceIndexing.time_indexing          |
| +        | 139±0.7μs                  | 159±0.5μs                            |    1.14 | indexing.Assignment.time_assignment_basic('1slice')        |
```
max-sixty: @pydata/xarray who is the best person to review this?
dcherian: I believe the solution in https://github.com/pydata/xarray/issues/9138#issuecomment-2198145554 is a better path
spencerkclark: Thanks for starting this @hmaarrfk—I went ahead and switched this to the approach suggested in https://github.com/pydata/xarray/issues/9138#issuecomment-2198145554 and moved the test to `test_calendar_ops.py`.  

@aulemahal it would be great if you could take a look at this to ensure the test coverage is adequate.  Thanks!
hmaarrfk: Thank you @spencerkclark glad this is moving forward!

Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 8c6b3a09..f237b406 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -37,6 +37,8 @@ Deprecations
 
 Bug fixes
 ~~~~~~~~~
+- Fix scatter plot broadcasting unneccesarily. (:issue:`9129`, :pull:`9206`)
+  By `Jimmy Westling <https://github.com/illviljan>`_.
 - Don't convert custom indexes to ``pandas`` indexes when computing a diff (:pull:`9157`)
   By `Justus Magin <https://github.com/keewis>`_.
 - Make :py:func:`testing.assert_allclose` work with numpy 2.0 (:issue:`9165`, :pull:`9166`).
@@ -45,6 +47,12 @@ Bug fixes
   By `Justus Magin <https://github.com/keewis>`_.
 - Promote floating-point numeric datetimes before decoding (:issue:`9179`, :pull:`9182`).
   By `Justus Magin <https://github.com/keewis>`_.
+- Address regression introduced in :pull:`9002` that prevented objects returned
+  by py:meth:`DataArray.convert_calendar` to be indexed by a time index in
+  certain circumstances (:issue:`9138`, :pull:`9192`). By `Mark Harfouche
+  <https://github.com/hmaarrfk>`_ and `Spencer Clark
+  <https://github.com/spencerkclark>`.
+
 - Fiy static typing of tolerance arguments by allowing `str` type (:issue:`8892`, :pull:`9194`).
   By `Michael Niklas <https://github.com/headtr1ck>`_.
 - Dark themes are now properly detected for ``html[data-theme=dark]``-tags (:pull:`9200`).
diff --git a/xarray/coding/calendar_ops.py b/xarray/coding/calendar_ops.py
index c4fe9e1f..6f492e78 100644
--- a/xarray/coding/calendar_ops.py
+++ b/xarray/coding/calendar_ops.py
@@ -5,7 +5,10 @@ import pandas as pd
 
 from xarray.coding.cftime_offsets import date_range_like, get_date_type
 from xarray.coding.cftimeindex import CFTimeIndex
-from xarray.coding.times import _should_cftime_be_used, convert_times
+from xarray.coding.times import (
+    _should_cftime_be_used,
+    convert_times,
+)
 from xarray.core.common import _contains_datetime_like_objects, is_np_datetime_like
 
 try:
@@ -222,6 +225,13 @@ def convert_calendar(
         # Remove NaN that where put on invalid dates in target calendar
         out = out.where(out[dim].notnull(), drop=True)
 
+        if use_cftime:
+            # Reassign times to ensure time index of output is a CFTimeIndex
+            # (previously it was an Index due to the presence of NaN values).
+            # Note this is not needed in the case that the output time index is
+            # a DatetimeIndex, since DatetimeIndexes can handle NaN values.
+            out[dim] = CFTimeIndex(out[dim].data)
+
     if missing is not None:
         time_target = date_range_like(time, calendar=calendar, use_cftime=use_cftime)
         out = out.reindex({dim: time_target}, fill_value=missing)
diff --git a/xarray/coding/times.py b/xarray/coding/times.py
index 34d4f9a2..50a2ba93 100644
--- a/xarray/coding/times.py
+++ b/xarray/coding/times.py
@@ -22,7 +22,7 @@ from xarray.coding.variables import (
 )
 from xarray.core import indexing
 from xarray.core.common import contains_cftime_datetimes, is_np_datetime_like
-from xarray.core.duck_array_ops import asarray
+from xarray.core.duck_array_ops import asarray, ravel, reshape
 from xarray.core.formatting import first_n_items, format_timestamp, last_item
 from xarray.core.pdcompat import nanosecond_precision_timestamp
 from xarray.core.utils import emit_user_level_warning
@@ -315,7 +315,7 @@ def decode_cf_datetime(
     cftime.num2date
     """
     num_dates = np.asarray(num_dates)
-    flat_num_dates = num_dates.ravel()
+    flat_num_dates = ravel(num_dates)
     if calendar is None:
         calendar = "standard"
 
@@ -348,7 +348,7 @@ def decode_cf_datetime(
     else:
         dates = _decode_datetime_with_pandas(flat_num_dates, units, calendar)
 
-    return dates.reshape(num_dates.shape)
+    return reshape(dates, num_dates.shape)
 
 
 def to_timedelta_unboxed(value, **kwargs):
@@ -369,8 +369,8 @@ def decode_cf_timedelta(num_timedeltas, units: str) -> np.ndarray:
     """
     num_timedeltas = np.asarray(num_timedeltas)
     units = _netcdf_to_numpy_timeunit(units)
-    result = to_timedelta_unboxed(num_timedeltas.ravel(), unit=units)
-    return result.reshape(num_timedeltas.shape)
+    result = to_timedelta_unboxed(ravel(num_timedeltas), unit=units)
+    return reshape(result, num_timedeltas.shape)
 
 
 def _unit_timedelta_cftime(units: str) -> timedelta:
@@ -428,7 +428,7 @@ def infer_datetime_units(dates) -> str:
     'hours', 'minutes' or 'seconds' (the first one that can evenly divide all
     unique time deltas in `dates`)
     """
-    dates = np.asarray(dates).ravel()
+    dates = ravel(np.asarray(dates))
     if np.asarray(dates).dtype == "datetime64[ns]":
         dates = to_datetime_unboxed(dates)
         dates = dates[pd.notnull(dates)]
@@ -456,7 +456,7 @@ def infer_timedelta_units(deltas) -> str:
     {'days', 'hours', 'minutes' 'seconds'} (the first one that can evenly
     divide all unique time deltas in `deltas`)
     """
-    deltas = to_timedelta_unboxed(np.asarray(deltas).ravel())
+    deltas = to_timedelta_unboxed(ravel(np.asarray(deltas)))
     unique_timedeltas = np.unique(deltas[pd.notnull(deltas)])
     return _infer_time_units_from_diff(unique_timedeltas)
 
@@ -643,7 +643,7 @@ def _encode_datetime_with_cftime(dates, units: str, calendar: str) -> np.ndarray
         except TypeError:
             return np.nan if d is None else cftime.date2num(d, units, calendar)
 
-    return np.array([encode_datetime(d) for d in dates.ravel()]).reshape(dates.shape)
+    return reshape(np.array([encode_datetime(d) for d in ravel(dates)]), dates.shape)
 
 
 def cast_to_int_if_safe(num) -> np.ndarray:
@@ -753,7 +753,7 @@ def _eagerly_encode_cf_datetime(
         # Wrap the dates in a DatetimeIndex to do the subtraction to ensure
         # an OverflowError is raised if the ref_date is too far away from
         # dates to be encoded (GH 2272).
-        dates_as_index = pd.DatetimeIndex(dates.ravel())
+        dates_as_index = pd.DatetimeIndex(ravel(dates))
         time_deltas = dates_as_index - ref_date
 
         # retrieve needed units to faithfully encode to int64
@@ -791,7 +791,7 @@ def _eagerly_encode_cf_datetime(
                 floor_division = True
 
         num = _division(time_deltas, time_delta, floor_division)
-        num = num.values.reshape(dates.shape)
+        num = reshape(num.values, dates.shape)
 
     except (OutOfBoundsDatetime, OverflowError, ValueError):
         num = _encode_datetime_with_cftime(dates, units, calendar)
@@ -879,7 +879,7 @@ def _eagerly_encode_cf_timedelta(
         units = data_units
 
     time_delta = _time_units_to_timedelta64(units)
-    time_deltas = pd.TimedeltaIndex(timedeltas.ravel())
+    time_deltas = pd.TimedeltaIndex(ravel(timedeltas))
 
     # retrieve needed units to faithfully encode to int64
     needed_units = data_units
@@ -911,7 +911,7 @@ def _eagerly_encode_cf_timedelta(
             floor_division = True
 
     num = _division(time_deltas, time_delta, floor_division)
-    num = num.values.reshape(timedeltas.shape)
+    num = reshape(num.values, timedeltas.shape)
 
     if dtype is not None:
         num = _cast_to_dtype_if_safe(num, dtype)
diff --git a/xarray/core/datatree.py b/xarray/core/datatree.py
index 38f8f8cd..65ff8667 100644
--- a/xarray/core/datatree.py
+++ b/xarray/core/datatree.py
@@ -61,7 +61,7 @@ if TYPE_CHECKING:
     import pandas as pd
 
     from xarray.core.datatree_io import T_DataTreeNetcdfEngine, T_DataTreeNetcdfTypes
-    from xarray.core.merge import CoercibleValue
+    from xarray.core.merge import CoercibleMapping, CoercibleValue
     from xarray.core.types import ErrorOptions, NetcdfWriteModes, ZarrWriteModes
 
 # """
@@ -954,23 +954,29 @@ class DataTree(
 
         Just like `dict.update` this is an in-place operation.
         """
-        # TODO separate by type
         new_children: dict[str, DataTree] = {}
-        new_variables = {}
-        for k, v in other.items():
-            if isinstance(v, DataTree):
-                # avoid named node being stored under inconsistent key
-                new_child: DataTree = v.copy()
-                # Datatree's name is always a string until we fix that (#8836)
-                new_child.name = str(k)
-                new_children[str(k)] = new_child
-            elif isinstance(v, (DataArray, Variable)):
-                # TODO this should also accommodate other types that can be coerced into Variables
-                new_variables[k] = v
-            else:
-                raise TypeError(f"Type {type(v)} cannot be assigned to a DataTree")
-
-        vars_merge_result = dataset_update_method(self.to_dataset(), new_variables)
+        new_variables: CoercibleMapping
+
+        if isinstance(other, Dataset):
+            new_variables = other
+        else:
+            new_variables = {}
+            for k, v in other.items():
+                if isinstance(v, DataTree):
+                    # avoid named node being stored under inconsistent key
+                    new_child: DataTree = v.copy()
+                    # Datatree's name is always a string until we fix that (#8836)
+                    new_child.name = str(k)
+                    new_children[str(k)] = new_child
+                elif isinstance(v, (DataArray, Variable)):
+                    # TODO this should also accommodate other types that can be coerced into Variables
+                    new_variables[k] = v
+                else:
+                    raise TypeError(f"Type {type(v)} cannot be assigned to a DataTree")
+
+        vars_merge_result = dataset_update_method(
+            self.to_dataset(inherited=False), new_variables
+        )
         data = Dataset._construct_direct(**vars_merge_result._asdict())
 
         # TODO are there any subtleties with preserving order of children like this?
diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py
index edc2bf43..96b59f61 100644
--- a/xarray/plot/dataset_plot.py
+++ b/xarray/plot/dataset_plot.py
@@ -721,8 +721,8 @@ def _temp_dataarray(ds: Dataset, y: Hashable, locals_: dict[str, Any]) -> DataAr
     """Create a temporary datarray with extra coords."""
     from xarray.core.dataarray import DataArray
 
-    # Base coords:
-    coords = dict(ds.coords)
+    coords = dict(ds[y].coords)
+    dims = set(ds[y].dims)
 
     # Add extra coords to the DataArray from valid kwargs, if using all
     # kwargs there is a risk that we add unnecessary dataarrays as
@@ -732,12 +732,17 @@ def _temp_dataarray(ds: Dataset, y: Hashable, locals_: dict[str, Any]) -> DataAr
     coord_kwargs = locals_.keys() & valid_coord_kwargs
     for k in coord_kwargs:
         key = locals_[k]
-        if ds.data_vars.get(key) is not None:
-            coords[key] = ds[key]
+        darray = ds.get(key)
+        if darray is not None:
+            coords[key] = darray
+            dims.update(darray.dims)
+
+    # Trim dataset from unneccessary dims:
+    ds_trimmed = ds.drop_dims(ds.sizes.keys() - dims)  # TODO: Use ds.dims in the future
 
     # The dataarray has to include all the dims. Broadcast to that shape
     # and add the additional coords:
-    _y = ds[y].broadcast_like(ds)
+    _y = ds[y].broadcast_like(ds_trimmed)
 
     return DataArray(_y, coords=coords)
 


Test patch:
diff --git a/xarray/tests/test_calendar_ops.py b/xarray/tests/test_calendar_ops.py
index 7d229371..13e9f7a1 100644
--- a/xarray/tests/test_calendar_ops.py
+++ b/xarray/tests/test_calendar_ops.py
@@ -1,9 +1,10 @@
 from __future__ import annotations
 
 import numpy as np
+import pandas as pd
 import pytest
 
-from xarray import DataArray, infer_freq
+from xarray import CFTimeIndex, DataArray, infer_freq
 from xarray.coding.calendar_ops import convert_calendar, interp_calendar
 from xarray.coding.cftime_offsets import date_range
 from xarray.testing import assert_identical
@@ -286,3 +287,25 @@ def test_interp_calendar_errors():
         ValueError, match="Both 'source.x' and 'target' must contain datetime objects."
     ):
         interp_calendar(da1, da2, dim="x")
+
+
+@requires_cftime
+@pytest.mark.parametrize(
+    ("source_calendar", "target_calendar", "expected_index"),
+    [("standard", "noleap", CFTimeIndex), ("all_leap", "standard", pd.DatetimeIndex)],
+)
+def test_convert_calendar_produces_time_index(
+    source_calendar, target_calendar, expected_index
+):
+    # https://github.com/pydata/xarray/issues/9138
+    time = date_range("2000-01-01", "2002-01-01", freq="D", calendar=source_calendar)
+    temperature = np.ones(len(time))
+    da = DataArray(
+        data=temperature,
+        dims=["time"],
+        coords=dict(
+            time=time,
+        ),
+    )
+    converted = da.convert_calendar(target_calendar)
+    assert isinstance(converted.indexes["time"], expected_index)
diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py
index 393f8400..623e4e9f 100644
--- a/xarray/tests/test_coding_times.py
+++ b/xarray/tests/test_coding_times.py
@@ -14,13 +14,15 @@ from xarray import (
     Dataset,
     Variable,
     cftime_range,
-    coding,
     conventions,
     date_range,
     decode_cf,
 )
+from xarray.coding.times import _STANDARD_CALENDARS as _STANDARD_CALENDARS_UNSORTED
 from xarray.coding.times import (
+    CFDatetimeCoder,
     _encode_datetime_with_cftime,
+    _netcdf_to_numpy_timeunit,
     _numpy_to_netcdf_timeunit,
     _should_cftime_be_used,
     cftime_to_nptime,
@@ -28,6 +30,9 @@ from xarray.coding.times import (
     decode_cf_timedelta,
     encode_cf_datetime,
     encode_cf_timedelta,
+    format_cftime_datetime,
+    infer_datetime_units,
+    infer_timedelta_units,
     to_timedelta_unboxed,
 )
 from xarray.coding.variables import SerializationWarning
@@ -39,6 +44,8 @@ from xarray.tests import (
     FirstElementAccessibleArray,
     arm_xfail,
     assert_array_equal,
+    assert_duckarray_allclose,
+    assert_duckarray_equal,
     assert_no_warnings,
     has_cftime,
     requires_cftime,
@@ -53,11 +60,9 @@ _NON_STANDARD_CALENDARS_SET = {
     "all_leap",
     "366_day",
 }
-_ALL_CALENDARS = sorted(
-    _NON_STANDARD_CALENDARS_SET.union(coding.times._STANDARD_CALENDARS)
-)
+_STANDARD_CALENDARS = sorted(_STANDARD_CALENDARS_UNSORTED)
+_ALL_CALENDARS = sorted(_NON_STANDARD_CALENDARS_SET.union(_STANDARD_CALENDARS))
 _NON_STANDARD_CALENDARS = sorted(_NON_STANDARD_CALENDARS_SET)
-_STANDARD_CALENDARS = sorted(coding.times._STANDARD_CALENDARS)
 _CF_DATETIME_NUM_DATES_UNITS = [
     (np.arange(10), "days since 2000-01-01"),
     (np.arange(10).astype("float64"), "days since 2000-01-01"),
@@ -130,7 +135,7 @@ def test_cf_datetime(num_dates, units, calendar) -> None:
 
     with warnings.catch_warnings():
         warnings.filterwarnings("ignore", "Unable to decode time axis")
-        actual = coding.times.decode_cf_datetime(num_dates, units, calendar)
+        actual = decode_cf_datetime(num_dates, units, calendar)
 
     abs_diff = np.asarray(abs(actual - expected)).ravel()
     abs_diff = pd.to_timedelta(abs_diff.tolist()).to_numpy()
@@ -139,17 +144,15 @@ def test_cf_datetime(num_dates, units, calendar) -> None:
     # we could do this check with near microsecond accuracy:
     # https://github.com/Unidata/netcdf4-python/issues/355
     assert (abs_diff <= np.timedelta64(1, "s")).all()
-    encoded, _, _ = coding.times.encode_cf_datetime(actual, units, calendar)
+    encoded, _, _ = encode_cf_datetime(actual, units, calendar)
 
-    assert_array_equal(num_dates, np.around(encoded, 1))
+    assert_duckarray_allclose(num_dates, encoded)
     if hasattr(num_dates, "ndim") and num_dates.ndim == 1 and "1000" not in units:
         # verify that wrapping with a pandas.Index works
         # note that it *does not* currently work to put
         # non-datetime64 compatible dates into a pandas.Index
-        encoded, _, _ = coding.times.encode_cf_datetime(
-            pd.Index(actual), units, calendar
-        )
-        assert_array_equal(num_dates, np.around(encoded, 1))
+        encoded, _, _ = encode_cf_datetime(pd.Index(actual), units, calendar)
+        assert_duckarray_allclose(num_dates, encoded)
 
 
 @requires_cftime
@@ -169,7 +172,7 @@ def test_decode_cf_datetime_overflow() -> None:
     for i, day in enumerate(days):
         with warnings.catch_warnings():
             warnings.filterwarnings("ignore", "Unable to decode time axis")
-            result = coding.times.decode_cf_datetime(day, units)
+            result = decode_cf_datetime(day, units)
         assert result == expected[i]
 
 
@@ -178,7 +181,7 @@ def test_decode_cf_datetime_non_standard_units() -> None:
     # netCDFs from madis.noaa.gov use this format for their time units
     # they cannot be parsed by cftime, but pd.Timestamp works
     units = "hours since 1-1-1970"
-    actual = coding.times.decode_cf_datetime(np.arange(100), units)
+    actual = decode_cf_datetime(np.arange(100), units)
     assert_array_equal(actual, expected)
 
 
@@ -193,7 +196,7 @@ def test_decode_cf_datetime_non_iso_strings() -> None:
         (np.arange(100), "hours since 2000-01-01 0:00"),
     ]
     for num_dates, units in cases:
-        actual = coding.times.decode_cf_datetime(num_dates, units)
+        actual = decode_cf_datetime(num_dates, units)
         abs_diff = abs(actual - expected.values)
         # once we no longer support versions of netCDF4 older than 1.1.5,
         # we could do this check with near microsecond accuracy:
@@ -212,7 +215,7 @@ def test_decode_standard_calendar_inside_timestamp_range(calendar) -> None:
     expected = times.values
     expected_dtype = np.dtype("M8[ns]")
 
-    actual = coding.times.decode_cf_datetime(time, units, calendar=calendar)
+    actual = decode_cf_datetime(time, units, calendar=calendar)
     assert actual.dtype == expected_dtype
     abs_diff = abs(actual - expected)
     # once we no longer support versions of netCDF4 older than 1.1.5,
@@ -235,9 +238,7 @@ def test_decode_non_standard_calendar_inside_timestamp_range(calendar) -> None:
     )
     expected_dtype = np.dtype("O")
 
-    actual = coding.times.decode_cf_datetime(
-        non_standard_time, units, calendar=calendar
-    )
+    actual = decode_cf_datetime(non_standard_time, units, calendar=calendar)
     assert actual.dtype == expected_dtype
     abs_diff = abs(actual - expected)
     # once we no longer support versions of netCDF4 older than 1.1.5,
@@ -264,7 +265,7 @@ def test_decode_dates_outside_timestamp_range(calendar) -> None:
 
     with warnings.catch_warnings():
         warnings.filterwarnings("ignore", "Unable to decode time axis")
-        actual = coding.times.decode_cf_datetime(time, units, calendar=calendar)
+        actual = decode_cf_datetime(time, units, calendar=calendar)
     assert all(isinstance(value, expected_date_type) for value in actual)
     abs_diff = abs(actual - expected)
     # once we no longer support versions of netCDF4 older than 1.1.5,
@@ -282,7 +283,7 @@ def test_decode_standard_calendar_single_element_inside_timestamp_range(
     for num_time in [735368, [735368], [[735368]]]:
         with warnings.catch_warnings():
             warnings.filterwarnings("ignore", "Unable to decode time axis")
-            actual = coding.times.decode_cf_datetime(num_time, units, calendar=calendar)
+            actual = decode_cf_datetime(num_time, units, calendar=calendar)
         assert actual.dtype == np.dtype("M8[ns]")
 
 
@@ -295,7 +296,7 @@ def test_decode_non_standard_calendar_single_element_inside_timestamp_range(
     for num_time in [735368, [735368], [[735368]]]:
         with warnings.catch_warnings():
             warnings.filterwarnings("ignore", "Unable to decode time axis")
-            actual = coding.times.decode_cf_datetime(num_time, units, calendar=calendar)
+            actual = decode_cf_datetime(num_time, units, calendar=calendar)
         assert actual.dtype == np.dtype("O")
 
 
@@ -309,9 +310,7 @@ def test_decode_single_element_outside_timestamp_range(calendar) -> None:
         for num_time in [days, [days], [[days]]]:
             with warnings.catch_warnings():
                 warnings.filterwarnings("ignore", "Unable to decode time axis")
-                actual = coding.times.decode_cf_datetime(
-                    num_time, units, calendar=calendar
-                )
+                actual = decode_cf_datetime(num_time, units, calendar=calendar)
 
             expected = cftime.num2date(
                 days, units, calendar, only_use_cftime_datetimes=True
@@ -338,7 +337,7 @@ def test_decode_standard_calendar_multidim_time_inside_timestamp_range(
     expected1 = times1.values
     expected2 = times2.values
 
-    actual = coding.times.decode_cf_datetime(mdim_time, units, calendar=calendar)
+    actual = decode_cf_datetime(mdim_time, units, calendar=calendar)
     assert actual.dtype == np.dtype("M8[ns]")
 
     abs_diff1 = abs(actual[:, 0] - expected1)
@@ -379,7 +378,7 @@ def test_decode_nonstandard_calendar_multidim_time_inside_timestamp_range(
 
     expected_dtype = np.dtype("O")
 
-    actual = coding.times.decode_cf_datetime(mdim_time, units, calendar=calendar)
+    actual = decode_cf_datetime(mdim_time, units, calendar=calendar)
 
     assert actual.dtype == expected_dtype
     abs_diff1 = abs(actual[:, 0] - expected1)
@@ -412,7 +411,7 @@ def test_decode_multidim_time_outside_timestamp_range(calendar) -> None:
 
     with warnings.catch_warnings():
         warnings.filterwarnings("ignore", "Unable to decode time axis")
-        actual = coding.times.decode_cf_datetime(mdim_time, units, calendar=calendar)
+        actual = decode_cf_datetime(mdim_time, units, calendar=calendar)
 
     assert actual.dtype == np.dtype("O")
 
@@ -435,7 +434,7 @@ def test_decode_non_standard_calendar_single_element(calendar, num_time) -> None
 
     units = "days since 0001-01-01"
 
-    actual = coding.times.decode_cf_datetime(num_time, units, calendar=calendar)
+    actual = decode_cf_datetime(num_time, units, calendar=calendar)
 
     expected = np.asarray(
         cftime.num2date(num_time, units, calendar, only_use_cftime_datetimes=True)
@@ -460,9 +459,7 @@ def test_decode_360_day_calendar() -> None:
 
         with warnings.catch_warnings(record=True) as w:
             warnings.simplefilter("always")
-            actual = coding.times.decode_cf_datetime(
-                num_times, units, calendar=calendar
-            )
+            actual = decode_cf_datetime(num_times, units, calendar=calendar)
             assert len(w) == 0
 
         assert actual.dtype == np.dtype("O")
@@ -476,8 +473,8 @@ def test_decode_abbreviation() -> None:
 
     val = np.array([1586628000000.0])
     units = "msecs since 1970-01-01T00:00:00Z"
-    actual = coding.times.decode_cf_datetime(val, units)
-    expected = coding.times.cftime_to_nptime(cftime.num2date(val, units))
+    actual = decode_cf_datetime(val, units)
+    expected = cftime_to_nptime(cftime.num2date(val, units))
     assert_array_equal(actual, expected)
 
 
@@ -498,7 +495,7 @@ def test_decode_abbreviation() -> None:
 def test_cf_datetime_nan(num_dates, units, expected_list) -> None:
     with warnings.catch_warnings():
         warnings.filterwarnings("ignore", "All-NaN")
-        actual = coding.times.decode_cf_datetime(num_dates, units)
+        actual = decode_cf_datetime(num_dates, units)
     # use pandas because numpy will deprecate timezone-aware conversions
     expected = pd.to_datetime(expected_list).to_numpy(dtype="datetime64[ns]")
     assert_array_equal(expected, actual)
@@ -510,7 +507,7 @@ def test_decoded_cf_datetime_array_2d() -> None:
     variable = Variable(
         ("x", "y"), np.array([[0, 1], [2, 3]]), {"units": "days since 2000-01-01"}
     )
-    result = coding.times.CFDatetimeCoder().decode(variable)
+    result = CFDatetimeCoder().decode(variable)
     assert result.dtype == "datetime64[ns]"
     expected = pd.date_range("2000-01-01", periods=4).values.reshape(2, 2)
     assert_array_equal(np.asarray(result), expected)
@@ -531,7 +528,7 @@ FREQUENCIES_TO_ENCODING_UNITS = {
 def test_infer_datetime_units(freq, units) -> None:
     dates = pd.date_range("2000", periods=2, freq=freq)
     expected = f"{units} since 2000-01-01 00:00:00"
-    assert expected == coding.times.infer_datetime_units(dates)
+    assert expected == infer_datetime_units(dates)
 
 
 @pytest.mark.parametrize(
@@ -549,7 +546,7 @@ def test_infer_datetime_units(freq, units) -> None:
     ],
 )
 def test_infer_datetime_units_with_NaT(dates, expected) -> None:
-    assert expected == coding.times.infer_datetime_units(dates)
+    assert expected == infer_datetime_units(dates)
 
 
 _CFTIME_DATETIME_UNITS_TESTS = [
@@ -573,7 +570,7 @@ _CFTIME_DATETIME_UNITS_TESTS = [
 def test_infer_cftime_datetime_units(calendar, date_args, expected) -> None:
     date_type = _all_cftime_date_types()[calendar]
     dates = [date_type(*args) for args in date_args]
-    assert expected == coding.times.infer_datetime_units(dates)
+    assert expected == infer_datetime_units(dates)
 
 
 @pytest.mark.filterwarnings("ignore:Timedeltas can't be serialized faithfully")
@@ -600,18 +597,18 @@ def test_cf_timedelta(timedeltas, units, numbers) -> None:
     numbers = np.array(numbers)
 
     expected = numbers
-    actual, _ = coding.times.encode_cf_timedelta(timedeltas, units)
+    actual, _ = encode_cf_timedelta(timedeltas, units)
     assert_array_equal(expected, actual)
     assert expected.dtype == actual.dtype
 
     if units is not None:
         expected = timedeltas
-        actual = coding.times.decode_cf_timedelta(numbers, units)
+        actual = decode_cf_timedelta(numbers, units)
         assert_array_equal(expected, actual)
         assert expected.dtype == actual.dtype
 
     expected = np.timedelta64("NaT", "ns")
-    actual = coding.times.decode_cf_timedelta(np.array(np.nan), "days")
+    actual = decode_cf_timedelta(np.array(np.nan), "days")
     assert_array_equal(expected, actual)
 
 
@@ -622,7 +619,7 @@ def test_cf_timedelta_2d() -> None:
     timedeltas = np.atleast_2d(to_timedelta_unboxed(["1D", "2D", "3D"]))
     expected = timedeltas
 
-    actual = coding.times.decode_cf_timedelta(numbers, units)
+    actual = decode_cf_timedelta(numbers, units)
     assert_array_equal(expected, actual)
     assert expected.dtype == actual.dtype
 
@@ -637,7 +634,7 @@ def test_cf_timedelta_2d() -> None:
     ],
 )
 def test_infer_timedelta_units(deltas, expected) -> None:
-    assert expected == coding.times.infer_timedelta_units(deltas)
+    assert expected == infer_timedelta_units(deltas)
 
 
 @requires_cftime
@@ -653,7 +650,7 @@ def test_infer_timedelta_units(deltas, expected) -> None:
 def test_format_cftime_datetime(date_args, expected) -> None:
     date_types = _all_cftime_date_types()
     for date_type in date_types.values():
-        result = coding.times.format_cftime_datetime(date_type(*date_args))
+        result = format_cftime_datetime(date_type(*date_args))
         assert result == expected
 
 
@@ -898,10 +895,10 @@ def test_time_units_with_timezone_roundtrip(calendar) -> None:
     )
 
     if calendar in _STANDARD_CALENDARS:
-        np.testing.assert_array_equal(result_num_dates, expected_num_dates)
+        assert_duckarray_equal(result_num_dates, expected_num_dates)
     else:
         # cftime datetime arithmetic is not quite exact.
-        np.testing.assert_allclose(result_num_dates, expected_num_dates)
+        assert_duckarray_allclose(result_num_dates, expected_num_dates)
 
     assert result_units == expected_units
     assert result_calendar == calendar
@@ -1008,7 +1005,7 @@ def test_decode_ambiguous_time_warns(calendar) -> None:
 
     # we don't decode non-standard calendards with
     # pandas so expect no warning will be emitted
-    is_standard_calendar = calendar in coding.times._STANDARD_CALENDARS
+    is_standard_calendar = calendar in _STANDARD_CALENDARS
 
     dates = [1, 2, 3]
     units = "days since 1-1-1"
@@ -1043,9 +1040,9 @@ def test_encode_cf_datetime_defaults_to_correct_dtype(
         pytest.skip("Nanosecond frequency is not valid for cftime dates.")
     times = date_range("2000", periods=3, freq=freq)
     units = f"{encoding_units} since 2000-01-01"
-    encoded, _units, _ = coding.times.encode_cf_datetime(times, units)
+    encoded, _units, _ = encode_cf_datetime(times, units)
 
-    numpy_timeunit = coding.times._netcdf_to_numpy_timeunit(encoding_units)
+    numpy_timeunit = _netcdf_to_numpy_timeunit(encoding_units)
     encoding_units_as_timedelta = np.timedelta64(1, numpy_timeunit)
     if pd.to_timedelta(1, freq) >= encoding_units_as_timedelta:
         assert encoded.dtype == np.int64
@@ -1202,7 +1199,7 @@ def test_decode_float_datetime():
 def test_scalar_unit() -> None:
     # test that a scalar units (often NaN when using to_netcdf) does not raise an error
     variable = Variable(("x", "y"), np.array([[0, 1], [2, 3]]), {"units": np.nan})
-    result = coding.times.CFDatetimeCoder().decode(variable)
+    result = CFDatetimeCoder().decode(variable)
     assert np.isnan(result.attrs["units"])
 
 
diff --git a/xarray/tests/test_computation.py b/xarray/tests/test_computation.py
index 4b9b95b2..b000de31 100644
--- a/xarray/tests/test_computation.py
+++ b/xarray/tests/test_computation.py
@@ -2602,7 +2602,7 @@ def test_cross(a, b, ae, be, dim: str, axis: int, use_dask: bool) -> None:
 
 @pytest.mark.parametrize("compute_backend", ["numbagg"], indirect=True)
 def test_complex_number_reduce(compute_backend):
-    da = xr.DataArray(np.ones((2,), dtype=np.complex_), dims=["x"])
+    da = xr.DataArray(np.ones((2,), dtype=np.complex64), dims=["x"])
     # Check that xarray doesn't call into numbagg, which doesn't compile for complex
     # numbers at the moment (but will when numba supports dynamic compilation)
     da.min()
diff --git a/xarray/tests/test_datatree.py b/xarray/tests/test_datatree.py
index f2b58fa2..f7cff17b 100644
--- a/xarray/tests/test_datatree.py
+++ b/xarray/tests/test_datatree.py
@@ -244,11 +244,6 @@ class TestUpdate:
         dt: DataTree = DataTree()
         dt.update({"foo": xr.DataArray(0), "a": DataTree()})
         expected = DataTree.from_dict({"/": xr.Dataset({"foo": 0}), "a": None})
-        print(dt)
-        print(dt.children)
-        print(dt._children)
-        print(dt["a"])
-        print(expected)
         assert_equal(dt, expected)
 
     def test_update_new_named_dataarray(self):
@@ -268,14 +263,38 @@ class TestUpdate:
     def test_update_overwrite(self):
         actual = DataTree.from_dict({"a": DataTree(xr.Dataset({"x": 1}))})
         actual.update({"a": DataTree(xr.Dataset({"x": 2}))})
-
         expected = DataTree.from_dict({"a": DataTree(xr.Dataset({"x": 2}))})
+        assert_equal(actual, expected)
 
-        print(actual)
-        print(expected)
-
+    def test_update_coordinates(self):
+        expected = DataTree.from_dict({"/": xr.Dataset(coords={"a": 1})})
+        actual = DataTree.from_dict({"/": xr.Dataset()})
+        actual.update(xr.Dataset(coords={"a": 1}))
         assert_equal(actual, expected)
 
+    def test_update_inherited_coords(self):
+        expected = DataTree.from_dict(
+            {
+                "/": xr.Dataset(coords={"a": 1}),
+                "/b": xr.Dataset(coords={"c": 1}),
+            }
+        )
+        actual = DataTree.from_dict(
+            {
+                "/": xr.Dataset(coords={"a": 1}),
+                "/b": xr.Dataset(),
+            }
+        )
+        actual["/b"].update(xr.Dataset(coords={"c": 1}))
+        assert_identical(actual, expected)
+
+        # DataTree.identical() currently does not require that non-inherited
+        # coordinates are defined identically, so we need to check this
+        # explicitly
+        actual_node = actual.children["b"].to_dataset(inherited=False)
+        expected_node = expected.children["b"].to_dataset(inherited=False)
+        assert_identical(actual_node, expected_node)
+
 
 class TestCopy:
     def test_copy(self, create_test_datatree):
diff --git a/xarray/tests/test_missing.py b/xarray/tests/test_missing.py
index da9513a7..bd75f633 100644
--- a/xarray/tests/test_missing.py
+++ b/xarray/tests/test_missing.py
@@ -421,46 +421,37 @@ def test_ffill():
     assert_equal(actual, expected)
 
 
-def test_ffill_use_bottleneck_numbagg():
+@pytest.mark.parametrize("compute_backend", [None], indirect=True)
+@pytest.mark.parametrize("method", ["ffill", "bfill"])
+def test_b_ffill_use_bottleneck_numbagg(method, compute_backend):
+    """
+    bfill & ffill fail if both bottleneck and numba are disabled
+    """
     da = xr.DataArray(np.array([4, 5, np.nan], dtype=np.float64), dims="x")
-    with xr.set_options(use_bottleneck=False, use_numbagg=False):
-        with pytest.raises(RuntimeError):
-            da.ffill("x")
+    with pytest.raises(RuntimeError):
+        getattr(da, method)("x")
 
 
 @requires_dask
-def test_ffill_use_bottleneck_dask():
+@pytest.mark.parametrize("compute_backend", [None], indirect=True)
+@pytest.mark.parametrize("method", ["ffill", "bfill"])
+def test_b_ffill_use_bottleneck_dask(method, compute_backend):
+    """
+    ffill fails if both bottleneck and numba are disabled, on dask arrays
+    """
     da = xr.DataArray(np.array([4, 5, np.nan], dtype=np.float64), dims="x")
-    da = da.chunk({"x": 1})
-    with xr.set_options(use_bottleneck=False, use_numbagg=False):
-        with pytest.raises(RuntimeError):
-            da.ffill("x")
+    with pytest.raises(RuntimeError):
+        getattr(da, method)("x")
 
 
 @requires_numbagg
 @requires_dask
-def test_ffill_use_numbagg_dask():
-    with xr.set_options(use_bottleneck=False):
-        da = xr.DataArray(np.array([4, 5, np.nan], dtype=np.float64), dims="x")
-        da = da.chunk(x=-1)
-        # Succeeds with a single chunk:
-        _ = da.ffill("x").compute()
-
-
-def test_bfill_use_bottleneck():
-    da = xr.DataArray(np.array([4, 5, np.nan], dtype=np.float64), dims="x")
-    with xr.set_options(use_bottleneck=False, use_numbagg=False):
-        with pytest.raises(RuntimeError):
-            da.bfill("x")
-
-
-@requires_dask
-def test_bfill_use_bottleneck_dask():
+@pytest.mark.parametrize("compute_backend", ["numbagg"], indirect=True)
+def test_ffill_use_numbagg_dask(compute_backend):
     da = xr.DataArray(np.array([4, 5, np.nan], dtype=np.float64), dims="x")
-    da = da.chunk({"x": 1})
-    with xr.set_options(use_bottleneck=False, use_numbagg=False):
-        with pytest.raises(RuntimeError):
-            da.bfill("x")
+    da = da.chunk(x=-1)
+    # Succeeds with a single chunk:
+    _ = da.ffill("x").compute()
 
 
 @requires_bottleneck
diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py
index b302ad3a..fa08e997 100644
--- a/xarray/tests/test_plot.py
+++ b/xarray/tests/test_plot.py
@@ -3416,3 +3416,43 @@ def test_9155() -> None:
         data = xr.DataArray([1, 2, 3], dims=["x"])
         fig, ax = plt.subplots(ncols=1, nrows=1)
         data.plot(ax=ax)
+
+
+@requires_matplotlib
+def test_temp_dataarray() -> None:
+    from xarray.plot.dataset_plot import _temp_dataarray
+
+    x = np.arange(1, 4)
+    y = np.arange(4, 6)
+    var1 = np.arange(x.size * y.size).reshape((x.size, y.size))
+    var2 = np.arange(x.size * y.size).reshape((x.size, y.size))
+    ds = xr.Dataset(
+        {
+            "var1": (["x", "y"], var1),
+            "var2": (["x", "y"], 2 * var2),
+            "var3": (["x"], 3 * x),
+        },
+        coords={
+            "x": x,
+            "y": y,
+            "model": np.arange(7),
+        },
+    )
+
+    # No broadcasting:
+    y_ = "var1"
+    locals_ = {"x": "var2"}
+    da = _temp_dataarray(ds, y_, locals_)
+    assert da.shape == (3, 2)
+
+    # Broadcast from 1 to 2dim:
+    y_ = "var3"
+    locals_ = {"x": "var1"}
+    da = _temp_dataarray(ds, y_, locals_)
+    assert da.shape == (3, 2)
+
+    # Ignore non-valid coord kwargs:
+    y_ = "var3"
+    locals_ = dict(x="x", extend="var2")
+    da = _temp_dataarray(ds, y_, locals_)
+    assert da.shape == (3,)