From 370c6f5661a0fc2459ac9e36df4401510e62a7e7 Mon Sep 17 00:00:00 2001 From: Deepak Cherian Date: Wed, 12 Nov 2025 13:37:51 -0700 Subject: [PATCH 1/2] Rewrite test_iso8601_decode to work without cftime Closes #10907 Co-Authored-By: Claude --- properties/test_encode_decode.py | 7 +-- xarray/testing/strategies.py | 94 ++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/properties/test_encode_decode.py b/properties/test_encode_decode.py index 1d5b43a6da6..87bbfdba933 100644 --- a/properties/test_encode_decode.py +++ b/properties/test_encode_decode.py @@ -13,14 +13,12 @@ # isort: split import hypothesis.extra.numpy as npst -import hypothesis.strategies as st import numpy as np from hypothesis import given import xarray as xr from xarray.coding.times import _parse_iso8601 -from xarray.testing.strategies import CFTimeStrategyISO8601, variables -from xarray.tests import requires_cftime +from xarray.testing.strategies import datetimes, variables @pytest.mark.slow @@ -50,8 +48,7 @@ def test_CFScaleOffset_coder_roundtrip(original) -> None: xr.testing.assert_identical(original, roundtripped) -@requires_cftime -@given(dt=st.datetimes() | CFTimeStrategyISO8601()) +@given(dt=datetimes()) def test_iso8601_decode(dt): iso = dt.isoformat() with warnings.catch_warnings(): diff --git a/xarray/testing/strategies.py b/xarray/testing/strategies.py index 13973b9f550..2698a05f662 100644 --- a/xarray/testing/strategies.py +++ b/xarray/testing/strategies.py @@ -9,7 +9,7 @@ import xarray as xr from xarray.core.types import T_DuckArray -from xarray.core.utils import attempt_import +from xarray.core.utils import attempt_import, module_available if TYPE_CHECKING: from xarray.core.types import _DTypeLikeNested, _ShapeLike @@ -22,6 +22,8 @@ __all__ = [ "attrs", + "cftime_datetimes", + "datetimes", "dimension_names", "dimension_sizes", "names", @@ -84,6 +86,25 @@ def pandas_index_dtypes() -> st.SearchStrategy[np.dtype]: ) +def datetimes() -> st.SearchStrategy: + """ + Generates datetime objects including both standard library datetimes and cftime datetimes. + + Returns standard library datetime.datetime objects, and if cftime is available, + also includes cftime datetime objects from various calendars. + + Requires the hypothesis package to be installed. + + See Also + -------- + :ref:`testing.hypothesis`_ + """ + strategy = st.datetimes() + if module_available("cftime"): + strategy = strategy | cftime_datetimes() + return strategy + + # TODO Generalize to all valid unicode characters once formatting bugs in xarray's reprs are fixed + docs can handle it. _readable_characters = st.characters( categories=["L", "N"], max_codepoint=0x017F @@ -477,36 +498,41 @@ def unique_subset_of( ) -class CFTimeStrategy(st.SearchStrategy): - def __init__(self, min_value, max_value): - super().__init__() - self.min_value = min_value - self.max_value = max_value - - def do_draw(self, data): - unit_microsecond = datetime.timedelta(microseconds=1) - timespan_microseconds = (self.max_value - self.min_value) // unit_microsecond - result = data.draw_integer(0, timespan_microseconds) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message=".*date/calendar/year zero.*") - return self.min_value + datetime.timedelta(microseconds=result) - - -class CFTimeStrategyISO8601(st.SearchStrategy): - def __init__(self): - from xarray.tests.test_coding_times import _all_cftime_date_types - - super().__init__() - self.date_types = _all_cftime_date_types() - self.calendars = list(self.date_types) - - def do_draw(self, data): - calendar = data.draw(st.sampled_from(self.calendars)) - date_type = self.date_types[calendar] - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message=".*date/calendar/year zero.*") - daysinmonth = date_type(99999, 12, 1).daysinmonth - min_value = date_type(-99999, 1, 1) - max_value = date_type(99999, 12, daysinmonth, 23, 59, 59, 999999) - strategy = CFTimeStrategy(min_value, max_value) - return strategy.do_draw(data) +@st.composite +def cftime_datetimes(draw: st.DrawFn): + """ + Generates cftime datetime objects across various calendars. + + This strategy generates cftime datetime objects from all available + cftime calendars with dates ranging from year -99999 to 99999. + + Requires both the hypothesis and cftime packages to be installed. + + Returns + ------- + cftime_datetime_strategy + Strategy for generating cftime datetime objects. + + See Also + -------- + :ref:`testing.hypothesis`_ + """ + from xarray.tests import _all_cftime_date_types + + date_types = _all_cftime_date_types() + calendars = list(date_types) + + calendar = draw(st.sampled_from(calendars)) + date_type = date_types[calendar] + + daysinmonth = date_type(99999, 12, 1).daysinmonth + min_value = date_type(-99999, 1, 1) + max_value = date_type(99999, 12, daysinmonth, 23, 59, 59, 999999) + + unit_microsecond = datetime.timedelta(microseconds=1) + timespan_microseconds = (max_value - min_value) // unit_microsecond + microseconds_offset = draw(st.integers(0, timespan_microseconds)) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=".*date/calendar/year zero.*") + + return min_value + datetime.timedelta(microseconds=microseconds_offset) From 4104e2c87d6e5c18481904f158ea2021de76eb77 Mon Sep 17 00:00:00 2001 From: Deepak Cherian Date: Wed, 12 Nov 2025 14:46:58 -0700 Subject: [PATCH 2/2] silence warning --- xarray/testing/strategies.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/xarray/testing/strategies.py b/xarray/testing/strategies.py index 2698a05f662..19caf16cca9 100644 --- a/xarray/testing/strategies.py +++ b/xarray/testing/strategies.py @@ -525,14 +525,14 @@ def cftime_datetimes(draw: st.DrawFn): calendar = draw(st.sampled_from(calendars)) date_type = date_types[calendar] - daysinmonth = date_type(99999, 12, 1).daysinmonth - min_value = date_type(-99999, 1, 1) - max_value = date_type(99999, 12, daysinmonth, 23, 59, 59, 999999) - - unit_microsecond = datetime.timedelta(microseconds=1) - timespan_microseconds = (max_value - min_value) // unit_microsecond - microseconds_offset = draw(st.integers(0, timespan_microseconds)) with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=".*date/calendar/year zero.*") + daysinmonth = date_type(99999, 12, 1).daysinmonth + min_value = date_type(-99999, 1, 1) + max_value = date_type(99999, 12, daysinmonth, 23, 59, 59, 999999) + + unit_microsecond = datetime.timedelta(microseconds=1) + timespan_microseconds = (max_value - min_value) // unit_microsecond + microseconds_offset = draw(st.integers(0, timespan_microseconds)) return min_value + datetime.timedelta(microseconds=microseconds_offset)