Skip to content

Commit d475036

Browse files
spencerkclarkkmuehlbauerdcherianpre-commit-ci[bot]
authored
Enable passing a CFTimedeltaCoder to decode_timedelta (#9966)
* Allow passing a CFTimedeltaCoder instance to decode_timedelta * Updates based on @kmuehlbauer's branch https://github.com/kmuehlbauer/xarray/tree/split-out-coders * Increment what's new PR number * Add FutureWarning for change in decode_timedelta behavior * Include a note about opting out of timedelta decoding Co-authored-by: Kai Mühlbauer <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix typing * Fix typo * Fix doc build * Fix order of arguments in filterwarnings * Switch to :okwarning: * Fix missing :okwarning: --------- Co-authored-by: Kai Mühlbauer <[email protected]> Co-authored-by: Deepak Cherian <[email protected]> Co-authored-by: Kai Mühlbauer <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e28f171 commit d475036

File tree

9 files changed

+316
-53
lines changed

9 files changed

+316
-53
lines changed

doc/internals/time-coding.rst

+62
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,65 @@ on-disk resolution, if possible.
473473
474474
coder = xr.coders.CFDatetimeCoder(time_unit="s")
475475
xr.open_dataset("test-datetimes2.nc", decode_times=coder)
476+
477+
Similar logic applies for decoding timedelta values. The default resolution is
478+
``"ns"``:
479+
480+
.. ipython:: python
481+
482+
attrs = {"units": "hours"}
483+
ds = xr.Dataset({"time": ("time", [0, 1, 2, 3], attrs)})
484+
ds.to_netcdf("test-timedeltas1.nc")
485+
486+
.. ipython:: python
487+
:okwarning:
488+
489+
xr.open_dataset("test-timedeltas1.nc")
490+
491+
By default, timedeltas will be decoded to the same resolution as datetimes:
492+
493+
.. ipython:: python
494+
:okwarning:
495+
496+
coder = xr.coders.CFDatetimeCoder(time_unit="s")
497+
xr.open_dataset("test-timedeltas1.nc", decode_times=coder)
498+
499+
but if one would like to decode timedeltas to a different resolution, one can
500+
provide a coder specifically for timedeltas to ``decode_timedelta``:
501+
502+
.. ipython:: python
503+
504+
timedelta_coder = xr.coders.CFTimedeltaCoder(time_unit="ms")
505+
xr.open_dataset(
506+
"test-timedeltas1.nc", decode_times=coder, decode_timedelta=timedelta_coder
507+
)
508+
509+
As with datetimes, if a coarser unit is requested the timedeltas are decoded
510+
into their native on-disk resolution, if possible:
511+
512+
.. ipython:: python
513+
514+
attrs = {"units": "milliseconds"}
515+
ds = xr.Dataset({"time": ("time", [0, 1, 2, 3], attrs)})
516+
ds.to_netcdf("test-timedeltas2.nc")
517+
518+
.. ipython:: python
519+
:okwarning:
520+
521+
xr.open_dataset("test-timedeltas2.nc")
522+
523+
.. ipython:: python
524+
:okwarning:
525+
526+
coder = xr.coders.CFDatetimeCoder(time_unit="s")
527+
xr.open_dataset("test-timedeltas2.nc", decode_times=coder)
528+
529+
To opt-out of timedelta decoding (see issue `Undesired decoding to timedelta64 <https://github.com/pydata/xarray/issues/1621>`_) pass ``False`` to ``decode_timedelta``:
530+
531+
.. ipython:: python
532+
533+
xr.open_dataset("test-timedeltas2.nc", decode_timedelta=False)
534+
535+
.. note::
536+
Note that in the future the default value of ``decode_timedelta`` will be
537+
``False`` rather than ``None``.

doc/whats-new.rst

+31-12
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,36 @@ What's New
1919
v2025.01.2 (unreleased)
2020
-----------------------
2121

22-
This release brings non-nanosecond datetime resolution to xarray. In the
23-
last couple of releases xarray has been prepared for that change. The code had
24-
to be changed and adapted in numerous places, affecting especially the test suite.
25-
The documentation has been updated accordingly and a new internal chapter
26-
on :ref:`internals.timecoding` has been added.
27-
28-
To make the transition as smooth as possible this is designed to be fully backwards
29-
compatible, keeping the current default of ``'ns'`` resolution on decoding.
30-
To opt-in decoding into other resolutions (``'us'``, ``'ms'`` or ``'s'``) the
31-
new :py:class:`coders.CFDatetimeCoder` is used as parameter to ``decode_times``
32-
kwarg (see also :ref:`internals.default_timeunit`):
22+
This release brings non-nanosecond datetime and timedelta resolution to xarray.
23+
In the last couple of releases xarray has been prepared for that change. The
24+
code had to be changed and adapted in numerous places, affecting especially the
25+
test suite. The documentation has been updated accordingly and a new internal
26+
chapter on :ref:`internals.timecoding` has been added.
27+
28+
To make the transition as smooth as possible this is designed to be fully
29+
backwards compatible, keeping the current default of ``'ns'`` resolution on
30+
decoding. To opt-into decoding to other resolutions (``'us'``, ``'ms'`` or
31+
``'s'``) an instance of the newly public :py:class:`coders.CFDatetimeCoder`
32+
class can be passed through the ``decode_times`` keyword argument (see also
33+
:ref:`internals.default_timeunit`):
3334

3435
.. code-block:: python
3536
3637
coder = xr.coders.CFDatetimeCoder(time_unit="s")
3738
ds = xr.open_dataset(filename, decode_times=coder)
3839
40+
Similar control of the resoution of decoded timedeltas can be achieved through
41+
passing a :py:class:`coders.CFTimedeltaCoder` instance to the
42+
``decode_timedelta`` keyword argument:
43+
44+
.. code-block:: python
45+
46+
coder = xr.coders.CFTimedeltaCoder(time_unit="s")
47+
ds = xr.open_dataset(filename, decode_timedelta=coder)
48+
49+
though by default timedeltas will be decoded to the same ``time_unit`` as
50+
datetimes.
51+
3952
There might slight changes when encoding/decoding times as some warning and
4053
error messages have been removed or rewritten. Xarray will now also allow
4154
non-nanosecond datetimes (with ``'us'``, ``'ms'`` or ``'s'`` resolution) when
@@ -50,7 +63,7 @@ eventually be deprecated.
5063

5164
New Features
5265
~~~~~~~~~~~~
53-
- Relax nanosecond datetime restriction in CF time decoding (:issue:`7493`, :pull:`9618`, :pull:`9977`).
66+
- Relax nanosecond datetime / timedelta restriction in CF time decoding (:issue:`7493`, :pull:`9618`, :pull:`9966`, :pull:`9977`).
5467
By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_ and `Spencer Clark <https://github.com/spencerkclark>`_.
5568
- Enable the ``compute=False`` option in :py:meth:`DataTree.to_zarr`. (:pull:`9958`).
5669
By `Sam Levang <https://github.com/slevang>`_.
@@ -72,6 +85,12 @@ Breaking changes
7285

7386
Deprecations
7487
~~~~~~~~~~~~
88+
- In a future version of xarray decoding of variables into
89+
:py:class:`numpy.timedelta64` values will be disabled by default. To silence
90+
warnings associated with this, set ``decode_timedelta`` to ``True``,
91+
``False``, or a :py:class:`coders.CFTimedeltaCoder` instance when opening
92+
data (:issue:`1621`, :pull:`9966`). By `Spencer Clark
93+
<https://github.com/spencerkclark>`_.
7594

7695

7796
Bug fixes

xarray/backends/api.py

+31-12
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
_normalize_path,
3434
)
3535
from xarray.backends.locks import _get_scheduler
36-
from xarray.coders import CFDatetimeCoder
36+
from xarray.coders import CFDatetimeCoder, CFTimedeltaCoder
3737
from xarray.core import indexing
3838
from xarray.core.combine import (
3939
_infer_concat_order_from_positions,
@@ -487,7 +487,10 @@ def open_dataset(
487487
| CFDatetimeCoder
488488
| Mapping[str, bool | CFDatetimeCoder]
489489
| None = None,
490-
decode_timedelta: bool | Mapping[str, bool] | None = None,
490+
decode_timedelta: bool
491+
| CFTimedeltaCoder
492+
| Mapping[str, bool | CFTimedeltaCoder]
493+
| None = None,
491494
use_cftime: bool | Mapping[str, bool] | None = None,
492495
concat_characters: bool | Mapping[str, bool] | None = None,
493496
decode_coords: Literal["coordinates", "all"] | bool | None = None,
@@ -555,11 +558,14 @@ def open_dataset(
555558
Pass a mapping, e.g. ``{"my_variable": False}``,
556559
to toggle this feature per-variable individually.
557560
This keyword may not be supported by all the backends.
558-
decode_timedelta : bool or dict-like, optional
561+
decode_timedelta : bool, CFTimedeltaCoder, or dict-like, optional
559562
If True, decode variables and coordinates with time units in
560563
{"days", "hours", "minutes", "seconds", "milliseconds", "microseconds"}
561564
into timedelta objects. If False, leave them encoded as numbers.
562-
If None (default), assume the same value of decode_time.
565+
If None (default), assume the same value of ``decode_times``; if
566+
``decode_times`` is a :py:class:`coders.CFDatetimeCoder` instance, this
567+
takes the form of a :py:class:`coders.CFTimedeltaCoder` instance with a
568+
matching ``time_unit``.
563569
Pass a mapping, e.g. ``{"my_variable": False}``,
564570
to toggle this feature per-variable individually.
565571
This keyword may not be supported by all the backends.
@@ -712,7 +718,7 @@ def open_dataarray(
712718
| CFDatetimeCoder
713719
| Mapping[str, bool | CFDatetimeCoder]
714720
| None = None,
715-
decode_timedelta: bool | None = None,
721+
decode_timedelta: bool | CFTimedeltaCoder | None = None,
716722
use_cftime: bool | None = None,
717723
concat_characters: bool | None = None,
718724
decode_coords: Literal["coordinates", "all"] | bool | None = None,
@@ -785,7 +791,10 @@ def open_dataarray(
785791
If True, decode variables and coordinates with time units in
786792
{"days", "hours", "minutes", "seconds", "milliseconds", "microseconds"}
787793
into timedelta objects. If False, leave them encoded as numbers.
788-
If None (default), assume the same value of decode_time.
794+
If None (default), assume the same value of ``decode_times``; if
795+
``decode_times`` is a :py:class:`coders.CFDatetimeCoder` instance, this
796+
takes the form of a :py:class:`coders.CFTimedeltaCoder` instance with a
797+
matching ``time_unit``.
789798
This keyword may not be supported by all the backends.
790799
use_cftime: bool, optional
791800
Only relevant if encoded dates come from a standard calendar
@@ -927,7 +936,10 @@ def open_datatree(
927936
| CFDatetimeCoder
928937
| Mapping[str, bool | CFDatetimeCoder]
929938
| None = None,
930-
decode_timedelta: bool | Mapping[str, bool] | None = None,
939+
decode_timedelta: bool
940+
| CFTimedeltaCoder
941+
| Mapping[str, bool | CFTimedeltaCoder]
942+
| None = None,
931943
use_cftime: bool | Mapping[str, bool] | None = None,
932944
concat_characters: bool | Mapping[str, bool] | None = None,
933945
decode_coords: Literal["coordinates", "all"] | bool | None = None,
@@ -995,7 +1007,10 @@ def open_datatree(
9951007
If True, decode variables and coordinates with time units in
9961008
{"days", "hours", "minutes", "seconds", "milliseconds", "microseconds"}
9971009
into timedelta objects. If False, leave them encoded as numbers.
998-
If None (default), assume the same value of decode_time.
1010+
If None (default), assume the same value of ``decode_times``; if
1011+
``decode_times`` is a :py:class:`coders.CFDatetimeCoder` instance, this
1012+
takes the form of a :py:class:`coders.CFTimedeltaCoder` instance with a
1013+
matching ``time_unit``.
9991014
Pass a mapping, e.g. ``{"my_variable": False}``,
10001015
to toggle this feature per-variable individually.
10011016
This keyword may not be supported by all the backends.
@@ -1150,7 +1165,10 @@ def open_groups(
11501165
| CFDatetimeCoder
11511166
| Mapping[str, bool | CFDatetimeCoder]
11521167
| None = None,
1153-
decode_timedelta: bool | Mapping[str, bool] | None = None,
1168+
decode_timedelta: bool
1169+
| CFTimedeltaCoder
1170+
| Mapping[str, bool | CFTimedeltaCoder]
1171+
| None = None,
11541172
use_cftime: bool | Mapping[str, bool] | None = None,
11551173
concat_characters: bool | Mapping[str, bool] | None = None,
11561174
decode_coords: Literal["coordinates", "all"] | bool | None = None,
@@ -1222,9 +1240,10 @@ def open_groups(
12221240
If True, decode variables and coordinates with time units in
12231241
{"days", "hours", "minutes", "seconds", "milliseconds", "microseconds"}
12241242
into timedelta objects. If False, leave them encoded as numbers.
1225-
If None (default), assume the same value of decode_time.
1226-
Pass a mapping, e.g. ``{"my_variable": False}``,
1227-
to toggle this feature per-variable individually.
1243+
If None (default), assume the same value of ``decode_times``; if
1244+
``decode_times`` is a :py:class:`coders.CFDatetimeCoder` instance, this
1245+
takes the form of a :py:class:`coders.CFTimedeltaCoder` instance with a
1246+
matching ``time_unit``.
12281247
This keyword may not be supported by all the backends.
12291248
use_cftime: bool or dict-like, optional
12301249
Only relevant if encoded dates come from a standard calendar

xarray/coders.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
"encoding/decoding" process.
44
"""
55

6-
from xarray.coding.times import CFDatetimeCoder
6+
from xarray.coding.times import CFDatetimeCoder, CFTimedeltaCoder
77

8-
__all__ = [
9-
"CFDatetimeCoder",
10-
]
8+
__all__ = ["CFDatetimeCoder", "CFTimedeltaCoder"]

xarray/coding/times.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -1343,6 +1343,21 @@ def decode(self, variable: Variable, name: T_Name = None) -> Variable:
13431343

13441344

13451345
class CFTimedeltaCoder(VariableCoder):
1346+
"""Coder for CF Timedelta coding.
1347+
1348+
Parameters
1349+
----------
1350+
time_unit : PDDatetimeUnitOptions
1351+
Target resolution when decoding timedeltas. Defaults to "ns".
1352+
"""
1353+
1354+
def __init__(
1355+
self,
1356+
time_unit: PDDatetimeUnitOptions = "ns",
1357+
) -> None:
1358+
self.time_unit = time_unit
1359+
self._emit_decode_timedelta_future_warning = False
1360+
13461361
def encode(self, variable: Variable, name: T_Name = None) -> Variable:
13471362
if np.issubdtype(variable.data.dtype, np.timedelta64):
13481363
dims, data, attrs, encoding = unpack_for_encoding(variable)
@@ -1359,12 +1374,21 @@ def encode(self, variable: Variable, name: T_Name = None) -> Variable:
13591374
def decode(self, variable: Variable, name: T_Name = None) -> Variable:
13601375
units = variable.attrs.get("units", None)
13611376
if isinstance(units, str) and units in TIME_UNITS:
1377+
if self._emit_decode_timedelta_future_warning:
1378+
emit_user_level_warning(
1379+
"In a future version of xarray decode_timedelta will "
1380+
"default to False rather than None. To silence this "
1381+
"warning, set decode_timedelta to True, False, or a "
1382+
"'CFTimedeltaCoder' instance.",
1383+
FutureWarning,
1384+
)
13621385
dims, data, attrs, encoding = unpack_for_decoding(variable)
13631386

13641387
units = pop_to(attrs, encoding, "units")
1365-
transform = partial(decode_cf_timedelta, units=units)
1366-
# todo: check, if we can relax this one here, too
1367-
dtype = np.dtype("timedelta64[ns]")
1388+
dtype = np.dtype(f"timedelta64[{self.time_unit}]")
1389+
transform = partial(
1390+
decode_cf_timedelta, units=units, time_unit=self.time_unit
1391+
)
13681392
data = lazy_elemwise_func(data, transform, dtype=dtype)
13691393

13701394
return Variable(dims, data, attrs, encoding, fastpath=True)

0 commit comments

Comments
 (0)