Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions properties/test_encode_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down
90 changes: 58 additions & 32 deletions xarray/testing/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,6 +22,8 @@

__all__ = [
"attrs",
"cftime_datetimes",
"datetimes",
"dimension_names",
"dimension_sizes",
"names",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
@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]

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)

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)
timespan_microseconds = (max_value - min_value) // unit_microsecond
microseconds_offset = draw(st.integers(0, timespan_microseconds))

return min_value + datetime.timedelta(microseconds=microseconds_offset)
Loading