Skip to content

Commit 58de6d2

Browse files
Finalize change in behavior of decode_timedelta=None (#11173)
1 parent a8732fc commit 58de6d2

5 files changed

Lines changed: 38 additions & 75 deletions

File tree

doc/whats-new.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@ New Features
1717

1818
Breaking Changes
1919
~~~~~~~~~~~~~~~~
20-
20+
- Xarray will no longer by default decode a variable into a
21+
:py:class:`np.timedelta64` dtype based on the presence of a timedelta-like
22+
``"units"`` attribute alone. Instead it will rely on the presence of a
23+
:py:class:`np.timedelta64` dtype attribute, which is now xarray's default way
24+
of encoding :py:class:`np.timedelta64` values. The old decoding behavior can
25+
be restored by specifying ``decode_timedelta=True`` or
26+
``decode_timedelta=CFTimedeltaCoder(decode_via_units=True)`` in
27+
:py:meth:`open_dataset`. This finalizes the deprecation cycle initiated in
28+
xarray version 2025.01.2 (:pull:`11173`). By `Spencer Clark
29+
<https://github.com/spencerkclark>`_.
2130

2231
Deprecations
2332
~~~~~~~~~~~~

xarray/coding/times.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,13 +1492,12 @@ class CFTimedeltaCoder(VariableCoder):
14921492
def __init__(
14931493
self,
14941494
time_unit: PDDatetimeUnitOptions | None = None,
1495-
decode_via_units: bool = True,
1495+
decode_via_units: bool = False,
14961496
decode_via_dtype: bool = True,
14971497
) -> None:
14981498
self.time_unit = time_unit
14991499
self.decode_via_units = decode_via_units
15001500
self.decode_via_dtype = decode_via_dtype
1501-
self._emit_decode_timedelta_future_warning = False
15021501

15031502
def encode(self, variable: Variable, name: T_Name = None) -> Variable:
15041503
if np.issubdtype(variable.dtype, np.timedelta64):
@@ -1540,23 +1539,6 @@ def decode(self, variable: Variable, name: T_Name = None) -> Variable:
15401539
else:
15411540
time_unit = self.time_unit
15421541
else:
1543-
if self._emit_decode_timedelta_future_warning:
1544-
var_string = f"the variable {name!r}" if name else ""
1545-
emit_user_level_warning(
1546-
"In a future version, xarray will not decode "
1547-
f"{var_string} into a timedelta64 dtype based on the "
1548-
"presence of a timedelta-like 'units' attribute by "
1549-
"default. Instead it will rely on the presence of a "
1550-
"timedelta64 'dtype' attribute, which is now xarray's "
1551-
"default way of encoding timedelta64 values.\n"
1552-
"To continue decoding into a timedelta64 dtype, either "
1553-
"set `decode_timedelta=True` when opening this "
1554-
"dataset, or add the attribute "
1555-
"`dtype='timedelta64[ns]'` to this variable on disk.\n"
1556-
"To opt-in to future behavior, set "
1557-
"`decode_timedelta=False`.",
1558-
FutureWarning,
1559-
)
15601542
if self.time_unit is None:
15611543
time_unit = "ns"
15621544
else:

xarray/conventions.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,13 @@ def decode_cf_variable(
173173

174174
original_dtype = var.dtype
175175

176-
decode_timedelta_was_none = decode_timedelta is None
177176
if decode_timedelta is None:
178177
if isinstance(decode_times, CFDatetimeCoder):
179178
decode_timedelta = CFTimedeltaCoder(time_unit=decode_times.time_unit)
179+
elif decode_times:
180+
decode_timedelta = CFTimedeltaCoder()
180181
else:
181-
decode_timedelta = bool(decode_times)
182+
decode_timedelta = False
182183

183184
if concat_characters:
184185
if stack_char_dim:
@@ -208,9 +209,6 @@ def decode_cf_variable(
208209
decode_timedelta = CFTimedeltaCoder(
209210
decode_via_units=decode_timedelta, decode_via_dtype=decode_timedelta
210211
)
211-
decode_timedelta._emit_decode_timedelta_future_warning = (
212-
decode_timedelta_was_none
213-
)
214212
var = decode_timedelta.decode(var, name=name)
215213
if decode_times:
216214
# remove checks after end of deprecation cycle

xarray/tests/test_coding_times.py

Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,67 +1825,49 @@ def test_encode_cf_timedelta_small_dtype_missing_value(use_dask) -> None:
18251825

18261826

18271827
_DECODE_TIMEDELTA_VIA_UNITS_TESTS = {
1828-
"default": (True, None, np.dtype("timedelta64[ns]"), True),
1829-
"decode_timedelta=True": (True, True, np.dtype("timedelta64[ns]"), False),
1830-
"decode_timedelta=False": (True, False, np.dtype("int64"), False),
1831-
"inherit-time_unit-from-decode_times": (
1832-
CFDatetimeCoder(time_unit="s"),
1833-
None,
1834-
np.dtype("timedelta64[s]"),
1835-
True,
1836-
),
1828+
"default": (True, None, np.dtype("int64")),
1829+
"decode_timedelta=True": (True, True, np.dtype("timedelta64[ns]")),
1830+
"decode_timedelta=False": (True, False, np.dtype("int64")),
18371831
"set-time_unit-via-CFTimedeltaCoder-decode_times=True": (
18381832
True,
1839-
CFTimedeltaCoder(time_unit="s"),
1833+
CFTimedeltaCoder(decode_via_units=True, time_unit="s"),
18401834
np.dtype("timedelta64[s]"),
1841-
False,
18421835
),
18431836
"set-time_unit-via-CFTimedeltaCoder-decode_times=False": (
18441837
False,
1845-
CFTimedeltaCoder(time_unit="s"),
1838+
CFTimedeltaCoder(decode_via_units=True, time_unit="s"),
18461839
np.dtype("timedelta64[s]"),
1847-
False,
18481840
),
18491841
"override-time_unit-from-decode_times": (
18501842
CFDatetimeCoder(time_unit="ns"),
1851-
CFTimedeltaCoder(time_unit="s"),
1843+
CFTimedeltaCoder(decode_via_units=True, time_unit="s"),
18521844
np.dtype("timedelta64[s]"),
1853-
False,
18541845
),
18551846
}
18561847

18571848

18581849
@pytest.mark.parametrize(
1859-
("decode_times", "decode_timedelta", "expected_dtype", "warns"),
1850+
("decode_times", "decode_timedelta", "expected_dtype"),
18601851
list(_DECODE_TIMEDELTA_VIA_UNITS_TESTS.values()),
18611852
ids=list(_DECODE_TIMEDELTA_VIA_UNITS_TESTS.keys()),
18621853
)
18631854
def test_decode_timedelta_via_units(
1864-
decode_times, decode_timedelta, expected_dtype, warns
1855+
decode_times, decode_timedelta, expected_dtype
18651856
) -> None:
18661857
timedeltas = pd.timedelta_range(0, freq="D", periods=3)
18671858
attrs = {"units": "days"}
18681859
var = Variable(["time"], timedeltas, encoding=attrs)
18691860
encoded = Variable(["time"], np.array([0, 1, 2]), attrs=attrs)
1870-
if warns:
1871-
with pytest.warns(
1872-
FutureWarning,
1873-
match="xarray will not decode the variable 'foo' into a timedelta64 dtype",
1874-
):
1875-
decoded = conventions.decode_cf_variable(
1876-
"foo",
1877-
encoded,
1878-
decode_times=decode_times,
1879-
decode_timedelta=decode_timedelta,
1880-
)
1861+
decoded = conventions.decode_cf_variable(
1862+
"foo", encoded, decode_times=decode_times, decode_timedelta=decode_timedelta
1863+
)
1864+
if decode_timedelta is True or (
1865+
isinstance(decode_timedelta, CFTimedeltaCoder)
1866+
and decode_timedelta.decode_via_units
1867+
):
1868+
assert_equal(var, decoded)
18811869
else:
1882-
decoded = conventions.decode_cf_variable(
1883-
"foo", encoded, decode_times=decode_times, decode_timedelta=decode_timedelta
1884-
)
1885-
if decode_timedelta is False:
18861870
assert_equal(encoded, decoded)
1887-
else:
1888-
assert_equal(var, decoded)
18891871
assert decoded.dtype == expected_dtype
18901872

18911873

@@ -1954,7 +1936,7 @@ def test_decode_timedelta_via_dtype(
19541936
@pytest.mark.parametrize("dtype", [np.uint64, np.int64, np.float64])
19551937
def test_decode_timedelta_dtypes(dtype) -> None:
19561938
encoded = Variable(["time"], np.arange(10), {"units": "seconds"})
1957-
coder = CFTimedeltaCoder(time_unit="s")
1939+
coder = CFTimedeltaCoder(decode_via_units=True, time_unit="s")
19581940
decoded = coder.decode(encoded)
19591941
assert decoded.dtype.kind == "m"
19601942
assert_equal(coder.encode(decoded), encoded)
@@ -1963,8 +1945,9 @@ def test_decode_timedelta_dtypes(dtype) -> None:
19631945
def test_lazy_decode_timedelta_unexpected_dtype() -> None:
19641946
attrs = {"units": "seconds"}
19651947
encoded = Variable(["time"], [0, 0.5, 1], attrs=attrs)
1948+
decode_timedelta = CFTimedeltaCoder(decode_via_units=True, time_unit="s")
19661949
decoded = conventions.decode_cf_variable(
1967-
"foo", encoded, decode_timedelta=CFTimedeltaCoder(time_unit="s")
1950+
"foo", encoded, decode_timedelta=decode_timedelta
19681951
)
19691952

19701953
expected_dtype_upon_lazy_decoding = np.dtype("timedelta64[s]")
@@ -1978,8 +1961,9 @@ def test_lazy_decode_timedelta_unexpected_dtype() -> None:
19781961
def test_lazy_decode_timedelta_error() -> None:
19791962
attrs = {"units": "seconds"}
19801963
encoded = Variable(["time"], [0, np.iinfo(np.int64).max, 1], attrs=attrs)
1964+
decode_timedelta = CFTimedeltaCoder(decode_via_units=True, time_unit="ms")
19811965
decoded = conventions.decode_cf_variable(
1982-
"foo", encoded, decode_timedelta=CFTimedeltaCoder(time_unit="ms")
1966+
"foo", encoded, decode_timedelta=decode_timedelta
19831967
)
19841968
with pytest.raises(OutOfBoundsTimedelta, match="overflow"):
19851969
decoded.load()

xarray/tests/test_conventions.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,9 @@ def test_decode_cf_time_kwargs(self, time_unit) -> None:
553553
dsc = conventions.decode_cf(
554554
ds,
555555
decode_times=CFDatetimeCoder(time_unit=time_unit),
556-
decode_timedelta=CFTimedeltaCoder(time_unit=time_unit),
556+
decode_timedelta=CFTimedeltaCoder(
557+
decode_via_units=True, time_unit=time_unit
558+
),
557559
)
558560
assert dsc.timedelta.dtype == np.dtype(f"m8[{time_unit}]")
559561
assert dsc.time.dtype == np.dtype(f"M8[{time_unit}]")
@@ -679,15 +681,3 @@ def test_encode_cf_variable_with_vlen_dtype() -> None:
679681
encoded_v = conventions.encode_cf_variable(v)
680682
assert encoded_v.data.dtype.kind == "O"
681683
assert coding.strings.check_vlen_dtype(encoded_v.data.dtype) is str
682-
683-
684-
def test_decode_cf_variables_decode_timedelta_warning() -> None:
685-
v = Variable(["time"], [1, 2], attrs={"units": "seconds"})
686-
variables = {"a": v}
687-
688-
with warnings.catch_warnings():
689-
warnings.filterwarnings("error", "decode_timedelta", FutureWarning)
690-
conventions.decode_cf_variables(variables, {}, decode_timedelta=True)
691-
692-
with pytest.warns(FutureWarning, match="decode_timedelta"):
693-
conventions.decode_cf_variables(variables, {})

0 commit comments

Comments
 (0)