From a2d8e69078805918519b819c8e2d31ac1d649eff Mon Sep 17 00:00:00 2001
From: Kai Muehlbauer <kai.muehlbauer@uni-bonn.de>
Date: Fri, 22 Nov 2024 11:19:14 +0100
Subject: [PATCH 01/14] split out CFDatetimeCoder into coders, deprecate
 use_cftime as keyword argument

---
 doc/api.rst                       | 11 ++++++
 doc/whats-new.rst                 |  3 ++
 xarray/__init__.py                |  3 +-
 xarray/backends/api.py            | 47 +++++++++++++++++------
 xarray/coders.py                  | 10 +++++
 xarray/coding/times.py            | 21 +++++++++--
 xarray/conventions.py             | 46 +++++++++++++++++-----
 xarray/convert.py                 |  3 +-
 xarray/tests/test_backends.py     | 15 ++++++--
 xarray/tests/test_coding_times.py | 63 ++++++++++++++++++++++---------
 xarray/tests/test_conventions.py  |  4 +-
 xarray/tests/test_dataarray.py    |  2 +-
 12 files changed, 176 insertions(+), 52 deletions(-)
 create mode 100644 xarray/coders.py

diff --git a/doc/api.rst b/doc/api.rst
index 342ae08e1a4..f731ac1c59a 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -1096,6 +1096,17 @@ DataTree methods
 ..    Missing:
 ..    ``open_mfdatatree``
 
+Encoding/Decoding
+=================
+
+Coder objects
+-------------
+
+.. autosummary::
+   :toctree: generated/
+
+   coders.CFDatetimeCoder
+
 Coordinates objects
 ===================
 
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index ecf1702c356..324da980261 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -42,6 +42,9 @@ Deprecations
 - Finalize deprecation of ``closed`` parameters of :py:func:`cftime_range` and
   :py:func:`date_range` (:pull:`9882`).
   By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
+- Time decoding related kwarg ``use_cftime`` is deprecated. Use keyword argument
+  ``decode_times=CFDatetimeCoder(use_cftime=True)`` in the respective functions
+  instead.
 
 Bug fixes
 ~~~~~~~~~
diff --git a/xarray/__init__.py b/xarray/__init__.py
index 622c927b468..8af936ed27a 100644
--- a/xarray/__init__.py
+++ b/xarray/__init__.py
@@ -1,6 +1,6 @@
 from importlib.metadata import version as _version
 
-from xarray import groupers, testing, tutorial, ufuncs
+from xarray import coders, groupers, testing, tutorial, ufuncs
 from xarray.backends.api import (
     load_dataarray,
     load_dataset,
@@ -66,6 +66,7 @@
 # `mypy --strict` running in projects that import xarray.
 __all__ = (  # noqa: RUF022
     # Sub-packages
+    "coders",
     "groupers",
     "testing",
     "tutorial",
diff --git a/xarray/backends/api.py b/xarray/backends/api.py
index 785ab3913ef..12abb655e14 100644
--- a/xarray/backends/api.py
+++ b/xarray/backends/api.py
@@ -33,6 +33,7 @@
     _normalize_path,
 )
 from xarray.backends.locks import _get_scheduler
+from xarray.coders import CFDatetimeCoder
 from xarray.core import indexing
 from xarray.core.combine import (
     _infer_concat_order_from_positions,
@@ -481,7 +482,10 @@ def open_dataset(
     cache: bool | None = None,
     decode_cf: bool | None = None,
     mask_and_scale: bool | Mapping[str, bool] | None = None,
-    decode_times: bool | Mapping[str, bool] | None = None,
+    decode_times: bool
+    | CFDatetimeCoder
+    | Mapping[str, bool | CFDatetimeCoder]
+    | None = None,
     decode_timedelta: bool | Mapping[str, bool] | None = None,
     use_cftime: bool | Mapping[str, bool] | None = None,
     concat_characters: bool | Mapping[str, bool] | None = None,
@@ -543,9 +547,9 @@ def open_dataset(
         be replaced by NA. Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
-    decode_times : bool or dict-like, optional
+    decode_times : bool, CFDatetimeCoder or dict-like, optional
         If True, decode times encoded in the standard NetCDF datetime format
-        into datetime objects. Otherwise, leave them encoded as numbers.
+        into datetime objects. Otherwise, use CFDatetimeCoder or leave them encoded as numbers.
         Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
@@ -569,6 +573,8 @@ def open_dataset(
         raise an error. Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
+        Usage of 'use_cftime' as kwarg is deprecated. Please initialize it
+        with CFDatetimeCoder and 'decode_times' kwarg.
     concat_characters : bool or dict-like, optional
         If True, concatenate along the last dimension of character arrays to
         form string arrays. Dimensions will only be concatenated over (and
@@ -698,7 +704,10 @@ def open_dataarray(
     cache: bool | None = None,
     decode_cf: bool | None = None,
     mask_and_scale: bool | None = None,
-    decode_times: bool | None = None,
+    decode_times: bool
+    | CFDatetimeCoder
+    | Mapping[str, bool | CFDatetimeCoder]
+    | None = None,
     decode_timedelta: bool | None = None,
     use_cftime: bool | None = None,
     concat_characters: bool | None = None,
@@ -761,9 +770,11 @@ def open_dataarray(
         `missing_value` attribute contains multiple values a warning will be
         issued and all array values matching one of the multiple values will
         be replaced by NA. This keyword may not be supported by all the backends.
-    decode_times : bool, optional
+    decode_times : bool, CFDatetimeCoder or dict-like, optional
         If True, decode times encoded in the standard NetCDF datetime format
-        into datetime objects. Otherwise, leave them encoded as numbers.
+        into datetime objects. Otherwise, use CFDatetimeCoder or leave them encoded as numbers.
+        Pass a mapping, e.g. ``{"my_variable": False}``,
+        to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
     decode_timedelta : bool, optional
         If True, decode variables and coordinates with time units in
@@ -781,6 +792,8 @@ def open_dataarray(
         represented using ``np.datetime64[ns]`` objects.  If False, always
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error. This keyword may not be supported by all the backends.
+        Usage of 'use_cftime' as kwarg is deprecated. Please initialize it
+        with CFDatetimeCoder and 'decode_times' kwarg.
     concat_characters : bool, optional
         If True, concatenate along the last dimension of character arrays to
         form string arrays. Dimensions will only be concatenated over (and
@@ -903,7 +916,10 @@ def open_datatree(
     cache: bool | None = None,
     decode_cf: bool | None = None,
     mask_and_scale: bool | Mapping[str, bool] | None = None,
-    decode_times: bool | Mapping[str, bool] | None = None,
+    decode_times: bool
+    | CFDatetimeCoder
+    | Mapping[str, bool | CFDatetimeCoder]
+    | None = None,
     decode_timedelta: bool | Mapping[str, bool] | None = None,
     use_cftime: bool | Mapping[str, bool] | None = None,
     concat_characters: bool | Mapping[str, bool] | None = None,
@@ -961,9 +977,9 @@ def open_datatree(
         be replaced by NA. Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
-    decode_times : bool or dict-like, optional
+    decode_times : bool, CFDatetimeCoder or dict-like, optional
         If True, decode times encoded in the standard NetCDF datetime format
-        into datetime objects. Otherwise, leave them encoded as numbers.
+        into datetime objects. Otherwise, use CFDatetimeCoder or leave them encoded as numbers.
         Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
@@ -987,6 +1003,8 @@ def open_datatree(
         raise an error. Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
+        Usage of 'use_cftime' as kwarg is deprecated. Please initialize it
+        with CFDatetimeCoder and 'decode_times' kwarg.
     concat_characters : bool or dict-like, optional
         If True, concatenate along the last dimension of character arrays to
         form string arrays. Dimensions will only be concatenated over (and
@@ -1118,7 +1136,10 @@ def open_groups(
     cache: bool | None = None,
     decode_cf: bool | None = None,
     mask_and_scale: bool | Mapping[str, bool] | None = None,
-    decode_times: bool | Mapping[str, bool] | None = None,
+    decode_times: bool
+    | CFDatetimeCoder
+    | Mapping[str, bool | CFDatetimeCoder]
+    | None = None,
     decode_timedelta: bool | Mapping[str, bool] | None = None,
     use_cftime: bool | Mapping[str, bool] | None = None,
     concat_characters: bool | Mapping[str, bool] | None = None,
@@ -1180,9 +1201,9 @@ def open_groups(
         be replaced by NA. Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
-    decode_times : bool or dict-like, optional
+    decode_times : bool, CFDatetimeCoder or dict-like, optional
         If True, decode times encoded in the standard NetCDF datetime format
-        into datetime objects. Otherwise, leave them encoded as numbers.
+        into datetime objects. Otherwise, use CFDatetimeCoder or leave them encoded as numbers.
         Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
@@ -1206,6 +1227,8 @@ def open_groups(
         raise an error. Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
+        Usage of 'use_cftime' as kwarg is deprecated. Please initialize it
+        with CFDatetimeCoder and 'decode_times' kwarg.
     concat_characters : bool or dict-like, optional
         If True, concatenate along the last dimension of character arrays to
         form string arrays. Dimensions will only be concatenated over (and
diff --git a/xarray/coders.py b/xarray/coders.py
new file mode 100644
index 00000000000..238ac714780
--- /dev/null
+++ b/xarray/coders.py
@@ -0,0 +1,10 @@
+"""
+This module provides coder objects that encapsulate the
+"encoding/decoding" process.
+"""
+
+from xarray.coding.times import CFDatetimeCoder
+
+__all__ = [
+    "CFDatetimeCoder",
+]
diff --git a/xarray/coding/times.py b/xarray/coding/times.py
index 4622298e152..b6bd11b4490 100644
--- a/xarray/coding/times.py
+++ b/xarray/coding/times.py
@@ -36,7 +36,11 @@
 except ImportError:
     cftime = None
 
-from xarray.core.types import CFCalendar, NPDatetimeUnitOptions, T_DuckArray
+from xarray.core.types import (
+    CFCalendar,
+    NPDatetimeUnitOptions,
+    T_DuckArray,
+)
 
 T_Name = Union[Hashable, None]
 
@@ -204,7 +208,10 @@ def _unpack_time_units_and_ref_date(units: str) -> tuple[str, pd.Timestamp]:
 
 
 def _decode_cf_datetime_dtype(
-    data, units: str, calendar: str | None, use_cftime: bool | None
+    data,
+    units: str,
+    calendar: str | None,
+    use_cftime: bool | None,
 ) -> np.dtype:
     # Verify that at least the first and last date can be decoded
     # successfully. Otherwise, tracebacks end up swallowed by
@@ -311,7 +318,10 @@ def _decode_datetime_with_pandas(
 
 
 def decode_cf_datetime(
-    num_dates, units: str, calendar: str | None = None, use_cftime: bool | None = None
+    num_dates,
+    units: str,
+    calendar: str | None = None,
+    use_cftime: bool | None = None,
 ) -> np.ndarray:
     """Given an array of numeric dates in netCDF format, convert it into a
     numpy array of date time objects.
@@ -974,7 +984,10 @@ def _lazily_encode_cf_timedelta(
 
 
 class CFDatetimeCoder(VariableCoder):
-    def __init__(self, use_cftime: bool | None = None) -> None:
+    def __init__(
+        self,
+        use_cftime: bool | None = None,
+    ) -> None:
         self.use_cftime = use_cftime
 
     def encode(self, variable: Variable, name: T_Name = None) -> Variable:
diff --git a/xarray/conventions.py b/xarray/conventions.py
index 57407a15f51..042a7f14032 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -7,6 +7,7 @@
 
 import numpy as np
 
+from xarray.coders import CFDatetimeCoder
 from xarray.coding import strings, times, variables
 from xarray.coding.variables import SerializationWarning, pop_to
 from xarray.core import indexing
@@ -88,7 +89,7 @@ def encode_cf_variable(
     ensure_not_multiindex(var, name=name)
 
     for coder in [
-        times.CFDatetimeCoder(),
+        CFDatetimeCoder(),
         times.CFTimedeltaCoder(),
         variables.CFScaleOffsetCoder(),
         variables.CFMaskCoder(),
@@ -109,7 +110,7 @@ def decode_cf_variable(
     var: Variable,
     concat_characters: bool = True,
     mask_and_scale: bool = True,
-    decode_times: bool = True,
+    decode_times: bool | CFDatetimeCoder = True,
     decode_endianness: bool = True,
     stack_char_dim: bool = True,
     use_cftime: bool | None = None,
@@ -136,7 +137,7 @@ def decode_cf_variable(
         Lazily scale (using scale_factor and add_offset) and mask
         (using _FillValue). If the _Unsigned attribute is present
         treat integer arrays as unsigned.
-    decode_times : bool
+    decode_times : bool or CFDatetimeCoder
         Decode cf times ("hours since 2000-01-01") to np.datetime64.
     decode_endianness : bool
         Decode arrays from non-native to native endianness.
@@ -154,6 +155,8 @@ def decode_cf_variable(
         represented using ``np.datetime64[ns]`` objects.  If False, always
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error.
+        Usage of use_cftime as kwarg is deprecated, please initialize it with
+        CFDatetimeCoder and ``decode_times``.
 
     Returns
     -------
@@ -167,7 +170,7 @@ def decode_cf_variable(
     original_dtype = var.dtype
 
     if decode_timedelta is None:
-        decode_timedelta = decode_times
+        decode_timedelta = True if decode_times else False
 
     if concat_characters:
         if stack_char_dim:
@@ -191,7 +194,28 @@ def decode_cf_variable(
     if decode_timedelta:
         var = times.CFTimedeltaCoder().decode(var, name=name)
     if decode_times:
-        var = times.CFDatetimeCoder(use_cftime=use_cftime).decode(var, name=name)
+        # remove checks after end of deprecation cycle
+        if not isinstance(decode_times, CFDatetimeCoder):
+            if use_cftime is not None:
+                from warnings import warn
+
+                warn(
+                    "Usage of 'use_cftime' as kwarg is deprecated. "
+                    "Please initialize it with CFDatetimeCoder and "
+                    "'decode_times' kwarg.",
+                    DeprecationWarning,
+                    stacklevel=2,
+                )
+            decode_times = CFDatetimeCoder(use_cftime=use_cftime)
+        else:
+            if use_cftime is not None:
+                raise TypeError(
+                    "Usage of 'use_cftime' as kwarg is not allowed, "
+                    "if 'decode_times' is initialized with "
+                    "CFDatetimeCoder. Please add 'use_cftime' "
+                    "when initializing CFDatetimeCoder."
+                )
+        var = decode_times.decode(var, name=name)
 
     if decode_endianness and not var.dtype.isnative:
         var = variables.EndianCoder().decode(var)
@@ -302,7 +326,7 @@ def decode_cf_variables(
     attributes: T_Attrs,
     concat_characters: bool | Mapping[str, bool] = True,
     mask_and_scale: bool | Mapping[str, bool] = True,
-    decode_times: bool | Mapping[str, bool] = True,
+    decode_times: bool | CFDatetimeCoder | Mapping[str, bool | CFDatetimeCoder] = True,
     decode_coords: bool | Literal["coordinates", "all"] = True,
     drop_variables: T_DropVariables = None,
     use_cftime: bool | Mapping[str, bool] | None = None,
@@ -439,7 +463,7 @@ def decode_cf(
     obj: T_DatasetOrAbstractstore,
     concat_characters: bool = True,
     mask_and_scale: bool = True,
-    decode_times: bool = True,
+    decode_times: bool | CFDatetimeCoder = True,
     decode_coords: bool | Literal["coordinates", "all"] = True,
     drop_variables: T_DropVariables = None,
     use_cftime: bool | None = None,
@@ -458,7 +482,7 @@ def decode_cf(
     mask_and_scale : bool, optional
         Lazily scale (using scale_factor and add_offset) and mask
         (using _FillValue).
-    decode_times : bool, optional
+    decode_times : bool or CFDatetimeCoder, optional
         Decode cf times (e.g., integers since "hours since 2000-01-01") to
         np.datetime64.
     decode_coords : bool or {"coordinates", "all"}, optional
@@ -483,6 +507,8 @@ def decode_cf(
         represented using ``np.datetime64[ns]`` objects.  If False, always
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error.
+        Usage of use_cftime as kwarg is deprecated, please initialize it with
+        CFDatetimeCoder and ``decode_times``.
     decode_timedelta : bool, optional
         If True, decode variables and coordinates with time units in
         {"days", "hours", "minutes", "seconds", "milliseconds", "microseconds"}
@@ -536,7 +562,7 @@ def cf_decoder(
     attributes: T_Attrs,
     concat_characters: bool = True,
     mask_and_scale: bool = True,
-    decode_times: bool = True,
+    decode_times: bool | CFDatetimeCoder = True,
 ) -> tuple[T_Variables, T_Attrs]:
     """
     Decode a set of CF encoded variables and attributes.
@@ -553,7 +579,7 @@ def cf_decoder(
     mask_and_scale : bool
         Lazily scale (using scale_factor and add_offset) and mask
         (using _FillValue).
-    decode_times : bool
+    decode_times : bool | CFDatetimeCoder
         Decode cf times ("hours since 2000-01-01") to np.datetime64.
 
     Returns
diff --git a/xarray/convert.py b/xarray/convert.py
index 14df7cadb9b..29d8f9650e3 100644
--- a/xarray/convert.py
+++ b/xarray/convert.py
@@ -4,7 +4,8 @@
 
 import numpy as np
 
-from xarray.coding.times import CFDatetimeCoder, CFTimedeltaCoder
+from xarray.coders import CFDatetimeCoder
+from xarray.coding.times import CFTimedeltaCoder
 from xarray.conventions import decode_cf
 from xarray.core import duck_array_ops
 from xarray.core.dataarray import DataArray
diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py
index ff254225321..31e06e8efd8 100644
--- a/xarray/tests/test_backends.py
+++ b/xarray/tests/test_backends.py
@@ -48,6 +48,7 @@
 )
 from xarray.backends.pydap_ import PydapDataStore
 from xarray.backends.scipy_ import ScipyBackendEntrypoint
+from xarray.coders import CFDatetimeCoder
 from xarray.coding.cftime_offsets import cftime_range
 from xarray.coding.strings import check_vlen_dtype, create_vlen_dtype
 from xarray.coding.variables import SerializationWarning
@@ -3206,7 +3207,10 @@ def test_open_zarr_use_cftime(self) -> None:
             ds.to_zarr(store_target, **self.version_kwargs)
             ds_a = xr.open_zarr(store_target, **self.version_kwargs)
             assert_identical(ds, ds_a)
-            ds_b = xr.open_zarr(store_target, use_cftime=True, **self.version_kwargs)
+            decoder = CFDatetimeCoder(use_cftime=True)
+            ds_b = xr.open_zarr(
+                store_target, decode_times=decoder, **self.version_kwargs
+            )
             assert xr.coding.times.contains_cftime_datetimes(ds_b.time.variable)
 
     def test_write_read_select_write(self) -> None:
@@ -5622,7 +5626,8 @@ def test_use_cftime_true(calendar, units_year) -> None:
     with create_tmp_file() as tmp_file:
         original.to_netcdf(tmp_file)
         with warnings.catch_warnings(record=True) as record:
-            with open_dataset(tmp_file, use_cftime=True) as ds:
+            decoder = CFDatetimeCoder(use_cftime=True)
+            with open_dataset(tmp_file, decode_times=decoder) as ds:
                 assert_identical(expected_x, ds.x)
                 assert_identical(expected_time, ds.time)
             _assert_no_dates_out_of_range_warning(record)
@@ -5674,7 +5679,8 @@ def test_use_cftime_false_standard_calendar_out_of_range(calendar, units_year) -
     with create_tmp_file() as tmp_file:
         original.to_netcdf(tmp_file)
         with pytest.raises((OutOfBoundsDatetime, ValueError)):
-            open_dataset(tmp_file, use_cftime=False)
+            decoder = CFDatetimeCoder(use_cftime=False)
+            open_dataset(tmp_file, decode_times=decoder)
 
 
 @requires_scipy_or_netCDF4
@@ -5692,7 +5698,8 @@ def test_use_cftime_false_nonstandard_calendar(calendar, units_year) -> None:
     with create_tmp_file() as tmp_file:
         original.to_netcdf(tmp_file)
         with pytest.raises((OutOfBoundsDatetime, ValueError)):
-            open_dataset(tmp_file, use_cftime=False)
+            decoder = CFDatetimeCoder(use_cftime=False)
+            open_dataset(tmp_file, decode_times=decoder)
 
 
 @pytest.mark.parametrize("engine", ["netcdf4", "scipy"])
diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py
index 9a51ca40d07..e05d303e17b 100644
--- a/xarray/tests/test_coding_times.py
+++ b/xarray/tests/test_coding_times.py
@@ -19,9 +19,9 @@
     date_range,
     decode_cf,
 )
+from xarray.coders import CFDatetimeCoder
 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,
@@ -123,7 +123,11 @@ def _all_cftime_date_types():
 @pytest.mark.filterwarnings("ignore:Ambiguous reference date string")
 @pytest.mark.filterwarnings("ignore:Times can't be serialized faithfully")
 @pytest.mark.parametrize(["num_dates", "units", "calendar"], _CF_DATETIME_TESTS)
-def test_cf_datetime(num_dates, units, calendar) -> None:
+def test_cf_datetime(
+    num_dates,
+    units,
+    calendar,
+) -> None:
     import cftime
 
     expected = cftime.num2date(
@@ -167,8 +171,8 @@ def test_decode_cf_datetime_overflow() -> None:
     units = "days since 2000-01-01 00:00:00"
 
     # date after 2262 and before 1678
-    days = (-117608, 95795)
-    expected = (datetime(1677, 12, 31), datetime(2262, 4, 12))
+    days = (-117710, 95795)
+    expected = (datetime(1677, 9, 20), datetime(2262, 4, 12))
 
     for i, day in enumerate(days):
         with warnings.catch_warnings():
@@ -277,15 +281,15 @@ def test_decode_dates_outside_timestamp_range(calendar) -> None:
 
 @requires_cftime
 @pytest.mark.parametrize("calendar", _STANDARD_CALENDARS)
+@pytest.mark.parametrize("num_time", [735368, [735368], [[735368]]])
 def test_decode_standard_calendar_single_element_inside_timestamp_range(
-    calendar,
+    calendar, num_time
 ) -> None:
     units = "days since 0001-01-01"
-    for num_time in [735368, [735368], [[735368]]]:
-        with warnings.catch_warnings():
-            warnings.filterwarnings("ignore", "Unable to decode time axis")
-            actual = decode_cf_datetime(num_time, units, calendar=calendar)
-        assert actual.dtype == np.dtype("M8[ns]")
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", "Unable to decode time axis")
+        actual = decode_cf_datetime(num_time, units, calendar=calendar)
+    assert actual.dtype == np.dtype("M8[ns]")
 
 
 @requires_cftime
@@ -628,10 +632,10 @@ def test_cf_timedelta_2d() -> None:
 @pytest.mark.parametrize(
     ["deltas", "expected"],
     [
-        (pd.to_timedelta(["1 day", "2 days"]), "days"),  # type: ignore[arg-type, unused-ignore]
-        (pd.to_timedelta(["1 day", "2 days"]), "days"),  # type: ignore[arg-type, unused-ignore]
-        (pd.to_timedelta(["1 day", "2 days"]), "days"),  # type: ignore[arg-type, unused-ignore]
-        (pd.to_timedelta(["1 day", "2 days"]), "days"),  # type: ignore[arg-type, unused-ignore]
+        (pd.to_timedelta(["1 day", "2 days"]), "days"),
+        (pd.to_timedelta(["1h", "1 day 1 hour"]), "hours"),
+        (pd.to_timedelta(["1m", "2m", np.nan]), "minutes"),
+        (pd.to_timedelta(["1m3s", "1m4s"]), "seconds"),
     ],
 )
 def test_infer_timedelta_units(deltas, expected) -> None:
@@ -675,7 +679,7 @@ def test_decode_cf(calendar) -> None:
         if calendar not in _STANDARD_CALENDARS:
             assert ds.test.dtype == np.dtype("O")
         else:
-            assert ds.test.dtype == np.dtype("M8[ns]")
+            assert ds.test.dtype == np.dtype("=M8[ns]")
 
 
 def test_decode_cf_time_bounds() -> None:
@@ -700,7 +704,7 @@ def test_decode_cf_time_bounds() -> None:
         "calendar": "standard",
     }
     dsc = decode_cf(ds)
-    assert dsc.time_bnds.dtype == np.dtype("M8[ns]")
+    assert dsc.time_bnds.dtype == np.dtype("=M8[ns]")
     dsc = decode_cf(ds, decode_times=False)
     assert dsc.time_bnds.dtype == np.dtype("int64")
 
@@ -1072,7 +1076,8 @@ def test_encode_decode_roundtrip_cftime(freq) -> None:
     )
     variable = Variable(["time"], times)
     encoded = conventions.encode_cf_variable(variable)
-    decoded = conventions.decode_cf_variable("time", encoded, use_cftime=True)
+    decoder = CFDatetimeCoder(use_cftime=True)
+    decoded = conventions.decode_cf_variable("time", encoded, decode_times=decoder)
     assert_equal(variable, decoded)
 
 
@@ -1182,7 +1187,7 @@ def test_decode_0size_datetime(use_cftime):
     if use_cftime and not has_cftime:
         pytest.skip()
 
-    dtype = object if use_cftime else "M8[ns]"
+    dtype = object if use_cftime else "=M8[ns]"
     expected = np.array([], dtype=dtype)
     actual = decode_cf_datetime(
         np.zeros(shape=0, dtype=np.int64),
@@ -1209,6 +1214,28 @@ def test_decode_float_datetime():
     np.testing.assert_equal(actual, expected)
 
 
+def test_decode_float_datetime_with_decimals() -> None:
+    # test resolution enhancement for floats
+    values = np.array([0, 0.125, 0.25, 0.375, 0.75, 1.0], dtype="float32")
+    expected = np.array(
+        [
+            "2000-01-01T00:00:00.000",
+            "2000-01-01T00:00:00.125",
+            "2000-01-01T00:00:00.250",
+            "2000-01-01T00:00:00.375",
+            "2000-01-01T00:00:00.750",
+            "2000-01-01T00:00:01.000",
+        ],
+        dtype="=M8[ns]",
+    )
+
+    units = "seconds since 2000-01-01"
+    calendar = "standard"
+    actual = decode_cf_datetime(values, units, calendar)
+    assert actual.dtype == expected.dtype
+    np.testing.assert_equal(actual, expected)
+
+
 @requires_cftime
 def test_scalar_unit() -> None:
     # test that a scalar units (often NaN when using to_netcdf) does not raise an error
diff --git a/xarray/tests/test_conventions.py b/xarray/tests/test_conventions.py
index 495d760c534..7616f12957f 100644
--- a/xarray/tests/test_conventions.py
+++ b/xarray/tests/test_conventions.py
@@ -446,7 +446,9 @@ def test_dataset_repr_with_netcdf4_datetimes(self) -> None:
             assert "(time) object" in repr(ds)
 
         attrs = {"units": "days since 1900-01-01"}
-        ds = decode_cf(Dataset({"time": ("time", [0, 1], attrs)}))
+        ds = decode_cf(
+            Dataset({"time": ("time", [0, 1], attrs)}),
+        )
         assert "(time) datetime64[ns]" in repr(ds)
 
     @requires_cftime
diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py
index ea5186e59d0..a14f9990dca 100644
--- a/xarray/tests/test_dataarray.py
+++ b/xarray/tests/test_dataarray.py
@@ -30,7 +30,7 @@
     broadcast,
     set_options,
 )
-from xarray.coding.times import CFDatetimeCoder
+from xarray.coders import CFDatetimeCoder
 from xarray.core import dtypes
 from xarray.core.common import full_like
 from xarray.core.coordinates import Coordinates

From d6fe95683d980017e6827a64c16f01c1a92faad7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kai.muehlbauer@uni-bonn.de>
Date: Tue, 17 Dec 2024 16:53:37 +0100
Subject: [PATCH 02/14] add whats-new.rst entry

---
 doc/whats-new.rst | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 324da980261..6d4dead1153 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -27,6 +27,9 @@ New Features
 - Add ``unit`` - keyword argument to :py:func:`date_range` and ``microsecond`` parsing to
   iso8601-parser (:pull:`9885`).
   By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
+- Split out ``CFDatetimeCoder`` in ``xr.coders``, make ``decode_times`` keyword argument
+  consume ``CFDatetimeCoder``.
+
 
 Breaking changes
 ~~~~~~~~~~~~~~~~
@@ -45,6 +48,7 @@ Deprecations
 - Time decoding related kwarg ``use_cftime`` is deprecated. Use keyword argument
   ``decode_times=CFDatetimeCoder(use_cftime=True)`` in the respective functions
   instead.
+  By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
 
 Bug fixes
 ~~~~~~~~~

From bd6a5d1e5403aba71191c8c4013d216f4f3be799 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kmuehlbauer@wradlib.org>
Date: Tue, 17 Dec 2024 17:19:45 +0100
Subject: [PATCH 03/14] Apply suggestions from code review

Co-authored-by: Deepak Cherian <dcherian@users.noreply.github.com>
---
 doc/whats-new.rst      | 5 ++---
 xarray/backends/api.py | 6 +++---
 xarray/conventions.py  | 2 +-
 3 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 6d4dead1153..b64a801e0a3 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -27,7 +27,7 @@ New Features
 - Add ``unit`` - keyword argument to :py:func:`date_range` and ``microsecond`` parsing to
   iso8601-parser (:pull:`9885`).
   By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
-- Split out ``CFDatetimeCoder`` in ``xr.coders``, make ``decode_times`` keyword argument
+- Split out ``CFDatetimeCoder`` as public API in ``xr.coders``, make ``decode_times`` keyword argument
   consume ``CFDatetimeCoder``.
 
 
@@ -46,8 +46,7 @@ Deprecations
   :py:func:`date_range` (:pull:`9882`).
   By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
 - Time decoding related kwarg ``use_cftime`` is deprecated. Use keyword argument
-  ``decode_times=CFDatetimeCoder(use_cftime=True)`` in the respective functions
-  instead.
+  ``decode_times=CFDatetimeCoder(use_cftime=True)`` in :py:func:`~xarray.open_dataset`, :py:func:`~xarray.open_dataarray`, :py:func:`~xarray.open_datatree`, :py:func:`~xarray.open_groups`, :py:func:`~xarray.open_zarr` and :py:func:`~xarray.decode_cf` instead.
   By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
 
 Bug fixes
diff --git a/xarray/backends/api.py b/xarray/backends/api.py
index 12abb655e14..4b71c6d2f91 100644
--- a/xarray/backends/api.py
+++ b/xarray/backends/api.py
@@ -549,7 +549,7 @@ def open_dataset(
         This keyword may not be supported by all the backends.
     decode_times : bool, CFDatetimeCoder or dict-like, optional
         If True, decode times encoded in the standard NetCDF datetime format
-        into datetime objects. Otherwise, use CFDatetimeCoder or leave them encoded as numbers.
+        into datetime objects. Otherwise, use ``CFDatetimeCoder`` or leave them encoded as numbers.
         Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
@@ -573,8 +573,8 @@ def open_dataset(
         raise an error. Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
-        Usage of 'use_cftime' as kwarg is deprecated. Please initialize it
-        with CFDatetimeCoder and 'decode_times' kwarg.
+        .. deprecated:: 2024.12.0
+           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
     concat_characters : bool or dict-like, optional
         If True, concatenate along the last dimension of character arrays to
         form string arrays. Dimensions will only be concatenated over (and
diff --git a/xarray/conventions.py b/xarray/conventions.py
index 042a7f14032..1c2759cd631 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -199,7 +199,7 @@ def decode_cf_variable(
             if use_cftime is not None:
                 from warnings import warn
 
-                warn(
+                emit_user_level_warning(
                     "Usage of 'use_cftime' as kwarg is deprecated. "
                     "Please initialize it with CFDatetimeCoder and "
                     "'decode_times' kwarg.",

From 6557ef97e398840a19291e49a4d474f9b7b8f0a0 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 17 Dec 2024 16:20:05 +0000
Subject: [PATCH 04/14] [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
---
 xarray/conventions.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/xarray/conventions.py b/xarray/conventions.py
index 1c2759cd631..e213c4258cc 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -197,7 +197,6 @@ def decode_cf_variable(
         # remove checks after end of deprecation cycle
         if not isinstance(decode_times, CFDatetimeCoder):
             if use_cftime is not None:
-                from warnings import warn
 
                 emit_user_level_warning(
                     "Usage of 'use_cftime' as kwarg is deprecated. "

From 759fb72dc8122049270258fc589ac28313f72aad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kai.muehlbauer@uni-bonn.de>
Date: Tue, 17 Dec 2024 17:25:08 +0100
Subject: [PATCH 05/14] fix warning

---
 xarray/conventions.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/xarray/conventions.py b/xarray/conventions.py
index e213c4258cc..7c076cfa3da 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -197,13 +197,11 @@ def decode_cf_variable(
         # remove checks after end of deprecation cycle
         if not isinstance(decode_times, CFDatetimeCoder):
             if use_cftime is not None:
-
                 emit_user_level_warning(
                     "Usage of 'use_cftime' as kwarg is deprecated. "
                     "Please initialize it with CFDatetimeCoder and "
                     "'decode_times' kwarg.",
                     DeprecationWarning,
-                    stacklevel=2,
                 )
             decode_times = CFDatetimeCoder(use_cftime=use_cftime)
         else:

From 21181917d0296cdd26c02aad4a4a4a5857e007ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kai.muehlbauer@uni-bonn.de>
Date: Tue, 17 Dec 2024 17:53:07 +0100
Subject: [PATCH 06/14] fix docstrings

---
 xarray/backends/api.py | 23 ++++++++++++++++-------
 xarray/conventions.py  |  5 +++--
 2 files changed, 19 insertions(+), 9 deletions(-)

diff --git a/xarray/backends/api.py b/xarray/backends/api.py
index 4b71c6d2f91..603db4b8e54 100644
--- a/xarray/backends/api.py
+++ b/xarray/backends/api.py
@@ -549,7 +549,8 @@ def open_dataset(
         This keyword may not be supported by all the backends.
     decode_times : bool, CFDatetimeCoder or dict-like, optional
         If True, decode times encoded in the standard NetCDF datetime format
-        into datetime objects. Otherwise, use ``CFDatetimeCoder`` or leave them encoded as numbers.
+        into datetime objects. Otherwise, use ``CFDatetimeCoder`` or leave them
+        encoded as numbers.
         Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
@@ -573,8 +574,10 @@ def open_dataset(
         raise an error. Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
+
         .. deprecated:: 2024.12.0
            Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+
     concat_characters : bool or dict-like, optional
         If True, concatenate along the last dimension of character arrays to
         form string arrays. Dimensions will only be concatenated over (and
@@ -792,8 +795,10 @@ def open_dataarray(
         represented using ``np.datetime64[ns]`` objects.  If False, always
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error. This keyword may not be supported by all the backends.
-        Usage of 'use_cftime' as kwarg is deprecated. Please initialize it
-        with CFDatetimeCoder and 'decode_times' kwarg.
+
+        .. deprecated:: 2024.12.0
+           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+
     concat_characters : bool, optional
         If True, concatenate along the last dimension of character arrays to
         form string arrays. Dimensions will only be concatenated over (and
@@ -1003,8 +1008,10 @@ def open_datatree(
         raise an error. Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
-        Usage of 'use_cftime' as kwarg is deprecated. Please initialize it
-        with CFDatetimeCoder and 'decode_times' kwarg.
+
+        .. deprecated:: 2024.12.0
+           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+
     concat_characters : bool or dict-like, optional
         If True, concatenate along the last dimension of character arrays to
         form string arrays. Dimensions will only be concatenated over (and
@@ -1227,8 +1234,10 @@ def open_groups(
         raise an error. Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
-        Usage of 'use_cftime' as kwarg is deprecated. Please initialize it
-        with CFDatetimeCoder and 'decode_times' kwarg.
+
+        .. deprecated:: 2024.12.0
+           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+
     concat_characters : bool or dict-like, optional
         If True, concatenate along the last dimension of character arrays to
         form string arrays. Dimensions will only be concatenated over (and
diff --git a/xarray/conventions.py b/xarray/conventions.py
index 7c076cfa3da..e7bb1041a99 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -155,8 +155,9 @@ def decode_cf_variable(
         represented using ``np.datetime64[ns]`` objects.  If False, always
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error.
-        Usage of use_cftime as kwarg is deprecated, please initialize it with
-        CFDatetimeCoder and ``decode_times``.
+
+        .. deprecated:: 2024.12.0
+           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
 
     Returns
     -------

From 262295a3490fb2420ea9098ccec44d6d4466346b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kai.muehlbauer@uni-bonn.de>
Date: Tue, 17 Dec 2024 18:28:34 +0100
Subject: [PATCH 07/14] try fix typing

---
 xarray/conventions.py | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

diff --git a/xarray/conventions.py b/xarray/conventions.py
index e7bb1041a99..e90902b3f47 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -310,9 +310,10 @@ def _update_bounds_encoding(variables: T_Variables) -> None:
 
 
 T = TypeVar("T")
+U = TypeVar("U")
 
 
-def _item_or_default(obj: Mapping[Any, T] | T, key: Hashable, default: T) -> T:
+def _item_or_default(obj: Mapping[Any, T | U] | T, key: Hashable, default: T) -> T | U:
     """
     Return item by key if obj is mapping and key is present, else return default value.
     """
@@ -461,7 +462,7 @@ def decode_cf(
     obj: T_DatasetOrAbstractstore,
     concat_characters: bool = True,
     mask_and_scale: bool = True,
-    decode_times: bool | CFDatetimeCoder = True,
+    decode_times: bool | CFDatetimeCoder | Mapping[str, bool | CFDatetimeCoder] = True,
     decode_coords: bool | Literal["coordinates", "all"] = True,
     drop_variables: T_DropVariables = None,
     use_cftime: bool | None = None,
@@ -480,7 +481,7 @@ def decode_cf(
     mask_and_scale : bool, optional
         Lazily scale (using scale_factor and add_offset) and mask
         (using _FillValue).
-    decode_times : bool or CFDatetimeCoder, optional
+    decode_times : bool | CFDatetimeCoder | Mapping[str, bool | CFDatetimeCoder], optional
         Decode cf times (e.g., integers since "hours since 2000-01-01") to
         np.datetime64.
     decode_coords : bool or {"coordinates", "all"}, optional
@@ -505,8 +506,10 @@ def decode_cf(
         represented using ``np.datetime64[ns]`` objects.  If False, always
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error.
-        Usage of use_cftime as kwarg is deprecated, please initialize it with
-        CFDatetimeCoder and ``decode_times``.
+
+        .. deprecated:: 2024.12.0
+           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+
     decode_timedelta : bool, optional
         If True, decode variables and coordinates with time units in
         {"days", "hours", "minutes", "seconds", "milliseconds", "microseconds"}
@@ -560,7 +563,7 @@ def cf_decoder(
     attributes: T_Attrs,
     concat_characters: bool = True,
     mask_and_scale: bool = True,
-    decode_times: bool | CFDatetimeCoder = True,
+    decode_times: bool | CFDatetimeCoder | Mapping[str, bool | CFDatetimeCoder] = True,
 ) -> tuple[T_Variables, T_Attrs]:
     """
     Decode a set of CF encoded variables and attributes.
@@ -577,7 +580,7 @@ def cf_decoder(
     mask_and_scale : bool
         Lazily scale (using scale_factor and add_offset) and mask
         (using _FillValue).
-    decode_times : bool | CFDatetimeCoder
+    decode_times : bool | CFDatetimeCoder | Mapping[str, bool | CFDatetimeCoder]
         Decode cf times ("hours since 2000-01-01") to np.datetime64.
 
     Returns

From adebafa0cecd331de501da2c3c3b35173be53f71 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kmuehlbauer@wradlib.org>
Date: Mon, 30 Dec 2024 12:06:37 +0100
Subject: [PATCH 08/14] Apply suggestions from code review

Co-authored-by: Spencer Clark <spencerkclark@gmail.com>
---
 doc/whats-new.rst      |  4 ++--
 xarray/backends/api.py | 14 +++++++-------
 xarray/conventions.py  | 17 ++++++++++-------
 3 files changed, 19 insertions(+), 16 deletions(-)

diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 17910753bdd..c16283b68ae 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -35,8 +35,8 @@ New Features
 - Add ``unit`` - keyword argument to :py:func:`date_range` and ``microsecond`` parsing to
   iso8601-parser (:pull:`9885`).
   By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
-- Split out ``CFDatetimeCoder`` as public API in ``xr.coders``, make ``decode_times`` keyword argument
-  consume ``CFDatetimeCoder``.
+- Split out :py:class:`coders.CFDatetimeCoder` as public API in ``xr.coders``, make ``decode_times`` keyword argument
+  consume :py:class:`coders.CFDatetimeCoder`.
 
 
 Breaking changes
diff --git a/xarray/backends/api.py b/xarray/backends/api.py
index 603db4b8e54..675e891fe68 100644
--- a/xarray/backends/api.py
+++ b/xarray/backends/api.py
@@ -549,7 +549,7 @@ def open_dataset(
         This keyword may not be supported by all the backends.
     decode_times : bool, CFDatetimeCoder or dict-like, optional
         If True, decode times encoded in the standard NetCDF datetime format
-        into datetime objects. Otherwise, use ``CFDatetimeCoder`` or leave them
+        into datetime objects. Otherwise, use :py:class:`coders.CFDatetimeCoder` or leave them
         encoded as numbers.
         Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
@@ -576,7 +576,7 @@ def open_dataset(
         This keyword may not be supported by all the backends.
 
         .. deprecated:: 2024.12.0
-           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+           Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool or dict-like, optional
         If True, concatenate along the last dimension of character arrays to
@@ -775,7 +775,7 @@ def open_dataarray(
         be replaced by NA. This keyword may not be supported by all the backends.
     decode_times : bool, CFDatetimeCoder or dict-like, optional
         If True, decode times encoded in the standard NetCDF datetime format
-        into datetime objects. Otherwise, use CFDatetimeCoder or leave them encoded as numbers.
+        into datetime objects. Otherwise, use :py:class:`coders.CFDatetimeCoder` or leave them encoded as numbers.
         Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
@@ -797,7 +797,7 @@ def open_dataarray(
         raise an error. This keyword may not be supported by all the backends.
 
         .. deprecated:: 2024.12.0
-           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+           Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool, optional
         If True, concatenate along the last dimension of character arrays to
@@ -984,7 +984,7 @@ def open_datatree(
         This keyword may not be supported by all the backends.
     decode_times : bool, CFDatetimeCoder or dict-like, optional
         If True, decode times encoded in the standard NetCDF datetime format
-        into datetime objects. Otherwise, use CFDatetimeCoder or leave them encoded as numbers.
+        into datetime objects. Otherwise, use :py:class:`coders.CFDatetimeCoder` or leave them encoded as numbers.
         Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
@@ -1210,7 +1210,7 @@ def open_groups(
         This keyword may not be supported by all the backends.
     decode_times : bool, CFDatetimeCoder or dict-like, optional
         If True, decode times encoded in the standard NetCDF datetime format
-        into datetime objects. Otherwise, use CFDatetimeCoder or leave them encoded as numbers.
+        into datetime objects. Otherwise, use :py:class:`coders.CFDatetimeCoder` or leave them encoded as numbers.
         Pass a mapping, e.g. ``{"my_variable": False}``,
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
@@ -1236,7 +1236,7 @@ def open_groups(
         This keyword may not be supported by all the backends.
 
         .. deprecated:: 2024.12.0
-           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+           Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool or dict-like, optional
         If True, concatenate along the last dimension of character arrays to
diff --git a/xarray/conventions.py b/xarray/conventions.py
index e90902b3f47..b8b969ce34d 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -157,7 +157,7 @@ def decode_cf_variable(
         raise an error.
 
         .. deprecated:: 2024.12.0
-           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+           Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     Returns
     -------
@@ -201,17 +201,20 @@ def decode_cf_variable(
                 emit_user_level_warning(
                     "Usage of 'use_cftime' as kwarg is deprecated. "
                     "Please initialize it with CFDatetimeCoder and "
-                    "'decode_times' kwarg.",
+                    "'decode_times' kwarg.\n",
+                    "Example usage:\n",
+                    "    time_coder = xr.coders.CFDatetimeCoder(use_cftime=True)\n",
+                    "    ds = xr.open_dataset(decode_times=time_coder)\n",
                     DeprecationWarning,
                 )
             decode_times = CFDatetimeCoder(use_cftime=use_cftime)
         else:
             if use_cftime is not None:
                 raise TypeError(
-                    "Usage of 'use_cftime' as kwarg is not allowed, "
-                    "if 'decode_times' is initialized with "
-                    "CFDatetimeCoder. Please add 'use_cftime' "
-                    "when initializing CFDatetimeCoder."
+                    "Usage of 'use_cftime' as a kwarg is not allowed "
+                    "if a CFDatetimeCoder instance is passed to "
+                    "decode_times. Please set use_cftime "
+                    "when initializing CFDatetimeCoder instead."
                 )
         var = decode_times.decode(var, name=name)
 
@@ -508,7 +511,7 @@ def decode_cf(
         raise an error.
 
         .. deprecated:: 2024.12.0
-           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+           Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     decode_timedelta : bool, optional
         If True, decode variables and coordinates with time units in

From 6cd81e57c01cea046272b658154ab54e7fdda63a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kmuehlbauer@wradlib.org>
Date: Mon, 30 Dec 2024 12:10:09 +0100
Subject: [PATCH 09/14] Apply suggestions from code review

Co-authored-by: Spencer Clark <spencerkclark@gmail.com>
---
 xarray/backends/api.py | 2 +-
 xarray/conventions.py  | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/xarray/backends/api.py b/xarray/backends/api.py
index 675e891fe68..40b3ba1ffba 100644
--- a/xarray/backends/api.py
+++ b/xarray/backends/api.py
@@ -1010,7 +1010,7 @@ def open_datatree(
         This keyword may not be supported by all the backends.
 
         .. deprecated:: 2024.12.0
-           Please initialize it with ``CFDatetimeCoder`` and ``decode_times`` kwarg.
+           Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool or dict-like, optional
         If True, concatenate along the last dimension of character arrays to
diff --git a/xarray/conventions.py b/xarray/conventions.py
index b8b969ce34d..01a50fa9a9b 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -199,9 +199,9 @@ def decode_cf_variable(
         if not isinstance(decode_times, CFDatetimeCoder):
             if use_cftime is not None:
                 emit_user_level_warning(
-                    "Usage of 'use_cftime' as kwarg is deprecated. "
-                    "Please initialize it with CFDatetimeCoder and "
-                    "'decode_times' kwarg.\n",
+                    "Usage of 'use_cftime' as a kwarg is deprecated. "
+                    "Please pass a CFDatetimeCoder instance initialized "
+                    "with use_cftime to the decode_times kwarg instead.\n",
                     "Example usage:\n",
                     "    time_coder = xr.coders.CFDatetimeCoder(use_cftime=True)\n",
                     "    ds = xr.open_dataset(decode_times=time_coder)\n",

From 1cec644710372ad09717df5f326b80f0da45d0bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kmuehlbauer@wradlib.org>
Date: Mon, 30 Dec 2024 12:16:06 +0100
Subject: [PATCH 10/14] Update xarray/conventions.py

---
 xarray/conventions.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/xarray/conventions.py b/xarray/conventions.py
index 01a50fa9a9b..2d9d0e11d6d 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -201,9 +201,9 @@ def decode_cf_variable(
                 emit_user_level_warning(
                     "Usage of 'use_cftime' as a kwarg is deprecated. "
                     "Please pass a CFDatetimeCoder instance initialized "
-                    "with use_cftime to the decode_times kwarg instead.\n",
-                    "Example usage:\n",
-                    "    time_coder = xr.coders.CFDatetimeCoder(use_cftime=True)\n",
+                    "with use_cftime to the decode_times kwarg instead.\n"
+                    "Example usage:\n"
+                    "    time_coder = xr.coders.CFDatetimeCoder(use_cftime=True)\n"
                     "    ds = xr.open_dataset(decode_times=time_coder)\n",
                     DeprecationWarning,
                 )

From 33a1563f7c2b017985aa7eba07175038dc295fdb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kmuehlbauer@wradlib.org>
Date: Thu, 2 Jan 2025 08:02:44 +0100
Subject: [PATCH 11/14] Update deprecated directive

---
 xarray/backends/api.py | 8 ++++----
 xarray/conventions.py  | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/xarray/backends/api.py b/xarray/backends/api.py
index 089d215b371..d8e3729528c 100644
--- a/xarray/backends/api.py
+++ b/xarray/backends/api.py
@@ -575,7 +575,7 @@ def open_dataset(
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
 
-        .. deprecated:: 2024.12.0
+        .. deprecated:: 2025.01.0
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool or dict-like, optional
@@ -796,7 +796,7 @@ def open_dataarray(
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error. This keyword may not be supported by all the backends.
 
-        .. deprecated:: 2024.12.0
+        .. deprecated:: 2025.01.0
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool, optional
@@ -1009,7 +1009,7 @@ def open_datatree(
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
 
-        .. deprecated:: 2024.12.0
+        .. deprecated:: 2025.01.0
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool or dict-like, optional
@@ -1235,7 +1235,7 @@ def open_groups(
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
 
-        .. deprecated:: 2024.12.0
+        .. deprecated:: 2025.01.0
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool or dict-like, optional
diff --git a/xarray/conventions.py b/xarray/conventions.py
index 2d9d0e11d6d..f0fef471c26 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -156,7 +156,7 @@ def decode_cf_variable(
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error.
 
-        .. deprecated:: 2024.12.0
+        .. deprecated:: 2025.01.0
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     Returns
@@ -510,7 +510,7 @@ def decode_cf(
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error.
 
-        .. deprecated:: 2024.12.0
+        .. deprecated:: 2025.01.0
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     decode_timedelta : bool, optional

From 25be3fe1c57a4a6d9c108973f3d64943f1b93817 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kmuehlbauer@wradlib.org>
Date: Sat, 4 Jan 2025 10:01:13 +0100
Subject: [PATCH 12/14] fix docstrings/whats-new.rst after merge

---
 doc/whats-new.rst      | 4 ++--
 xarray/backends/api.py | 8 ++++----
 xarray/conventions.py  | 4 ++--
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 1688c6c60b0..1c16c6287f7 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -22,6 +22,8 @@ v2025.01.1 (unreleased)
 
 New Features
 ~~~~~~~~~~~~
+- Split out :py:class:`coders.CFDatetimeCoder` as public API in ``xr.coders``, make ``decode_times`` keyword argument
+  consume :py:class:`coders.CFDatetimeCoder`.
 
 
 Breaking changes
@@ -69,8 +71,6 @@ New Features
 - Add ``unit`` - keyword argument to :py:func:`date_range` and ``microsecond`` parsing to
   iso8601-parser (:pull:`9885`).
   By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
-- Split out :py:class:`coders.CFDatetimeCoder` as public API in ``xr.coders``, make ``decode_times`` keyword argument
-  consume :py:class:`coders.CFDatetimeCoder`.
 
 
 Breaking changes
diff --git a/xarray/backends/api.py b/xarray/backends/api.py
index d8e3729528c..2adcc57c6b9 100644
--- a/xarray/backends/api.py
+++ b/xarray/backends/api.py
@@ -575,7 +575,7 @@ def open_dataset(
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
 
-        .. deprecated:: 2025.01.0
+        .. deprecated:: 2025.01.1
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool or dict-like, optional
@@ -796,7 +796,7 @@ def open_dataarray(
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error. This keyword may not be supported by all the backends.
 
-        .. deprecated:: 2025.01.0
+        .. deprecated:: 2025.01.1
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool, optional
@@ -1009,7 +1009,7 @@ def open_datatree(
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
 
-        .. deprecated:: 2025.01.0
+        .. deprecated:: 2025.01.1
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool or dict-like, optional
@@ -1235,7 +1235,7 @@ def open_groups(
         to toggle this feature per-variable individually.
         This keyword may not be supported by all the backends.
 
-        .. deprecated:: 2025.01.0
+        .. deprecated:: 2025.01.1
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     concat_characters : bool or dict-like, optional
diff --git a/xarray/conventions.py b/xarray/conventions.py
index f0fef471c26..8f216146935 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -156,7 +156,7 @@ def decode_cf_variable(
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error.
 
-        .. deprecated:: 2025.01.0
+        .. deprecated:: 2025.01.1
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     Returns
@@ -510,7 +510,7 @@ def decode_cf(
         decode times to ``np.datetime64[ns]`` objects; if this is not possible
         raise an error.
 
-        .. deprecated:: 2025.01.0
+        .. deprecated:: 2025.01.1
            Please pass a :py:class:`coders.CFDatetimeCoder` instance initialized with ``use_cftime`` to the ``decode_times`` kwarg instead.
 
     decode_timedelta : bool, optional

From 272cdb84450feae86abe6f9fba614912992646ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kmuehlbauer@wradlib.org>
Date: Sat, 4 Jan 2025 10:41:11 +0100
Subject: [PATCH 13/14] fix whats-new.rst

---
 doc/whats-new.rst | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 1c16c6287f7..69a7b6acf97 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -23,8 +23,8 @@ v2025.01.1 (unreleased)
 New Features
 ~~~~~~~~~~~~
 - Split out :py:class:`coders.CFDatetimeCoder` as public API in ``xr.coders``, make ``decode_times`` keyword argument
-  consume :py:class:`coders.CFDatetimeCoder`.
-
+  consume :py:class:`coders.CFDatetimeCoder` (:pull:`9901`).
+  By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
 
 Breaking changes
 ~~~~~~~~~~~~~~~~
@@ -32,7 +32,9 @@ Breaking changes
 
 Deprecations
 ~~~~~~~~~~~~
-
+- Time decoding related kwarg ``use_cftime`` is deprecated. Use keyword argument
+  ``decode_times=CFDatetimeCoder(use_cftime=True)`` in :py:func:`~xarray.open_dataset`, :py:func:`~xarray.open_dataarray`, :py:func:`~xarray.open_datatree`, :py:func:`~xarray.open_groups`, :py:func:`~xarray.open_zarr` and :py:func:`~xarray.decode_cf` instead (:pull:`9901`).
+  By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
 
 Bug fixes
 ~~~~~~~~~
@@ -87,9 +89,6 @@ Deprecations
 - Finalize deprecation of ``closed`` parameters of :py:func:`cftime_range` and
   :py:func:`date_range` (:pull:`9882`).
   By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
-- Time decoding related kwarg ``use_cftime`` is deprecated. Use keyword argument
-  ``decode_times=CFDatetimeCoder(use_cftime=True)`` in :py:func:`~xarray.open_dataset`, :py:func:`~xarray.open_dataarray`, :py:func:`~xarray.open_datatree`, :py:func:`~xarray.open_groups`, :py:func:`~xarray.open_zarr` and :py:func:`~xarray.decode_cf` instead.
-  By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
 
 Performance
 ~~~~~~~~~~~

From bda9b575a9eccf05317bc5af09450424e87aebd1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kai=20M=C3=BChlbauer?= <kmuehlbauer@wradlib.org>
Date: Sat, 4 Jan 2025 10:53:02 +0100
Subject: [PATCH 14/14] update warnings/errors

---
 xarray/conventions.py | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/xarray/conventions.py b/xarray/conventions.py
index 8f216146935..485c9ac0c71 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -200,8 +200,8 @@ def decode_cf_variable(
             if use_cftime is not None:
                 emit_user_level_warning(
                     "Usage of 'use_cftime' as a kwarg is deprecated. "
-                    "Please pass a CFDatetimeCoder instance initialized "
-                    "with use_cftime to the decode_times kwarg instead.\n"
+                    "Please pass a 'CFDatetimeCoder' instance initialized "
+                    "with 'use_cftime' to the 'decode_times' kwarg instead.\n"
                     "Example usage:\n"
                     "    time_coder = xr.coders.CFDatetimeCoder(use_cftime=True)\n"
                     "    ds = xr.open_dataset(decode_times=time_coder)\n",
@@ -212,9 +212,12 @@ def decode_cf_variable(
             if use_cftime is not None:
                 raise TypeError(
                     "Usage of 'use_cftime' as a kwarg is not allowed "
-                    "if a CFDatetimeCoder instance is passed to "
-                    "decode_times. Please set use_cftime "
-                    "when initializing CFDatetimeCoder instead."
+                    "if a 'CFDatetimeCoder' instance is passed to "
+                    "'decode_times'. Please set 'use_cftime' "
+                    "when initializing 'CFDatetimeCoder' instead.\n"
+                    "Example usage:\n"
+                    "    time_coder = xr.coders.CFDatetimeCoder(use_cftime=True)\n"
+                    "    ds = xr.open_dataset(decode_times=time_coder)\n",
                 )
         var = decode_times.decode(var, name=name)
 
