From 8cc942e2a6529b9b4006ea70d683e12456fcab79 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Thu, 5 Feb 2026 10:54:24 -0800 Subject: [PATCH 1/6] added a on_error flag to CFDatetimeCoder -- and tests for it. --- xarray/coding/times.py | 25 ++++++++++++-- xarray/tests/test_coding_times.py | 57 +++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/xarray/coding/times.py b/xarray/coding/times.py index d45e8e4dc9f..ac32d044098 100644 --- a/xarray/coding/times.py +++ b/xarray/coding/times.py @@ -1370,15 +1370,24 @@ class CFDatetimeCoder(VariableCoder): May not be supported by all the backends. time_unit : PDDatetimeUnitOptions Target resolution when decoding dates. Defaults to "ns". + on_error : str, optional + What to do if there is an error when attempting to decode + a time variable. Options are: "raise", "warn", "ignore". + Defaults to "raise". """ def __init__( self, use_cftime: bool | None = None, time_unit: PDDatetimeUnitOptions = "ns", + on_error: str = "raise" ) -> None: self.use_cftime = use_cftime self.time_unit = time_unit + if on_error in {"raise", "warn", "ignore"}: + self.on_error = on_error + else: + raise ValueError('on_error must be one of "raise", "warn", "ignore")') def encode(self, variable: Variable, name: T_Name = None) -> Variable: if np.issubdtype(variable.dtype, np.datetime64) or contains_cftime_datetimes( @@ -1411,9 +1420,19 @@ def decode(self, variable: Variable, name: T_Name = None) -> Variable: units = pop_to(attrs, encoding, "units") calendar = pop_to(attrs, encoding, "calendar") - dtype = _decode_cf_datetime_dtype( - data, units, calendar, self.use_cftime, self.time_unit - ) + try: + dtype = _decode_cf_datetime_dtype( + data, units, calendar, self.use_cftime, self.time_unit + ) + except ValueError as err: + if self.on_error == "ignore": + return variable + elif self.on_error == "warn": + emit_user_level_warning(err.args[0]) + return variable + else: + raise + transform = partial( decode_cf_datetime, units=units, diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py index 1bb533dc7d8..d659465f9cd 100644 --- a/xarray/tests/test_coding_times.py +++ b/xarray/tests/test_coding_times.py @@ -2229,3 +2229,60 @@ def test_roundtrip_empty_datetime64_array(time_unit: PDDatetimeUnitOptions) -> N ) assert_identical(variable, roundtripped) assert roundtripped.dtype == variable.dtype + +@requires_cftime +def test_on_error_raises(): + """ + By default, decoding errors should raise + """ + array = np.array([0, 1, 2], dtype=np.dtype("int64")) + encoded = Variable(["time"], + array, + attrs={"units": "ms since 00:00:00"}) + + default is "raise" + coder = CFDatetimeCoder() + + with pytest.raises(ValueError): + coder.decode(encoded) + + # setting to "raise" should do the same thing. + coder = CFDatetimeCoder(on_error="raise") + + with pytest.raises(ValueError): + coder.decode(encoded) + +@requires_cftime +def test_on_error_ignore(): + """ + If on_error="ignore", no change. + """ + array = np.array([0, 1, 2], dtype=np.dtype("int64")) + encoded = Variable(["time"], + array, + attrs={"units": "ms since 00:00:00"}) + + coder = CFDatetimeCoder(on_error="ignore") + + decoded = coder.decode(encoded) + + # it shouldn't have changed the variable + assert decoded is encoded + +@requires_cftime +def test_on_error_warn(): + """ + If on_error="warn", no change, with a warning. + """ + array = np.array([0, 1, 2], dtype=np.dtype("int64")) + encoded = Variable(["time"], + array, + attrs={"units": "ms since 00:00:00"}) + + coder = CFDatetimeCoder(on_error="warn") + + with pytest.warns(UserWarning, match="unable to decode time units"): + decoded = coder.decode(encoded) + + # it shouldn't have changed the variable + assert decoded is encoded From 6598b3a7180cd4a2ecfa7f2f838159e206c23512 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Thu, 5 Feb 2026 16:55:51 -0800 Subject: [PATCH 2/6] fixed bug in test --- xarray/tests/test_coding_times.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py index d659465f9cd..6162e0d4e5e 100644 --- a/xarray/tests/test_coding_times.py +++ b/xarray/tests/test_coding_times.py @@ -2240,7 +2240,7 @@ def test_on_error_raises(): array, attrs={"units": "ms since 00:00:00"}) - default is "raise" + # default is "raise" coder = CFDatetimeCoder() with pytest.raises(ValueError): From afa62045f5c42e54403fa172af6d4f3c8f2aaf11 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Thu, 5 Feb 2026 10:54:24 -0800 Subject: [PATCH 3/6] added a on_error flag to CFDatetimeCoder -- and tests for it. --- xarray/coding/times.py | 25 ++++++++++++-- xarray/tests/test_coding_times.py | 57 +++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/xarray/coding/times.py b/xarray/coding/times.py index d45e8e4dc9f..ac32d044098 100644 --- a/xarray/coding/times.py +++ b/xarray/coding/times.py @@ -1370,15 +1370,24 @@ class CFDatetimeCoder(VariableCoder): May not be supported by all the backends. time_unit : PDDatetimeUnitOptions Target resolution when decoding dates. Defaults to "ns". + on_error : str, optional + What to do if there is an error when attempting to decode + a time variable. Options are: "raise", "warn", "ignore". + Defaults to "raise". """ def __init__( self, use_cftime: bool | None = None, time_unit: PDDatetimeUnitOptions = "ns", + on_error: str = "raise" ) -> None: self.use_cftime = use_cftime self.time_unit = time_unit + if on_error in {"raise", "warn", "ignore"}: + self.on_error = on_error + else: + raise ValueError('on_error must be one of "raise", "warn", "ignore")') def encode(self, variable: Variable, name: T_Name = None) -> Variable: if np.issubdtype(variable.dtype, np.datetime64) or contains_cftime_datetimes( @@ -1411,9 +1420,19 @@ def decode(self, variable: Variable, name: T_Name = None) -> Variable: units = pop_to(attrs, encoding, "units") calendar = pop_to(attrs, encoding, "calendar") - dtype = _decode_cf_datetime_dtype( - data, units, calendar, self.use_cftime, self.time_unit - ) + try: + dtype = _decode_cf_datetime_dtype( + data, units, calendar, self.use_cftime, self.time_unit + ) + except ValueError as err: + if self.on_error == "ignore": + return variable + elif self.on_error == "warn": + emit_user_level_warning(err.args[0]) + return variable + else: + raise + transform = partial( decode_cf_datetime, units=units, diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py index 1bb533dc7d8..d659465f9cd 100644 --- a/xarray/tests/test_coding_times.py +++ b/xarray/tests/test_coding_times.py @@ -2229,3 +2229,60 @@ def test_roundtrip_empty_datetime64_array(time_unit: PDDatetimeUnitOptions) -> N ) assert_identical(variable, roundtripped) assert roundtripped.dtype == variable.dtype + +@requires_cftime +def test_on_error_raises(): + """ + By default, decoding errors should raise + """ + array = np.array([0, 1, 2], dtype=np.dtype("int64")) + encoded = Variable(["time"], + array, + attrs={"units": "ms since 00:00:00"}) + + default is "raise" + coder = CFDatetimeCoder() + + with pytest.raises(ValueError): + coder.decode(encoded) + + # setting to "raise" should do the same thing. + coder = CFDatetimeCoder(on_error="raise") + + with pytest.raises(ValueError): + coder.decode(encoded) + +@requires_cftime +def test_on_error_ignore(): + """ + If on_error="ignore", no change. + """ + array = np.array([0, 1, 2], dtype=np.dtype("int64")) + encoded = Variable(["time"], + array, + attrs={"units": "ms since 00:00:00"}) + + coder = CFDatetimeCoder(on_error="ignore") + + decoded = coder.decode(encoded) + + # it shouldn't have changed the variable + assert decoded is encoded + +@requires_cftime +def test_on_error_warn(): + """ + If on_error="warn", no change, with a warning. + """ + array = np.array([0, 1, 2], dtype=np.dtype("int64")) + encoded = Variable(["time"], + array, + attrs={"units": "ms since 00:00:00"}) + + coder = CFDatetimeCoder(on_error="warn") + + with pytest.warns(UserWarning, match="unable to decode time units"): + decoded = coder.decode(encoded) + + # it shouldn't have changed the variable + assert decoded is encoded From 8af06c2531326bfd7ae77111928e7905d4e328e3 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Thu, 5 Feb 2026 16:55:51 -0800 Subject: [PATCH 4/6] fixed bug in test --- xarray/tests/test_coding_times.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py index d659465f9cd..6162e0d4e5e 100644 --- a/xarray/tests/test_coding_times.py +++ b/xarray/tests/test_coding_times.py @@ -2240,7 +2240,7 @@ def test_on_error_raises(): array, attrs={"units": "ms since 00:00:00"}) - default is "raise" + # default is "raise" coder = CFDatetimeCoder() with pytest.raises(ValueError): From 1082fba8f21787a7c400757236ab1e634665c119 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:48:24 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/coding/times.py | 6 +++--- xarray/tests/test_coding_times.py | 17 +++++++---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/xarray/coding/times.py b/xarray/coding/times.py index ac32d044098..af7cd461166 100644 --- a/xarray/coding/times.py +++ b/xarray/coding/times.py @@ -1373,21 +1373,21 @@ class CFDatetimeCoder(VariableCoder): on_error : str, optional What to do if there is an error when attempting to decode a time variable. Options are: "raise", "warn", "ignore". - Defaults to "raise". + Defaults to "raise". """ def __init__( self, use_cftime: bool | None = None, time_unit: PDDatetimeUnitOptions = "ns", - on_error: str = "raise" + on_error: str = "raise", ) -> None: self.use_cftime = use_cftime self.time_unit = time_unit if on_error in {"raise", "warn", "ignore"}: self.on_error = on_error else: - raise ValueError('on_error must be one of "raise", "warn", "ignore")') + raise ValueError('on_error must be one of "raise", "warn", "ignore")') def encode(self, variable: Variable, name: T_Name = None) -> Variable: if np.issubdtype(variable.dtype, np.datetime64) or contains_cftime_datetimes( diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py index 6162e0d4e5e..e8a631edaf1 100644 --- a/xarray/tests/test_coding_times.py +++ b/xarray/tests/test_coding_times.py @@ -2230,15 +2230,14 @@ def test_roundtrip_empty_datetime64_array(time_unit: PDDatetimeUnitOptions) -> N assert_identical(variable, roundtripped) assert roundtripped.dtype == variable.dtype + @requires_cftime def test_on_error_raises(): """ By default, decoding errors should raise """ array = np.array([0, 1, 2], dtype=np.dtype("int64")) - encoded = Variable(["time"], - array, - attrs={"units": "ms since 00:00:00"}) + encoded = Variable(["time"], array, attrs={"units": "ms since 00:00:00"}) # default is "raise" coder = CFDatetimeCoder() @@ -2252,15 +2251,14 @@ def test_on_error_raises(): with pytest.raises(ValueError): coder.decode(encoded) + @requires_cftime def test_on_error_ignore(): """ If on_error="ignore", no change. """ array = np.array([0, 1, 2], dtype=np.dtype("int64")) - encoded = Variable(["time"], - array, - attrs={"units": "ms since 00:00:00"}) + encoded = Variable(["time"], array, attrs={"units": "ms since 00:00:00"}) coder = CFDatetimeCoder(on_error="ignore") @@ -2269,20 +2267,19 @@ def test_on_error_ignore(): # it shouldn't have changed the variable assert decoded is encoded + @requires_cftime def test_on_error_warn(): """ If on_error="warn", no change, with a warning. """ array = np.array([0, 1, 2], dtype=np.dtype("int64")) - encoded = Variable(["time"], - array, - attrs={"units": "ms since 00:00:00"}) + encoded = Variable(["time"], array, attrs={"units": "ms since 00:00:00"}) coder = CFDatetimeCoder(on_error="warn") with pytest.warns(UserWarning, match="unable to decode time units"): decoded = coder.decode(encoded) - # it shouldn't have changed the variable + # it shouldn't have changed the variable assert decoded is encoded From 318b93efe3cfa5538075a7b5c07a5dcf99136205 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:20:25 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/conventions.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/xarray/conventions.py b/xarray/conventions.py index 1b68fab9f94..31000eab934 100644 --- a/xarray/conventions.py +++ b/xarray/conventions.py @@ -225,18 +225,20 @@ def decode_cf_variable( " ds = xr.open_dataset(decode_times=time_coder)\n", FutureWarning, ) -# decode_times = CFDatetimeCoder(use_cftime=use_cftime) - decode_times_options = {True: 'raise', - 'error': 'raise', - 'ignore': 'ignore', - 'warn': 'warn', - } + # decode_times = CFDatetimeCoder(use_cftime=use_cftime) + decode_times_options = { + True: "raise", + "error": "raise", + "ignore": "ignore", + "warn": "warn", + } try: on_error = decode_times_options[decode_times] except KeyError: - raise ValueError("`decode_times` must be one of:" - "True, False, 'raise', 'warn', 'ignore'" - ) from None + raise ValueError( + "`decode_times` must be one of:" + "True, False, 'raise', 'warn', 'ignore'" + ) from None decode_times = CFDatetimeCoder(use_cftime=use_cftime, on_error=on_error) elif use_cftime is not None: