From fc954de3dddf6028bec7e78f1d26ce4a10d516d6 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Tue, 4 Feb 2025 15:13:13 -0700 Subject: [PATCH 01/38] Deprecate cftime_range() and some refactoring --- xarray/coding/cftime_offsets.py | 129 +++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 43 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index f3ed6444904..55c3c560d91 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -855,15 +855,20 @@ def normalize_date(date): return date.replace(hour=0, minute=0, second=0, microsecond=0) -def _maybe_normalize_date(date, normalize): - """Round datetime down to midnight if normalize is True.""" - if normalize: - return normalize_date(date) - else: +def _get_normalized_cfdate(date, calendar, normalize): + """convert to cf datetime and round down to midnight if normalize.""" + if date is None: return date + cf_date = to_cftime_datetime(date, calendar) + + return normalize_date(cf_date) if normalize else cf_date + +def _generate_date_range(): + pass -def _generate_linear_range(start, end, periods): + +def _generate_date_range(start, end, periods): """Generate an equally-spaced sequence of cftime.datetime objects between and including two dates (whose length equals the number of periods).""" if TYPE_CHECKING: @@ -880,9 +885,9 @@ def _generate_linear_range(start, end, periods): ) -def _generate_range(start, end, periods, offset): +def _generate_date_range_with_freq(start, end, periods, freq): """Generate a regular range of cftime.datetime objects with a - given time offset. + given frequency. Adapted from pandas.tseries.offsets.generate_range (now at pandas.core.arrays.datetimes._generate_range). @@ -895,13 +900,15 @@ def _generate_range(start, end, periods, offset): End of range periods : int, or None Number of elements in the sequence - offset : BaseCFTimeOffset - An offset class designed for working with cftime.datetime objects + freq: str + Step size bewtween cftime.datetime objects. Not None. Returns ------- - A generator object + A generator object of cftime.datetime objects """ + offset = to_offset(freq) + if start: # From pandas GH 56147 / 56832 to account for negative direction and # range bounds @@ -949,7 +956,9 @@ def cftime_range( inclusive: InclusiveOptions = "both", calendar="standard", ) -> CFTimeIndex: - """Return a fixed frequency CFTimeIndex. + """DEPRECATED: use date_range(use_cftime=True) instead. + + Return a fixed frequency CFTimeIndex. Parameters ---------- @@ -1118,52 +1127,86 @@ def cftime_range( -------- pandas.date_range """ + return date_range( + start=start, + end=end, + periods=periods, + freq=freq, + normalize=normalize, + name=name, + inclusive=inclusive, + calendar=calendar, + use_cftime=True) + + +def _cftime_range( + start=None, + end=None, + periods=None, + freq=None, + normalize=False, + name=None, + inclusive: InclusiveOptions = "both", + calendar="standard", +) -> CFTimeIndex: + """Return a fixed frequency CFTimeIndex. + + Parameters + ---------- + start : str or cftime.datetime, optional + Left bound for generating dates. + end : str or cftime.datetime, optional + Right bound for generating dates. + periods : int, optional + Number of periods to generate. + freq : str or None, default: "D" + Frequency strings can have multiples, e.g. "5h" and negative values, e.g. "-1D". + normalize : bool, default: False + Normalize start/end dates to midnight before generating date range. + name : str, default: None + Name of the resulting index + inclusive : {"both", "neither", "left", "right"}, default "both" + Include boundaries; whether to set each bound as closed or open. + calendar : str, default: "standard" + Calendar type for the datetimes. + Returns + ------- + CFTimeIndex + + Notes + ----- + see cftime_range + """ if freq is None and any(arg is None for arg in [periods, start, end]): freq = "D" # Adapted from pandas.core.indexes.datetimes._generate_range. if count_not_none(start, end, periods, freq) != 3: raise ValueError( - "Of the arguments 'start', 'end', 'periods', and 'freq', three " - "must be specified at a time." + "Illegal combination of the arguments 'start', 'end', 'periods', " + "and 'freq' specified." ) - if start is not None: - start = to_cftime_datetime(start, calendar) - start = _maybe_normalize_date(start, normalize) - if end is not None: - end = to_cftime_datetime(end, calendar) - end = _maybe_normalize_date(end, normalize) + start = _get_normalized_cfdate(start, calendar, normalize) + end = _get_normalized_cfdate(start, calendar, normalize) if freq is None: - dates = _generate_linear_range(start, end, periods) - else: - offset = to_offset(freq) - dates = np.array(list(_generate_range(start, end, periods, offset))) - - if inclusive == "neither": - left_closed = False - right_closed = False - elif inclusive == "left": - left_closed = True - right_closed = False - elif inclusive == "right": - left_closed = False - right_closed = True - elif inclusive == "both": - left_closed = True - right_closed = True + dates = _generate_date_range(start, end, periods) else: + dates = np.array(list(_generate_date_range_with_freq(start, end, periods, freq))) + + if inclusive not in InclusiveOptions: raise ValueError( f"Argument `inclusive` must be either 'both', 'neither', " - f"'left', 'right', or None. Got {inclusive}." + f"'left', or 'right'. Got {inclusive}." ) - if not left_closed and len(dates) and start is not None and dates[0] == start: - dates = dates[1:] - if not right_closed and len(dates) and end is not None and dates[-1] == end: - dates = dates[:-1] + if len(dates) and inclusive != 'both': + if inclusive != 'left' and dates[0] == start: + dates = dates[1:] + if inclusive != 'right' and dates[-1] == end: + dates = dates[:-1] return CFTimeIndex(dates, name=name) @@ -1259,7 +1302,7 @@ def date_range( f"Invalid calendar {calendar} for pandas DatetimeIndex, try using `use_cftime=True`." ) - return cftime_range( + return _cftime_range( start=start, end=end, periods=periods, From 7affcc67a43129d5d5af7bfc3e65cca0cca6538d Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Tue, 4 Feb 2025 15:23:12 -0700 Subject: [PATCH 02/38] updated whats-new.rst --- doc/whats-new.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 56d9a3d9bed..281c174babc 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -29,10 +29,11 @@ Breaking changes Deprecations ~~~~~~~~~~~~ - +xarray.cftime_range deprecated in favor of xarray.data_range(use_cftime=true) Bug fixes ~~~~~~~~~ +#9886 Deprecate xr.cftime_range in favor of xr.date_range Documentation From 87f0cde0b4abbd63a55b9a70f982f75a479d1819 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Tue, 4 Feb 2025 15:37:27 -0700 Subject: [PATCH 03/38] Add deprecation warning to cftime_range() Issue #9886 --- xarray/coding/cftime_offsets.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 55c3c560d91..efe9c5ffe3c 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -861,9 +861,10 @@ def _get_normalized_cfdate(date, calendar, normalize): return date cf_date = to_cftime_datetime(date, calendar) - + return normalize_date(cf_date) if normalize else cf_date + def _generate_date_range(): pass @@ -908,7 +909,7 @@ def _generate_date_range_with_freq(start, end, periods, freq): A generator object of cftime.datetime objects """ offset = to_offset(freq) - + if start: # From pandas GH 56147 / 56832 to account for negative direction and # range bounds @@ -1127,6 +1128,12 @@ def cftime_range( -------- pandas.date_range """ + warnings.warn( + "cftime_range() is deprecated, please use date_range(use_cftime=True) instead.", + DeprecationWarning, + stacklevel=2, + ) + return date_range( start=start, end=end, @@ -1136,7 +1143,8 @@ def cftime_range( name=name, inclusive=inclusive, calendar=calendar, - use_cftime=True) + use_cftime=True, + ) def _cftime_range( @@ -1194,7 +1202,9 @@ def _cftime_range( if freq is None: dates = _generate_date_range(start, end, periods) else: - dates = np.array(list(_generate_date_range_with_freq(start, end, periods, freq))) + dates = np.array( + list(_generate_date_range_with_freq(start, end, periods, freq)) + ) if inclusive not in InclusiveOptions: raise ValueError( @@ -1202,10 +1212,10 @@ def _cftime_range( f"'left', or 'right'. Got {inclusive}." ) - if len(dates) and inclusive != 'both': - if inclusive != 'left' and dates[0] == start: + if len(dates) and inclusive != "both": + if inclusive != "left" and dates[0] == start: dates = dates[1:] - if inclusive != 'right' and dates[-1] == end: + if inclusive != "right" and dates[-1] == end: dates = dates[:-1] return CFTimeIndex(dates, name=name) From 392c092ffb7bee7dc36ca99e5fde5566aa48b5b0 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 11:38:48 -0700 Subject: [PATCH 04/38] - Update cftime_range example to expect deprecation warning. - update other examples that call cftime_range to use date_range(use_cftime=True) instead Issue #9886 --- xarray/coding/cftime_offsets.py | 3 +++ xarray/coding/cftimeindex.py | 18 ++++++++++++------ xarray/core/dataarray.py | 2 +- xarray/core/dataset.py | 2 +- xarray/core/parallel.py | 2 +- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index efe9c5ffe3c..b2ca19532d6 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1113,6 +1113,9 @@ def cftime_range( objects associated with the specified calendar type, e.g. >>> xr.cftime_range(start="2000", periods=6, freq="2MS", calendar="noleap") + Traceback (most recent call last): + ... + DeprecationWarning: cftime_range() is deprecated, please use date_range(use_cftime=True) instead. CFTimeIndex([2000-01-01 00:00:00, 2000-03-01 00:00:00, 2000-05-01 00:00:00, 2000-07-01 00:00:00, 2000-09-01 00:00:00, 2000-11-01 00:00:00], dtype='object', length=6, calendar='noleap', freq='2MS') diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 7ef7ad894a6..053e4e76a3d 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -236,7 +236,7 @@ class CFTimeIndex(pd.Index): See Also -------- - cftime_range + date_range """ _data: np.ndarray @@ -463,7 +463,7 @@ def shift( # type: ignore[override] # freq is typed Any, we are more precise ) -> Self: """Shift the CFTimeIndex a multiple of the given frequency. - See the documentation for :py:func:`~xarray.cftime_range` for a + See the documentation for :py:func:`~xarray.date_range` for a complete listing of valid frequency strings. Parameters @@ -483,7 +483,7 @@ def shift( # type: ignore[override] # freq is typed Any, we are more precise Examples -------- - >>> index = xr.cftime_range("2000", periods=1, freq="ME") + >>> index = xr.date_range("2000", periods=1, freq="ME", use_cftime=True) >>> index CFTimeIndex([2000-01-31 00:00:00], dtype='object', length=1, calendar='standard', freq=None) @@ -586,7 +586,9 @@ def to_datetimeindex( Examples -------- - >>> times = xr.cftime_range("2000", periods=2, calendar="gregorian") + >>> times = xr.date_range( + ... "2000", periods=2, calendar="gregorian", use_cftime=True + ... ) >>> times CFTimeIndex([2000-01-01 00:00:00, 2000-01-02 00:00:00], dtype='object', length=2, calendar='standard', freq=None) @@ -652,8 +654,12 @@ def strftime(self, date_format): Examples -------- - >>> rng = xr.cftime_range( - ... start="2000", periods=5, freq="2MS", calendar="noleap" + >>> rng = xr.date_range( + ... start="2000", + ... periods=5, + ... freq="2MS", + ... calendar="noleap", + ... use_cftime=True, ... ) >>> rng.strftime("%B %d, %Y, %r") Index(['January 01, 2000, 12:00:00 AM', 'March 01, 2000, 12:00:00 AM', diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py index 6c4c17e76cd..b1cb4b05775 100644 --- a/xarray/core/dataarray.py +++ b/xarray/core/dataarray.py @@ -5618,7 +5618,7 @@ def map_blocks( ... clim = gb.mean(dim="time") ... return gb - clim ... - >>> time = xr.cftime_range("1990-01", "1992-01", freq="ME") + >>> time = xr.date_range("1990-01", "1992-01", freq="ME", use_cftime=True) >>> month = xr.DataArray(time.month, coords={"time": time}, dims=["time"]) >>> np.random.seed(123) >>> array = xr.DataArray( diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 79fea33e4c1..d9967b93e20 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -9024,7 +9024,7 @@ def map_blocks( ... clim = gb.mean(dim="time") ... return gb - clim ... - >>> time = xr.cftime_range("1990-01", "1992-01", freq="ME") + >>> time = xr.date_range("1990-01", "1992-01", freq="ME", use_cftime=True) >>> month = xr.DataArray(time.month, coords={"time": time}, dims=["time"]) >>> np.random.seed(123) >>> array = xr.DataArray( diff --git a/xarray/core/parallel.py b/xarray/core/parallel.py index 6d6a6672470..1b2afbe516b 100644 --- a/xarray/core/parallel.py +++ b/xarray/core/parallel.py @@ -296,7 +296,7 @@ def map_blocks( ... clim = gb.mean(dim="time") ... return gb - clim ... - >>> time = xr.cftime_range("1990-01", "1992-01", freq="ME") + >>> time = xr.date_range("1990-01", "1992-01", freq="ME", use_cftime=True) >>> month = xr.DataArray(time.month, coords={"time": time}, dims=["time"]) >>> np.random.seed(123) >>> array = xr.DataArray( From dec7df5af9298d1b19b4018fa25bb2fac11c486c Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Fri, 7 Feb 2025 11:54:52 -0700 Subject: [PATCH 05/38] Update whats-new.rst Fix typo in pull request number --- doc/whats-new.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 968df75ce5b..0765d5109f4 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -42,7 +42,9 @@ Bug fixes - Use mean of min/max years as offset in calculation of datetime64 mean (:issue:`10019`, :pull:`10035`). By `Kai Mühlbauer `_. -- Deprecate xr.cftime_range() in favor of xr.date_range(use_cftime=True) (:issue: `9886`, :pull`10024`) +- Deprecate xr.cftime_range() in favor of xr.date_range(use_cftime=True) + (:issue:`9886`, :pull:`10024`). + By `Josh Kihm `_. Documentation From 003edc8099430cb6b6ebacaa1e03cf9c97d89922 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 18:57:41 +0000 Subject: [PATCH 06/38] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/whats-new.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 0765d5109f4..2d84cba01d6 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -42,8 +42,8 @@ Bug fixes - Use mean of min/max years as offset in calculation of datetime64 mean (:issue:`10019`, :pull:`10035`). By `Kai Mühlbauer `_. -- Deprecate xr.cftime_range() in favor of xr.date_range(use_cftime=True) - (:issue:`9886`, :pull:`10024`). +- Deprecate xr.cftime_range() in favor of xr.date_range(use_cftime=True) + (:issue:`9886`, :pull:`10024`). By `Josh Kihm `_. From a072f1b48429362ddcc8c5cc11afb02f1b850365 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 12:31:58 -0700 Subject: [PATCH 07/38] always import core.types.InclusionOptions --- xarray/coding/cftime_offsets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index b2ca19532d6..5ecf98c4c01 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -66,11 +66,11 @@ count_not_none, default_precision_timestamp, ) +from xarray.core.types import InclusiveOptions from xarray.core.utils import attempt_import, emit_user_level_warning if TYPE_CHECKING: from xarray.core.types import ( - InclusiveOptions, PDDatetimeUnitOptions, Self, TypeAlias, @@ -1112,7 +1112,9 @@ def cftime_range( This function returns a ``CFTimeIndex``, populated with ``cftime.datetime`` objects associated with the specified calendar type, e.g. - >>> xr.cftime_range(start="2000", periods=6, freq="2MS", calendar="noleap") + >>> xr.cftime_range( + ... start="2000", periods=6, freq="2MS", calendar="noleap" + ... ) # +doctest: ELLIPSIS Traceback (most recent call last): ... DeprecationWarning: cftime_range() is deprecated, please use date_range(use_cftime=True) instead. From 9bf05e240b05f543f2bc0ef5c46996beda0d1df5 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 13:07:49 -0700 Subject: [PATCH 08/38] fix check of InclusiveOptions Literal check --- xarray/coding/cftime_offsets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 5ecf98c4c01..3bbe847acda 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -47,7 +47,7 @@ from collections.abc import Mapping from datetime import datetime, timedelta from functools import partial -from typing import TYPE_CHECKING, ClassVar, Literal, TypeVar +from typing import TYPE_CHECKING, ClassVar, Literal, TypeVar, get_args import numpy as np import pandas as pd @@ -1211,7 +1211,7 @@ def _cftime_range( list(_generate_date_range_with_freq(start, end, periods, freq)) ) - if inclusive not in InclusiveOptions: + if inclusive not in get_args(InclusiveOptions): raise ValueError( f"Argument `inclusive` must be either 'both', 'neither', " f"'left', or 'right'. Got {inclusive}." From 49a249f56f4a87e6ac0bd11bc36d1e02ae6a7153 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 13:08:43 -0700 Subject: [PATCH 09/38] remove dead code --- xarray/coding/cftime_offsets.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 3bbe847acda..00181c70fb1 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -865,10 +865,6 @@ def _get_normalized_cfdate(date, calendar, normalize): return normalize_date(cf_date) if normalize else cf_date -def _generate_date_range(): - pass - - def _generate_date_range(start, end, periods): """Generate an equally-spaced sequence of cftime.datetime objects between and including two dates (whose length equals the number of periods).""" From 62df18140cf5d23926e038a303e0d1af2eeae52d Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 14:21:06 -0700 Subject: [PATCH 10/38] - Replace calls to cftime_range in core and unit tests - Fix bug in copy/paste bug in cftime_offsets. --- xarray/coding/cftime_offsets.py | 2 +- xarray/core/resample_cftime.py | 6 +- xarray/tests/test_backends.py | 4 +- xarray/tests/test_cftimeindex.py | 88 +++++++++++++---------- xarray/tests/test_cftimeindex_resample.py | 18 +++-- xarray/tests/test_coarsen.py | 4 +- xarray/tests/test_coding_times.py | 28 +++++--- xarray/tests/test_conventions.py | 4 +- xarray/tests/test_dataset.py | 8 ++- xarray/tests/test_distributed.py | 6 +- xarray/tests/test_duck_array_ops.py | 17 +++-- xarray/tests/test_interp.py | 24 ++++--- xarray/tests/test_missing.py | 26 ++++--- xarray/tests/test_plot.py | 8 ++- xarray/tests/test_weighted.py | 2 +- 15 files changed, 145 insertions(+), 100 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 00181c70fb1..3c811fe780d 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1198,7 +1198,7 @@ def _cftime_range( ) start = _get_normalized_cfdate(start, calendar, normalize) - end = _get_normalized_cfdate(start, calendar, normalize) + end = _get_normalized_cfdate(end, calendar, normalize) if freq is None: dates = _generate_date_range(start, end, periods) diff --git a/xarray/core/resample_cftime.py b/xarray/core/resample_cftime.py index c084640e763..949a95e32de 100644 --- a/xarray/core/resample_cftime.py +++ b/xarray/core/resample_cftime.py @@ -50,7 +50,7 @@ QuarterEnd, Tick, YearEnd, - cftime_range, + date_range, normalize_date, to_offset, ) @@ -219,8 +219,8 @@ def _get_time_bins( first, last = _get_range_edges( index.min(), index.max(), freq, closed=closed, origin=origin, offset=offset ) - datetime_bins = labels = cftime_range( - freq=freq, start=first, end=last, name=index.name + datetime_bins = labels = date_range( + freq=freq, start=first, end=last, name=index.name, use_cftime=True ) datetime_bins, labels = _adjust_bin_edges( diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 0237252f975..cbe4a7f9cbb 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -50,7 +50,7 @@ from xarray.backends.scipy_ import ScipyBackendEntrypoint from xarray.backends.zarr import ZarrStore from xarray.coders import CFDatetimeCoder, CFTimedeltaCoder -from xarray.coding.cftime_offsets import cftime_range +from xarray.coding.cftime_offsets import date_range from xarray.coding.strings import check_vlen_dtype, create_vlen_dtype from xarray.coding.variables import SerializationWarning from xarray.conventions import encode_dataset_coordinates @@ -3299,7 +3299,7 @@ def test_chunked_datetime64_or_timedelta64(self, dtype) -> None: @requires_dask def test_chunked_cftime_datetime(self) -> None: # Based on @malmans2's test in PR #8253 - times = cftime_range("2000", freq="D", periods=3) + times = date_range("2000", freq="D", periods=3, use_cftime=True) original = xr.Dataset(data_vars={"chunked_times": (["time"], times)}) original = original.chunk({"time": 1}) with self.roundtrip(original, open_kwargs={"chunks": {}}) as actual: diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 9531f3fbf0b..159e26130eb 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -819,7 +819,7 @@ def test_cftimeindex_add(index): @requires_cftime @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_cftimeindex_add_timedeltaindex(calendar) -> None: - a = xr.cftime_range("2000", periods=5, calendar=calendar) + a = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) deltas = pd.TimedeltaIndex([timedelta(days=2) for _ in range(5)]) result = a + deltas expected = a.shift(2, "D") @@ -841,7 +841,7 @@ def test_cftimeindex_add_timedeltaindex(calendar) -> None: ) @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_cftimeindex_shift_float(n, freq, units, calendar) -> None: - a = xr.cftime_range("2000", periods=3, calendar=calendar, freq="D") + a = xr.date_range("2000", periods=3, calendar=calendar, freq="D", use_cftime=True) result = a + pd.Timedelta(n, units) expected = a.shift(n, freq) assert result.equals(expected) @@ -850,7 +850,7 @@ def test_cftimeindex_shift_float(n, freq, units, calendar) -> None: @requires_cftime def test_cftimeindex_shift_float_us() -> None: - a = xr.cftime_range("2000", periods=3, freq="D") + a = xr.date_range("2000", periods=3, freq="D", use_cftime=True) with pytest.raises( ValueError, match="Could not convert to integer offset at any resolution" ): @@ -860,7 +860,7 @@ def test_cftimeindex_shift_float_us() -> None: @requires_cftime @pytest.mark.parametrize("freq", ["YS", "YE", "QS", "QE", "MS", "ME"]) def test_cftimeindex_shift_float_fails_for_non_tick_freqs(freq) -> None: - a = xr.cftime_range("2000", periods=3, freq="D") + a = xr.date_range("2000", periods=3, freq="D", use_cftime=True) with pytest.raises(TypeError, match="unsupported operand type"): a.shift(2.5, freq) @@ -883,7 +883,7 @@ def test_cftimeindex_radd(index): @requires_cftime @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_timedeltaindex_add_cftimeindex(calendar) -> None: - a = xr.cftime_range("2000", periods=5, calendar=calendar) + a = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) deltas = pd.TimedeltaIndex([timedelta(days=2) for _ in range(5)]) result = deltas + a expected = a.shift(2, "D") @@ -931,7 +931,7 @@ def test_cftimeindex_sub_timedelta_array(index, other): @requires_cftime @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_cftimeindex_sub_cftimeindex(calendar) -> None: - a = xr.cftime_range("2000", periods=5, calendar=calendar) + a = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) b = a.shift(2, "D") result = b - a expected = pd.TimedeltaIndex([timedelta(days=2) for _ in range(5)]) @@ -942,7 +942,7 @@ def test_cftimeindex_sub_cftimeindex(calendar) -> None: @requires_cftime @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_cftimeindex_sub_cftime_datetime(calendar): - a = xr.cftime_range("2000", periods=5, calendar=calendar) + a = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) result = a - a[0] expected = pd.TimedeltaIndex([timedelta(days=i) for i in range(5)]) assert result.equals(expected) @@ -952,7 +952,7 @@ def test_cftimeindex_sub_cftime_datetime(calendar): @requires_cftime @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_cftime_datetime_sub_cftimeindex(calendar): - a = xr.cftime_range("2000", periods=5, calendar=calendar) + a = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) result = a[0] - a expected = pd.TimedeltaIndex([timedelta(days=-i) for i in range(5)]) assert result.equals(expected) @@ -962,7 +962,7 @@ def test_cftime_datetime_sub_cftimeindex(calendar): @requires_cftime @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_distant_cftime_datetime_sub_cftimeindex(calendar): - a = xr.cftime_range("2000", periods=5, calendar=calendar) + a = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) with pytest.raises(ValueError, match="difference exceeds"): a.date_type(1, 1, 1) - a @@ -970,7 +970,7 @@ def test_distant_cftime_datetime_sub_cftimeindex(calendar): @requires_cftime @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_cftimeindex_sub_timedeltaindex(calendar) -> None: - a = xr.cftime_range("2000", periods=5, calendar=calendar) + a = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) deltas = pd.TimedeltaIndex([timedelta(days=2) for _ in range(5)]) result = a - deltas expected = a.shift(-2, "D") @@ -981,7 +981,7 @@ def test_cftimeindex_sub_timedeltaindex(calendar) -> None: @requires_cftime @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_cftimeindex_sub_index_of_cftime_datetimes(calendar): - a = xr.cftime_range("2000", periods=5, calendar=calendar) + a = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) b = pd.Index(a.values) expected = a - a result = a - b @@ -992,7 +992,7 @@ def test_cftimeindex_sub_index_of_cftime_datetimes(calendar): @requires_cftime @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_cftimeindex_sub_not_implemented(calendar): - a = xr.cftime_range("2000", periods=5, calendar=calendar) + a = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) with pytest.raises(TypeError, match="unsupported operand"): a - 1 @@ -1021,14 +1021,14 @@ def test_cftimeindex_shift(index, freq) -> None: @requires_cftime def test_cftimeindex_shift_invalid_periods() -> None: - index = xr.cftime_range("2000", periods=3) + index = xr.date_range("2000", periods=3, use_cftime=True) with pytest.raises(TypeError): index.shift("a", "D") # type: ignore[arg-type] @requires_cftime def test_cftimeindex_shift_invalid_freq() -> None: - index = xr.cftime_range("2000", periods=3) + index = xr.date_range("2000", periods=3, use_cftime=True) with pytest.raises(TypeError): index.shift(1, 1) # type: ignore[arg-type] @@ -1047,7 +1047,7 @@ def test_cftimeindex_shift_invalid_freq() -> None: ], ) def test_cftimeindex_calendar_property(calendar, expected): - index = xr.cftime_range(start="2000", periods=3, calendar=calendar) + index = xr.date_range(start="2000", periods=3, calendar=calendar, use_cftime=True) assert index.calendar == expected @@ -1072,7 +1072,9 @@ def test_empty_cftimeindex_calendar_property(): ) def test_cftimeindex_freq_property_none_size_lt_3(calendar): for periods in range(3): - index = xr.cftime_range(start="2000", periods=periods, calendar=calendar) + index = xr.date_range( + start="2000", periods=periods, calendar=calendar, use_cftime=True + ) assert index.freq is None @@ -1091,7 +1093,7 @@ def test_cftimeindex_freq_property_none_size_lt_3(calendar): ) def test_cftimeindex_calendar_repr(calendar, expected): """Test that cftimeindex has calendar property in repr.""" - index = xr.cftime_range(start="2000", periods=3, calendar=calendar) + index = xr.date_range(start="2000", periods=3, calendar=calendar, use_cftime=True) repr_str = index.__repr__() assert f" calendar='{expected}'" in repr_str assert "2000-01-01 00:00:00, 2000-01-02 00:00:00" in repr_str @@ -1101,7 +1103,7 @@ def test_cftimeindex_calendar_repr(calendar, expected): @pytest.mark.parametrize("periods", [2, 40]) def test_cftimeindex_periods_repr(periods): """Test that cftimeindex has periods property in repr.""" - index = xr.cftime_range(start="2000", periods=periods) + index = xr.date_range(start="2000", periods=periods, use_cftime=True) repr_str = index.__repr__() assert f" length={periods}" in repr_str @@ -1111,7 +1113,9 @@ def test_cftimeindex_periods_repr(periods): @pytest.mark.parametrize("freq", ["D", "h"]) def test_cftimeindex_freq_in_repr(freq, calendar): """Test that cftimeindex has frequency property in repr.""" - index = xr.cftime_range(start="2000", periods=3, freq=freq, calendar=calendar) + index = xr.date_range( + start="2000", periods=3, freq=freq, calendar=calendar, use_cftime=True + ) repr_str = index.__repr__() assert f", freq='{freq}'" in repr_str @@ -1151,7 +1155,7 @@ def test_cftimeindex_freq_in_repr(freq, calendar): ) def test_cftimeindex_repr_formatting(periods, expected): """Test that cftimeindex.__repr__ is formatted similar to pd.Index.__repr__.""" - index = xr.cftime_range(start="2000", periods=periods, freq="D") + index = xr.date_range(start="2000", periods=periods, freq="D", use_cftime=True) expected = dedent(expected) assert expected == repr(index) @@ -1161,7 +1165,7 @@ def test_cftimeindex_repr_formatting(periods, expected): @pytest.mark.parametrize("periods", [2, 3, 4, 100, 101]) def test_cftimeindex_repr_formatting_width(periods, display_width): """Test that cftimeindex is sensitive to OPTIONS['display_width'].""" - index = xr.cftime_range(start="2000", periods=periods) + index = xr.date_range(start="2000", periods=periods, use_cftime=True) len_intro_str = len("CFTimeIndex(") with xr.set_options(display_width=display_width): repr_str = index.__repr__() @@ -1177,8 +1181,8 @@ def test_cftimeindex_repr_formatting_width(periods, display_width): @requires_cftime @pytest.mark.parametrize("periods", [22, 50, 100]) def test_cftimeindex_repr_101_shorter(periods): - index_101 = xr.cftime_range(start="2000", periods=101) - index_periods = xr.cftime_range(start="2000", periods=periods) + index_101 = xr.date_range(start="2000", periods=101, use_cftime=True) + index_periods = xr.date_range(start="2000", periods=periods, use_cftime=True) index_101_repr_str = index_101.__repr__() index_periods_repr_str = index_periods.__repr__() assert len(index_101_repr_str) < len(index_periods_repr_str) @@ -1210,7 +1214,7 @@ def test_parse_array_of_cftime_strings(): @pytest.mark.parametrize("calendar", _ALL_CALENDARS) def test_strftime_of_cftime_array(calendar): date_format = "%Y%m%d%H%M" - cf_values = xr.cftime_range("2000", periods=5, calendar=calendar) + cf_values = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) dt_values = pd.date_range("2000", periods=5) expected = pd.Index(dt_values.strftime(date_format)) result = cf_values.strftime(date_format) @@ -1221,7 +1225,7 @@ def test_strftime_of_cftime_array(calendar): @pytest.mark.parametrize("calendar", _ALL_CALENDARS) @pytest.mark.parametrize("unsafe", [False, True]) def test_to_datetimeindex(calendar, unsafe) -> None: - index = xr.cftime_range("2000", periods=5, calendar=calendar) + index = xr.date_range("2000", periods=5, calendar=calendar, use_cftime=True) expected = pd.date_range("2000", periods=5, unit="ns") if calendar in _NON_STANDARD_CALENDARS and not unsafe: @@ -1237,7 +1241,7 @@ def test_to_datetimeindex(calendar, unsafe) -> None: @requires_cftime def test_to_datetimeindex_future_warning() -> None: - index = xr.cftime_range("2000", periods=5) + index = xr.date_range("2000", periods=5, use_cftime=True) expected = pd.date_range("2000", periods=5, unit="ns") with pytest.warns(FutureWarning, match="In a future version"): result = index.to_datetimeindex() @@ -1248,7 +1252,7 @@ def test_to_datetimeindex_future_warning() -> None: @requires_cftime @pytest.mark.parametrize("calendar", _ALL_CALENDARS) def test_to_datetimeindex_out_of_range(calendar) -> None: - index = xr.cftime_range("0001", periods=5, calendar=calendar) + index = xr.date_range("0001", periods=5, calendar=calendar, use_cftime=True) with pytest.raises(ValueError, match="0001"): index.to_datetimeindex(time_unit="ns") @@ -1256,7 +1260,7 @@ def test_to_datetimeindex_out_of_range(calendar) -> None: @requires_cftime @pytest.mark.parametrize("unsafe", [False, True]) def test_to_datetimeindex_gregorian_pre_reform(unsafe) -> None: - index = xr.cftime_range("1582", periods=5, calendar="gregorian") + index = xr.date_range("1582", periods=5, calendar="gregorian", use_cftime=True) if unsafe: result = index.to_datetimeindex(time_unit="us", unsafe=unsafe) else: @@ -1270,7 +1274,7 @@ def test_to_datetimeindex_gregorian_pre_reform(unsafe) -> None: @requires_cftime @pytest.mark.parametrize("calendar", ["all_leap", "360_day"]) def test_to_datetimeindex_feb_29(calendar) -> None: - index = xr.cftime_range("2001-02-28", periods=2, calendar=calendar) + index = xr.date_range("2001-02-28", periods=2, calendar=calendar, use_cftime=True) with pytest.raises(ValueError, match="29"): index.to_datetimeindex(time_unit="ns") @@ -1278,7 +1282,9 @@ def test_to_datetimeindex_feb_29(calendar) -> None: @pytest.mark.xfail(reason="fails on pandas main branch") @requires_cftime def test_multiindex(): - index = xr.cftime_range("2001-01-01", periods=100, calendar="360_day") + index = xr.date_range( + "2001-01-01", periods=100, calendar="360_day", use_cftime=True + ) mindex = pd.MultiIndex.from_arrays([index]) assert mindex.get_loc("2001-01") == slice(0, 30) @@ -1290,7 +1296,9 @@ def test_rounding_methods_against_datetimeindex(freq, method) -> None: # for now unit="us" seems good enough expected = pd.date_range("2000-01-02T01:03:51", periods=10, freq="1777s", unit="ns") expected = getattr(expected, method)(freq) - result = xr.cftime_range("2000-01-02T01:03:51", periods=10, freq="1777s") + result = xr.date_range( + "2000-01-02T01:03:51", periods=10, freq="1777s", use_cftime=True + ) result = getattr(result, method)(freq).to_datetimeindex(time_unit="ns") assert result.equals(expected) @@ -1310,7 +1318,9 @@ def test_rounding_methods_empty_cftimindex(method): @requires_cftime @pytest.mark.parametrize("method", ["floor", "ceil", "round"]) def test_rounding_methods_invalid_freq(method): - index = xr.cftime_range("2000-01-02T01:03:51", periods=10, freq="1777s") + index = xr.date_range( + "2000-01-02T01:03:51", periods=10, freq="1777s", use_cftime=True + ) with pytest.raises(ValueError, match="fixed"): getattr(index, method)("MS") @@ -1395,7 +1405,7 @@ def test_asi8_empty_cftimeindex(): @requires_cftime def test_infer_freq_valid_types(time_unit: PDDatetimeUnitOptions) -> None: - cf_indx = xr.cftime_range("2000-01-01", periods=3, freq="D") + cf_indx = xr.date_range("2000-01-01", periods=3, freq="D", use_cftime=True) assert xr.infer_freq(cf_indx) == "D" assert xr.infer_freq(xr.DataArray(cf_indx)) == "D" @@ -1414,7 +1424,7 @@ def test_infer_freq_invalid_inputs(): with pytest.raises(ValueError, match="must contain datetime-like objects"): xr.infer_freq(xr.DataArray([0, 1, 2])) - indx = xr.cftime_range("1990-02-03", periods=4, freq="MS") + indx = xr.date_range("1990-02-03", periods=4, freq="MS", use_cftime=True) # 2D DataArray with pytest.raises(ValueError, match="must be 1D"): xr.infer_freq(xr.DataArray([indx, indx])) @@ -1433,7 +1443,7 @@ def test_infer_freq_invalid_inputs(): assert xr.infer_freq(indx[np.array([0, 1, 3])]) is None # Same, but for QS - indx = xr.cftime_range("1990-02-03", periods=4, freq="QS") + indx = xr.date_range("1990-02-03", periods=4, freq="QS", use_cftime=True) assert xr.infer_freq(indx[np.array([0, 1, 3])]) is None @@ -1458,7 +1468,9 @@ def test_infer_freq_invalid_inputs(): ) @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_infer_freq(freq, calendar): - indx = xr.cftime_range("2000-01-01", periods=3, freq=freq, calendar=calendar) + indx = xr.date_range( + "2000-01-01", periods=3, freq=freq, calendar=calendar, use_cftime=True + ) out = xr.infer_freq(indx) assert out == freq @@ -1466,6 +1478,8 @@ def test_infer_freq(freq, calendar): @requires_cftime @pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) def test_pickle_cftimeindex(calendar): - idx = xr.cftime_range("2000-01-01", periods=3, freq="D", calendar=calendar) + idx = xr.date_range( + "2000-01-01", periods=3, freq="D", calendar=calendar, use_cftime=True + ) idx_pkl = pickle.loads(pickle.dumps(idx)) assert (idx == idx_pkl).all() diff --git a/xarray/tests/test_cftimeindex_resample.py b/xarray/tests/test_cftimeindex_resample.py index 5510eaacf1f..fa896da0ed6 100644 --- a/xarray/tests/test_cftimeindex_resample.py +++ b/xarray/tests/test_cftimeindex_resample.py @@ -132,7 +132,9 @@ def test_resample(freqs, closed, label, offset) -> None: datetime_index = pd.date_range( start=start, periods=5, freq=_new_to_legacy_freq(initial_freq) ) - cftime_index = xr.cftime_range(start=start, periods=5, freq=initial_freq) + cftime_index = xr.date_range( + start=start, periods=5, freq=initial_freq, use_cftime=True + ) da_datetimeindex = da(datetime_index) da_cftimeindex = da(cftime_index) @@ -174,8 +176,12 @@ def test_closed_label_defaults(freq, expected) -> None: def test_calendars(calendar: str) -> None: # Limited testing for non-standard calendars freq, closed, label = "8001min", None, None - xr_index = xr.cftime_range( - start="2004-01-01T12:07:01", periods=7, freq="3D", calendar=calendar + xr_index = xr.date_range( + start="2004-01-01T12:07:01", + periods=7, + freq="3D", + calendar=calendar, + use_cftime=True, ) pd_index = pd.date_range(start="2004-01-01T12:07:01", periods=7, freq="3D") da_cftime = da(xr_index).resample(time=freq, closed=closed, label=label).mean() @@ -204,7 +210,7 @@ def test_origin(closed, origin) -> None: start = "1969-12-31T12:07:01" index_kwargs: DateRangeKwargs = dict(start=start, periods=12, freq=initial_freq) datetime_index = pd.date_range(**index_kwargs) - cftime_index = xr.cftime_range(**index_kwargs) + cftime_index = xr.date_range(**index_kwargs, use_cftime=True) da_datetimeindex = da(datetime_index) da_cftimeindex = da(cftime_index) @@ -219,7 +225,7 @@ def test_origin(closed, origin) -> None: @pytest.mark.parametrize("offset", ["foo", "5MS", 10]) def test_invalid_offset_error(offset: str | int) -> None: - cftime_index = xr.cftime_range("2000", periods=5) + cftime_index = xr.date_range("2000", periods=5, use_cftime=True) da_cftime = da(cftime_index) with pytest.raises(ValueError, match="offset must be"): da_cftime.resample(time="2D", offset=offset) # type: ignore[arg-type] @@ -229,7 +235,7 @@ def test_timedelta_offset() -> None: timedelta = datetime.timedelta(seconds=5) string = "5s" - cftime_index = xr.cftime_range("2000", periods=5) + cftime_index = xr.date_range("2000", periods=5, use_cftime=True) da_cftime = da(cftime_index) timedelta_result = da_cftime.resample(time="2D", offset=timedelta).mean() diff --git a/xarray/tests/test_coarsen.py b/xarray/tests/test_coarsen.py index ab04a7b3cde..8faa839d874 100644 --- a/xarray/tests/test_coarsen.py +++ b/xarray/tests/test_coarsen.py @@ -68,10 +68,10 @@ def test_coarsen_coords(ds, dask): @requires_cftime def test_coarsen_coords_cftime(): - times = xr.cftime_range("2000", periods=6) + times = xr.date_range("2000", periods=6, use_cftime=True) da = xr.DataArray(range(6), [("time", times)]) actual = da.coarsen(time=3).mean() - expected_times = xr.cftime_range("2000-01-02", freq="3D", periods=2) + expected_times = xr.date_range("2000-01-02", freq="3D", periods=2, use_cftime=True) np.testing.assert_array_equal(actual.time, expected_times) diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py index 2e61e5d853e..1bc0037decc 100644 --- a/xarray/tests/test_coding_times.py +++ b/xarray/tests/test_coding_times.py @@ -14,7 +14,6 @@ DataArray, Dataset, Variable, - cftime_range, conventions, date_range, decode_cf, @@ -1086,15 +1085,15 @@ def test_decode_ambiguous_time_warns(calendar) -> None: @pytest.mark.filterwarnings("ignore:Times can't be serialized faithfully") @pytest.mark.parametrize("encoding_units", FREQUENCIES_TO_ENCODING_UNITS.values()) @pytest.mark.parametrize("freq", FREQUENCIES_TO_ENCODING_UNITS.keys()) -@pytest.mark.parametrize("date_range", [pd.date_range, cftime_range]) +@pytest.mark.parametrize("use_cftime", [True, False]) def test_encode_cf_datetime_defaults_to_correct_dtype( - encoding_units, freq, date_range + encoding_units, freq, use_cftime ) -> None: - if not has_cftime and date_range == cftime_range: + if not has_cftime and use_cftime: pytest.skip("Test requires cftime") - if (freq == "ns" or encoding_units == "nanoseconds") and date_range == cftime_range: + if (freq == "ns" or encoding_units == "nanoseconds") and use_cftime: pytest.skip("Nanosecond frequency is not valid for cftime dates.") - times = date_range("2000", periods=3, freq=freq) + times = date_range("2000", periods=3, freq=freq, use_cftime=use_cftime) units = f"{encoding_units} since 2000-01-01" encoded, _units, _ = encode_cf_datetime(times, units) @@ -1125,9 +1124,10 @@ def test_encode_decode_roundtrip_datetime64( @requires_cftime @pytest.mark.parametrize("freq", ["us", "ms", "s", "min", "h", "D"]) def test_encode_decode_roundtrip_cftime(freq) -> None: - initial_time = cftime_range("0001", periods=1) + initial_time = date_range("0001", periods=1, use_cftime=True) times = initial_time.append( - cftime_range("0001", periods=2, freq=freq) + timedelta(days=291000 * 365) + date_range("0001", periods=2, freq=freq, use_cftime=True) + + timedelta(days=291000 * 365) ) variable = Variable(["time"], times) encoded = conventions.encode_cf_variable(variable) @@ -1208,7 +1208,9 @@ def test_decode_encode_roundtrip_with_non_lowercase_letters( @requires_cftime def test_should_cftime_be_used_source_outside_range(): - src = cftime_range("1000-01-01", periods=100, freq="MS", calendar="noleap") + src = date_range( + "1000-01-01", periods=100, freq="MS", calendar="noleap", use_cftime=True + ) with pytest.raises( ValueError, match="Source time range is not valid for numpy datetimes." ): @@ -1217,7 +1219,9 @@ def test_should_cftime_be_used_source_outside_range(): @requires_cftime def test_should_cftime_be_used_target_not_npable(): - src = cftime_range("2000-01-01", periods=100, freq="MS", calendar="noleap") + src = date_range( + "2000-01-01", periods=100, freq="MS", calendar="noleap", use_cftime=True + ) with pytest.raises( ValueError, match="Calendar 'noleap' is only valid with cftime." ): @@ -1678,7 +1682,9 @@ def test_encode_cf_datetime_cftime_datetime_via_dask(units, dtype) -> None: import dask.array calendar = "standard" - times_idx = cftime_range(start="1700", freq="D", periods=3, calendar=calendar) + times_idx = date_range( + start="1700", freq="D", periods=3, calendar=calendar, use_cftime=True + ) times = dask.array.from_array(times_idx, chunks=1) encoded_times, encoding_units, encoding_calendar = encode_cf_datetime( times, units, None, dtype diff --git a/xarray/tests/test_conventions.py b/xarray/tests/test_conventions.py index 8d3827fac54..1d3a8bc809d 100644 --- a/xarray/tests/test_conventions.py +++ b/xarray/tests/test_conventions.py @@ -11,9 +11,9 @@ Dataset, SerializationWarning, Variable, - cftime_range, coding, conventions, + date_range, open_dataset, ) from xarray.backends.common import WritableCFDataStore @@ -624,7 +624,7 @@ def test_decode_cf_variable_datetime64(): @requires_cftime def test_decode_cf_variable_cftime(): - variable = Variable(["time"], cftime_range("2000", periods=2)) + variable = Variable(["time"], date_range("2000", periods=2, use_cftime=True)) decoded = conventions.decode_cf_variable("time", variable) assert decoded.encoding == {} assert_identical(decoded, variable) diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index c3302dd6c9d..fa088f09f98 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -3222,7 +3222,9 @@ def test_rename_preserve_attrs_encoding(self) -> None: def test_rename_does_not_change_CFTimeIndex_type(self) -> None: # make sure CFTimeIndex is not converted to DatetimeIndex #3522 - time = xr.cftime_range(start="2000", periods=6, freq="2MS", calendar="noleap") + time = xr.date_range( + start="2000", periods=6, freq="2MS", calendar="noleap", use_cftime=True + ) orig = Dataset(coords={"time": time}) renamed = orig.rename(time="time_new") @@ -7265,7 +7267,7 @@ def test_differentiate_datetime(dask) -> None: @pytest.mark.parametrize("dask", [True, False]) def test_differentiate_cftime(dask) -> None: rs = np.random.default_rng(42) - coord = xr.cftime_range("2000", periods=8, freq="2ME") + coord = xr.date_range("2000", periods=8, freq="2ME", use_cftime=True) da = xr.DataArray( rs.random((8, 6)), @@ -7428,7 +7430,7 @@ def test_trapezoid_datetime(dask, which_datetime) -> None: else: if not has_cftime: pytest.skip("Test requires cftime.") - coord = xr.cftime_range("2000", periods=8, freq="2D") + coord = xr.date_range("2000", periods=8, freq="2D", use_cftime=True) da = xr.DataArray( rs.random((8, 6)), diff --git a/xarray/tests/test_distributed.py b/xarray/tests/test_distributed.py index e4fdf08d0b4..a2021e30e0f 100644 --- a/xarray/tests/test_distributed.py +++ b/xarray/tests/test_distributed.py @@ -132,7 +132,7 @@ def test_dask_distributed_write_netcdf_with_dimensionless_variables( @requires_netCDF4 @pytest.mark.parametrize("parallel", (True, False)) def test_open_mfdataset_can_open_files_with_cftime_index(parallel, tmp_path): - T = xr.cftime_range("20010101", "20010501", calendar="360_day") + T = xr.date_range("20010101", "20010501", calendar="360_day", use_cftime=True) Lon = np.arange(100) data = np.random.random((T.size, Lon.size)) da = xr.DataArray(data, coords={"time": T, "Lon": Lon}, name="test") @@ -149,7 +149,7 @@ def test_open_mfdataset_can_open_files_with_cftime_index(parallel, tmp_path): @pytest.mark.parametrize("parallel", (True, False)) def test_open_mfdataset_multiple_files_parallel_distributed(parallel, tmp_path): lon = np.arange(100) - time = xr.cftime_range("20010101", periods=100, calendar="360_day") + time = xr.date_range("20010101", periods=100, calendar="360_day", use_cftime=True) data = np.random.random((time.size, lon.size)) da = xr.DataArray(data, coords={"time": time, "lon": lon}, name="test") @@ -177,7 +177,7 @@ def test_open_mfdataset_multiple_files_parallel(parallel, tmp_path): "Flaky in CI. Would be a welcome contribution to make a similar test reliable." ) lon = np.arange(100) - time = xr.cftime_range("20010101", periods=100, calendar="360_day") + time = xr.date_range("20010101", periods=100, calendar="360_day", use_cftime=True) data = np.random.random((time.size, lon.size)) da = xr.DataArray(data, coords={"time": time, "lon": lon}, name="test") diff --git a/xarray/tests/test_duck_array_ops.py b/xarray/tests/test_duck_array_ops.py index 1f4ef2acf91..c0ecfe638dc 100644 --- a/xarray/tests/test_duck_array_ops.py +++ b/xarray/tests/test_duck_array_ops.py @@ -8,7 +8,7 @@ import pytest from numpy import array, nan -from xarray import DataArray, Dataset, cftime_range, concat +from xarray import DataArray, Dataset, concat, date_range from xarray.coding.times import _NS_PER_TIME_DELTA from xarray.core import dtypes, duck_array_ops from xarray.core.duck_array_ops import ( @@ -454,7 +454,7 @@ def test_cftime_datetime_mean(dask): if dask and not has_dask: pytest.skip("requires dask") - times = cftime_range("2000", periods=4) + times = date_range("2000", periods=4, use_cftime=True) da = DataArray(times, dims=["time"]) da_2d = DataArray(times.values.reshape(2, 2)) @@ -501,7 +501,10 @@ def test_mean_over_non_time_dim_of_dataset_with_dask_backed_cftime_data(): # dimension still fails if the time variable is dask-backed. ds = Dataset( { - "var1": (("time",), cftime_range("2021-10-31", periods=10, freq="D")), + "var1": ( + ("time",), + date_range("2021-10-31", periods=10, freq="D", use_cftime=True), + ), "var2": (("x",), list(range(10))), } ) @@ -900,7 +903,9 @@ def test_datetime_to_numeric_cftime(dask): if dask and not has_dask: pytest.skip("requires dask") - times = cftime_range("2000", periods=5, freq="7D", calendar="standard").values + times = date_range( + "2000", periods=5, freq="7D", calendar="standard", use_cftime=True + ).values if dask: import dask.array @@ -946,8 +951,8 @@ def test_datetime_to_numeric_potential_overflow(time_unit: PDDatetimeUnitOptions pytest.skip("out-of-bounds datetime64 overflow") dtype = f"M8[{time_unit}]" times = pd.date_range("2000", periods=5, freq="7D").values.astype(dtype) - cftimes = cftime_range( - "2000", periods=5, freq="7D", calendar="proleptic_gregorian" + cftimes = date_range( + "2000", periods=5, freq="7D", calendar="proleptic_gregorian", use_cftime=True ).values offset = np.datetime64("0001-01-01", time_unit) diff --git a/xarray/tests/test_interp.py b/xarray/tests/test_interp.py index b2171f31c33..df265029250 100644 --- a/xarray/tests/test_interp.py +++ b/xarray/tests/test_interp.py @@ -751,10 +751,12 @@ def test_datetime_single_string() -> None: @requires_cftime @requires_scipy def test_cftime() -> None: - times = xr.cftime_range("2000", periods=24, freq="D") + times = xr.date_range("2000", periods=24, freq="D", use_cftime=True) da = xr.DataArray(np.arange(24), coords=[times], dims="time") - times_new = xr.cftime_range("2000-01-01T12:00:00", periods=3, freq="D") + times_new = xr.date_range( + "2000-01-01T12:00:00", periods=3, freq="D", use_cftime=True + ) actual = da.interp(time=times_new) expected = xr.DataArray([0.5, 1.5, 2.5], coords=[times_new], dims=["time"]) @@ -764,11 +766,11 @@ def test_cftime() -> None: @requires_cftime @requires_scipy def test_cftime_type_error() -> None: - times = xr.cftime_range("2000", periods=24, freq="D") + times = xr.date_range("2000", periods=24, freq="D", use_cftime=True) da = xr.DataArray(np.arange(24), coords=[times], dims="time") - times_new = xr.cftime_range( - "2000-01-01T12:00:00", periods=3, freq="D", calendar="noleap" + times_new = xr.date_range( + "2000-01-01T12:00:00", periods=3, freq="D", calendar="noleap", use_cftime=True ) with pytest.raises(TypeError): da.interp(time=times_new) @@ -779,8 +781,8 @@ def test_cftime_type_error() -> None: def test_cftime_list_of_strings() -> None: from cftime import DatetimeProlepticGregorian - times = xr.cftime_range( - "2000", periods=24, freq="D", calendar="proleptic_gregorian" + times = xr.date_range( + "2000", periods=24, freq="D", calendar="proleptic_gregorian", use_cftime=True ) da = xr.DataArray(np.arange(24), coords=[times], dims="time") @@ -800,8 +802,8 @@ def test_cftime_list_of_strings() -> None: def test_cftime_single_string() -> None: from cftime import DatetimeProlepticGregorian - times = xr.cftime_range( - "2000", periods=24, freq="D", calendar="proleptic_gregorian" + times = xr.date_range( + "2000", periods=24, freq="D", calendar="proleptic_gregorian", use_cftime=True ) da = xr.DataArray(np.arange(24), coords=[times], dims="time") @@ -830,7 +832,7 @@ def test_datetime_to_non_datetime_error() -> None: @requires_cftime @requires_scipy def test_cftime_to_non_cftime_error() -> None: - times = xr.cftime_range("2000", periods=24, freq="D") + times = xr.date_range("2000", periods=24, freq="D", use_cftime=True) da = xr.DataArray(np.arange(24), coords=[times], dims="time") with pytest.raises(TypeError): @@ -859,7 +861,7 @@ def test_datetime_interp_noerror() -> None: @requires_cftime @requires_scipy def test_3641() -> None: - times = xr.cftime_range("0001", periods=3, freq="500YE") + times = xr.date_range("0001", periods=3, freq="500YE", use_cftime=True) da = xr.DataArray(range(3), dims=["time"], coords=[times]) da.interp(time=["0002-05-01"]) diff --git a/xarray/tests/test_missing.py b/xarray/tests/test_missing.py index d3eaa70f1df..1f9d94868eb 100644 --- a/xarray/tests/test_missing.py +++ b/xarray/tests/test_missing.py @@ -40,8 +40,12 @@ def da(): @pytest.fixture def cf_da(): def _cf_da(calendar, freq="1D"): - times = xr.cftime_range( - start="1970-01-01", freq=freq, periods=10, calendar=calendar + times = xr.date_range( + start="1970-01-01", + freq=freq, + periods=10, + calendar=calendar, + use_cftime=True, ) values = np.arange(10) return xr.DataArray(values, dims=("time",), coords={"time": times}) @@ -623,7 +627,11 @@ def test_get_clean_interp_index_potential_overflow(): da = xr.DataArray( [0, 1, 2], dims=("time",), - coords={"time": xr.cftime_range("0000-01-01", periods=3, calendar="360_day")}, + coords={ + "time": xr.date_range( + "0000-01-01", periods=3, calendar="360_day", use_cftime=True + ) + }, ) get_clean_interp_index(da, "time") @@ -670,17 +678,17 @@ def test_interpolate_na_max_gap_errors(da_time): @requires_bottleneck @pytest.mark.parametrize( - "time_range_func", - [pd.date_range, pytest.param(xr.cftime_range, marks=requires_cftime)], + "use_cftime", + [False, pytest.param(True, marks=requires_cftime)], ) @pytest.mark.parametrize("transform", [lambda x: x, lambda x: x.to_dataset(name="a")]) @pytest.mark.parametrize( "max_gap", ["3h", np.timedelta64(3, "h"), pd.to_timedelta("3h")] ) -def test_interpolate_na_max_gap_time_specifier( - da_time, max_gap, transform, time_range_func -): - da_time["t"] = time_range_func("2001-01-01", freq="h", periods=11) +def test_interpolate_na_max_gap_time_specifier(da_time, max_gap, transform, use_cftime): + da_time["t"] = xr.date_range( + "2001-01-01", freq="h", periods=11, use_cftime=use_cftime + ) expected = transform( da_time.copy(data=[np.nan, 1, 2, 3, 4, 5, np.nan, np.nan, np.nan, np.nan, 10]) ) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 0a05451cb85..b9adda5b52d 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -2994,7 +2994,9 @@ def setUp(self) -> None: """ # case for 1d array data = np.random.rand(4, 12) - time = xr.cftime_range(start="2017", periods=12, freq="1ME", calendar="noleap") + time = xr.date_range( + start="2017", periods=12, freq="1ME", calendar="noleap", use_cftime=True + ) darray = DataArray(data, dims=["x", "time"]) darray.coords["time"] = time @@ -3022,8 +3024,8 @@ def setUp(self) -> None: month = np.arange(1, 13, 1) data = np.sin(2 * np.pi * month / 12.0) darray = DataArray(data, dims=["time"]) - darray.coords["time"] = xr.cftime_range( - start="2017", periods=12, freq="1ME", calendar="noleap" + darray.coords["time"] = xr.date_range( + start="2017", periods=12, freq="1ME", calendar="noleap", use_cftime=True ) self.darray = darray diff --git a/xarray/tests/test_weighted.py b/xarray/tests/test_weighted.py index 93a200c07a6..e9be98ab76b 100644 --- a/xarray/tests/test_weighted.py +++ b/xarray/tests/test_weighted.py @@ -67,7 +67,7 @@ def mean_func(ds): return ds.weighted(ds.weights).mean("time") # example dataset - t = xr.cftime_range(start="2000", periods=20, freq="1YS") + t = xr.date_range(start="2000", periods=20, freq="1YS", use_cftime=True) weights = xr.DataArray(np.random.rand(len(t)), dims=["time"], coords={"time": t}) data = xr.DataArray( np.random.rand(len(t)), dims=["time"], coords={"time": t, "weights": weights} From 7938bb72bc2f47dbd77522e4499f98b8d6bb1904 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 14:51:54 -0700 Subject: [PATCH 11/38] ignore deprecation warnings in test_cftime_offsets.py --- xarray/coding/cftime_offsets.py | 2 +- xarray/tests/test_cftime_offsets.py | 116 +++++++++++++++------------- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 3c811fe780d..c4ccf8d01fd 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1112,7 +1112,7 @@ def cftime_range( ... start="2000", periods=6, freq="2MS", calendar="noleap" ... ) # +doctest: ELLIPSIS Traceback (most recent call last): - ... + ... DeprecationWarning: cftime_range() is deprecated, please use date_range(use_cftime=True) instead. CFTimeIndex([2000-01-01 00:00:00, 2000-03-01 00:00:00, 2000-05-01 00:00:00, 2000-07-01 00:00:00, 2000-09-01 00:00:00, 2000-11-01 00:00:00], diff --git a/xarray/tests/test_cftime_offsets.py b/xarray/tests/test_cftime_offsets.py index 086e187d2e6..a51a4385322 100644 --- a/xarray/tests/test_cftime_offsets.py +++ b/xarray/tests/test_cftime_offsets.py @@ -1,7 +1,6 @@ from __future__ import annotations import warnings -from collections.abc import Callable from itertools import product from typing import Literal @@ -1230,15 +1229,16 @@ def test_cftime_range( if isinstance(end, tuple): end = date_type(*end) - result = cftime_range( - start=start, - end=end, - periods=periods, - freq=freq, - inclusive=inclusive, - normalize=normalize, - calendar=calendar, - ) + with pytest.warns(DeprecationWarning): + result = cftime_range( + start=start, + end=end, + periods=periods, + freq=freq, + inclusive=inclusive, + normalize=normalize, + calendar=calendar, + ) resulting_dates = result.values assert isinstance(result, CFTimeIndex) @@ -1256,11 +1256,12 @@ def test_cftime_range( def test_cftime_range_name(): - result = cftime_range(start="2000", periods=4, name="foo") - assert result.name == "foo" + with pytest.warns(DeprecationWarning): + result = cftime_range(start="2000", periods=4, name="foo") + assert result.name == "foo" - result = cftime_range(start="2000", periods=4) - assert result.name is None + result = cftime_range(start="2000", periods=4) + assert result.name is None @pytest.mark.parametrize( @@ -1281,7 +1282,7 @@ def test_invalid_cftime_range_inputs( freq: str | None, inclusive: Literal["up", None], ) -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError), pytest.warns(DeprecationWarning): cftime_range(start, end, periods, freq, inclusive=inclusive) # type: ignore[arg-type] @@ -1304,12 +1305,13 @@ def test_calendar_specific_month_end( calendar: str, expected_month_day: list[tuple[int, int]] ) -> None: year = 2000 # Use a leap-year to highlight calendar differences - result = cftime_range( - start="2000-02", end="2001", freq="2ME", calendar=calendar - ).values date_type = get_date_type(calendar) expected = [date_type(year, *args) for args in expected_month_day] - np.testing.assert_equal(result, expected) + with pytest.warns(DeprecationWarning): + result = cftime_range( + start="2000-02", end="2001", freq="2ME", calendar=calendar + ).values + np.testing.assert_equal(result, expected) @pytest.mark.parametrize( @@ -1321,15 +1323,16 @@ def test_calendar_specific_month_end_negative_freq( calendar: str, expected_month_day: list[tuple[int, int]] ) -> None: year = 2000 # Use a leap-year to highlight calendar differences - result = cftime_range( - start="2001", - end="2000", - freq="-2ME", - calendar=calendar, - ).values date_type = get_date_type(calendar) expected = [date_type(year, *args) for args in expected_month_day[::-1]] - np.testing.assert_equal(result, expected) + with pytest.warns(DeprecationWarning): + result = cftime_range( + start="2001", + end="2000", + freq="-2ME", + calendar=calendar, + ).values + np.testing.assert_equal(result, expected) @pytest.mark.parametrize( @@ -1352,33 +1355,37 @@ def test_calendar_specific_month_end_negative_freq( def test_calendar_year_length( calendar: str, start: str, end: str, expected_number_of_days: int ) -> None: - result = cftime_range(start, end, freq="D", inclusive="left", calendar=calendar) - assert len(result) == expected_number_of_days + with pytest.warns(DeprecationWarning): + result = cftime_range(start, end, freq="D", inclusive="left", calendar=calendar) + assert len(result) == expected_number_of_days @pytest.mark.parametrize("freq", ["YE", "ME", "D"]) def test_dayofweek_after_cftime_range(freq: str) -> None: - result = cftime_range("2000-02-01", periods=3, freq=freq).dayofweek - # TODO: remove once requiring pandas 2.2+ - freq = _new_to_legacy_freq(freq) - expected = pd.date_range("2000-02-01", periods=3, freq=freq).dayofweek - np.testing.assert_array_equal(result, expected) + with pytest.warns(DeprecationWarning): + result = cftime_range("2000-02-01", periods=3, freq=freq).dayofweek + # TODO: remove once requiring pandas 2.2+ + freq = _new_to_legacy_freq(freq) + expected = pd.date_range("2000-02-01", periods=3, freq=freq).dayofweek + np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize("freq", ["YE", "ME", "D"]) def test_dayofyear_after_cftime_range(freq: str) -> None: - result = cftime_range("2000-02-01", periods=3, freq=freq).dayofyear - # TODO: remove once requiring pandas 2.2+ - freq = _new_to_legacy_freq(freq) - expected = pd.date_range("2000-02-01", periods=3, freq=freq).dayofyear - np.testing.assert_array_equal(result, expected) + with pytest.warns(DeprecationWarning): + result = cftime_range("2000-02-01", periods=3, freq=freq).dayofyear + # TODO: remove once requiring pandas 2.2+ + freq = _new_to_legacy_freq(freq) + expected = pd.date_range("2000-02-01", periods=3, freq=freq).dayofyear + np.testing.assert_array_equal(result, expected) def test_cftime_range_standard_calendar_refers_to_gregorian() -> None: from cftime import DatetimeGregorian - (result,) = cftime_range("2000", periods=1) - assert isinstance(result, DatetimeGregorian) + with pytest.warns(DeprecationWarning): + (result,) = cftime_range("2000", periods=1) + assert isinstance(result, DatetimeGregorian) @pytest.mark.parametrize( @@ -1518,22 +1525,24 @@ def as_timedelta_not_implemented_error(): tick.as_timedelta() -@pytest.mark.parametrize("function", [cftime_range, date_range]) -def test_cftime_or_date_range_invalid_inclusive_value(function: Callable) -> None: - if function == cftime_range and not has_cftime: +@pytest.mark.parametrize("use_cftime", [True, False]) +def test_cftime_or_date_range_invalid_inclusive_value(use_cftime: bool) -> None: + if use_cftime and not has_cftime: pytest.skip("requires cftime") with pytest.raises(ValueError, match="nclusive"): - function("2000", periods=3, inclusive="foo") + date_range("2000", periods=3, inclusive="foo", use_cftime=use_cftime) -@pytest.mark.parametrize("function", [cftime_range, date_range]) -def test_cftime_or_date_range_inclusive_None(function) -> None: - if function == cftime_range and not has_cftime: +@pytest.mark.parametrize("use_cftime", [True, False]) +def test_cftime_or_date_range_inclusive_None(use_cftime: bool) -> None: + if use_cftime and not has_cftime: pytest.skip("requires cftime") - result_None = function("2000-01-01", "2000-01-04") - result_both = function("2000-01-01", "2000-01-04", inclusive="both") + result_None = date_range("2000-01-01", "2000-01-04", use_cftime=use_cftime) + result_both = date_range( + "2000-01-01", "2000-01-04", inclusive="both", use_cftime=use_cftime + ) np.testing.assert_equal(result_None.values, result_both.values) @@ -1694,11 +1703,12 @@ def test_cftime_range_no_freq(start, end, periods): when freq is not provided, but start, end and periods are. """ # Generate date ranges using cftime_range - cftimeindex = cftime_range(start=start, end=end, periods=periods) - result = cftimeindex.to_datetimeindex(time_unit="ns") - expected = pd.date_range(start=start, end=end, periods=periods) + with pytest.warns(DeprecationWarning): + cftimeindex = cftime_range(start=start, end=end, periods=periods) + result = cftimeindex.to_datetimeindex(time_unit="ns") + expected = pd.date_range(start=start, end=end, periods=periods) - np.testing.assert_array_equal(result, expected) + np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize( From 7fbff45440e26f5fc414e9e58417f19e845bf327 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 15:01:37 -0700 Subject: [PATCH 12/38] - Update cftime_range doctest to ignore deprecation warning. Issue #9886 --- xarray/coding/cftime_offsets.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index c4ccf8d01fd..ddd47989d9c 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1108,12 +1108,10 @@ def cftime_range( This function returns a ``CFTimeIndex``, populated with ``cftime.datetime`` objects associated with the specified calendar type, e.g. - >>> xr.cftime_range( - ... start="2000", periods=6, freq="2MS", calendar="noleap" - ... ) # +doctest: ELLIPSIS - Traceback (most recent call last): - ... - DeprecationWarning: cftime_range() is deprecated, please use date_range(use_cftime=True) instead. + >>> with warnings.catch_warnings(DeprecationWarning): + ... warnings.simplefilter("ignore") + ... xr.cftime_range(start="2000", periods=6, freq="2MS", calendar="noleap") + ... CFTimeIndex([2000-01-01 00:00:00, 2000-03-01 00:00:00, 2000-05-01 00:00:00, 2000-07-01 00:00:00, 2000-09-01 00:00:00, 2000-11-01 00:00:00], dtype='object', length=6, calendar='noleap', freq='2MS') From 7324f25dbe8117ade945f4e30635ffd07dd55542 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 15:23:19 -0700 Subject: [PATCH 13/38] - Fix test_cftime_or_date_range_invalid_inclusive_value for cases where type checking is enabled - change the example for cftime_range to use date_range(use_cftime) instead to get around deprecation warnings --- xarray/coding/cftime_offsets.py | 7 +++---- xarray/tests/test_cftime_offsets.py | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index ddd47989d9c..74f924f134d 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1108,10 +1108,9 @@ def cftime_range( This function returns a ``CFTimeIndex``, populated with ``cftime.datetime`` objects associated with the specified calendar type, e.g. - >>> with warnings.catch_warnings(DeprecationWarning): - ... warnings.simplefilter("ignore") - ... xr.cftime_range(start="2000", periods=6, freq="2MS", calendar="noleap") - ... + >>> xr.date_range( + ... start="2000", periods=6, freq="2MS", calendar="noleap", use_cftime=True + ... ) CFTimeIndex([2000-01-01 00:00:00, 2000-03-01 00:00:00, 2000-05-01 00:00:00, 2000-07-01 00:00:00, 2000-09-01 00:00:00, 2000-11-01 00:00:00], dtype='object', length=6, calendar='noleap', freq='2MS') diff --git a/xarray/tests/test_cftime_offsets.py b/xarray/tests/test_cftime_offsets.py index a51a4385322..f1378766567 100644 --- a/xarray/tests/test_cftime_offsets.py +++ b/xarray/tests/test_cftime_offsets.py @@ -2,7 +2,7 @@ import warnings from itertools import product -from typing import Literal +from typing import TYPE_CHECKING, Literal import numpy as np import pandas as pd @@ -1530,7 +1530,8 @@ def test_cftime_or_date_range_invalid_inclusive_value(use_cftime: bool) -> None: if use_cftime and not has_cftime: pytest.skip("requires cftime") - with pytest.raises(ValueError, match="nclusive"): + error_type = TypeError if TYPE_CHECKING else ValueError + with pytest.raises(error_type, match="nclusive"): date_range("2000", periods=3, inclusive="foo", use_cftime=use_cftime) From da4d9f5eed6d55d78d1a44ebde392cd7112123db Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 15:51:27 -0700 Subject: [PATCH 14/38] update weather-climate.rst to use date_range rather than deprecated cftime_range --- doc/saved_on_disk.h5 | Bin 0 -> 8488 bytes doc/user-guide/weather-climate.rst | 10 +++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 doc/saved_on_disk.h5 diff --git a/doc/saved_on_disk.h5 b/doc/saved_on_disk.h5 new file mode 100644 index 0000000000000000000000000000000000000000..cd58f9e43c2850a73ffe304e863aa50124641983 GIT binary patch literal 8488 zcmeHMYit`u5T5fnt`lD#l#tMpASa?#)RxqVOr=&$FOK6hp^ii2s-U*2G4UmJ<@kc_ zmL#C^5|4rqkRSYrgox-*gy07P)CvhIil~qX4@Dy4kq`ywi-bp!Dp3eKyF0FHB@&U^ zKh>>#o}Ibb+1YPrcJKC%wzsww7p*R`Ih{adL6*82A@Rr?hN*tk)!y1|)84Vwa+&Io zx2Q=zEwX`vDrIH(f1gzEM}`$gIsPkl6$vZK;Jm1Sqk{8xo8;AA(|ky(-BJa1CRP>5 z?~QoT+h1}-!c~BbYw8W|esQ^?@SbgoqM#`(L{$wvE@N#)^JXY0L5aE!dFjt%q6edZ zH-(*o0D0zE(lgD6sw$Lॲj7(3g*XuXUR4ks1W{foNS~%kdbdcl68WBYU+Ir+w za4;vCu0QAthv8;+17SuVTRS=fp>Ri6C=&Mjf&tK~P_9COXb|#s2Dm=z;x)*wf4-Y} zTHq7N!AA~}wG?GJ_`E9{y&~HZJ}a&#K8Vvb$7hI}R2hOLY~~o`G;^)-{&?)_Ib;2c zxiZ%eekSCde!QK{6Bz8v=hm1vs zu~e+%aI{*qt}^I(y!Yg!3r0P5hMf;<`_Oe2N?9VX9y*WeymrOq?xqXsuRSJZz-WaWmP}xFN~c!zOS2=K97B2V%WXcX-v43dDB5P+0|2ScJe&=%+pg#3XB4~b383Wh{1nu(^3%$3-P z#rusUIt5O=FZy*ax**fOB>Dv?TsSbA>(PhOl{%?_9yxfBUT=?RLgv=|`ju3 zaivVGJt)G0@wHg#-t)K}XNMw6Zc(5NpOvX}%(tb5hi?!OSk7WXZe2wsR*3SsCok;H zjf^q3@K6%a9ue7h?P66X3C~;XD?e6g%)$?V;~hIps$0}Tz(T-6z(U{#Mqs33=!Kj%5|Y#77rnX@MD#F0deok_S<<6}1LEc0ODumr&oZncw|DtS0?;FG z7D-Be$0YB{lRqGNEl(bmJdsN!RI{jsfQ5jCfQ5jCfQ5jCfQ5jCfQ5jCfQ5jCz>R}I zY1OueFW!A#|McMex!cx!uHX9c?A*wEC-v2XHN)QN8NDr1{d?#B86Ea+I{nH1xAf26 zo?dxsc1GXz$BE2qedBt`iOQEIPfzG4w;lZErzht13D;K(Q;A>o-`>}s8sB|P-@ftZ zFUDU!ukUj;$G29W(OY*Oo=VM4>KE@n`)By_gzlN!SoiAmDZQ-%zOQVV)Ze`9Jo3!5 zv-^f5(n^r=F8ph6j)OVD{l?imN30p#*{R*=i%Swk+5 GKKuvIu;}Rk literal 0 HcmV?d00001 diff --git a/doc/user-guide/weather-climate.rst b/doc/user-guide/weather-climate.rst index ac50c27d233..11c2188a50c 100644 --- a/doc/user-guide/weather-climate.rst +++ b/doc/user-guide/weather-climate.rst @@ -98,13 +98,15 @@ coordinate with dates from a no-leap calendar and a ] da = xr.DataArray(np.arange(24), coords=[dates], dims=["time"], name="foo") -Xarray also includes a :py:func:`~xarray.cftime_range` function, which enables +Xarray also includes a :py:func:`~xarray.date_range` function, which enables creating a :py:class:`~xarray.CFTimeIndex` with regularly-spaced dates. For instance, we can create the same dates and DataArray we created above using: .. ipython:: python - dates = xr.cftime_range(start="0001", periods=24, freq="MS", calendar="noleap") + dates = xr.date_range( + start="0001", periods=24, freq="MS", calendar="noleap", use_cftime=True + ) da = xr.DataArray(np.arange(24), coords=[dates], dims=["time"], name="foo") Mirroring pandas' method with the same name, :py:meth:`~xarray.infer_freq` allows one to @@ -138,7 +140,9 @@ use ``pandas`` when possible, i.e. when the calendar is ``standard``/``gregorian .. ipython:: python - dates = xr.cftime_range(start="2001", periods=24, freq="MS", calendar="noleap") + dates = xr.date_range( + start="2001", periods=24, freq="MS", calendar="noleap", use_cftime=True + ) da_nl = xr.DataArray(np.arange(24), coords=[dates], dims=["time"], name="foo") da_std = da.convert_calendar("standard", use_cftime=True) From d500e790f0ac6867431d90c20069595982749a1e Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 15:51:58 -0700 Subject: [PATCH 15/38] only check inclusive argument when not already type checked to avoid mypy error --- xarray/coding/cftime_offsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 74f924f134d..352052d452e 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1204,7 +1204,7 @@ def _cftime_range( list(_generate_date_range_with_freq(start, end, periods, freq)) ) - if inclusive not in get_args(InclusiveOptions): + if not TYPE_CHECKING and inclusive not in get_args(InclusiveOptions): raise ValueError( f"Argument `inclusive` must be either 'both', 'neither', " f"'left', or 'right'. Got {inclusive}." From 9b45bc8b2a404dfe943ef1939a6d29275568452a Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 20:37:47 -0700 Subject: [PATCH 16/38] skip invalid inclusion value if type checking is turned on issue #9886 --- xarray/tests/test_cftime_offsets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/xarray/tests/test_cftime_offsets.py b/xarray/tests/test_cftime_offsets.py index f1378766567..5b7a15f7ff1 100644 --- a/xarray/tests/test_cftime_offsets.py +++ b/xarray/tests/test_cftime_offsets.py @@ -1530,8 +1530,10 @@ def test_cftime_or_date_range_invalid_inclusive_value(use_cftime: bool) -> None: if use_cftime and not has_cftime: pytest.skip("requires cftime") - error_type = TypeError if TYPE_CHECKING else ValueError - with pytest.raises(error_type, match="nclusive"): + if TYPE_CHECKING: + pytest.skip("inclusive type checked internally") + + with pytest.raises(ValueError, match="nclusive"): date_range("2000", periods=3, inclusive="foo", use_cftime=use_cftime) From f68277e27cc9d4138e618e749f5bffa2d7bf9067 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 7 Feb 2025 20:46:16 -0700 Subject: [PATCH 17/38] remove unneeded ignore arg type comments issue #9886 --- xarray/tests/test_cftimeindex.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 159e26130eb..f37a243057b 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -1023,14 +1023,14 @@ def test_cftimeindex_shift(index, freq) -> None: def test_cftimeindex_shift_invalid_periods() -> None: index = xr.date_range("2000", periods=3, use_cftime=True) with pytest.raises(TypeError): - index.shift("a", "D") # type: ignore[arg-type] + index.shift("a", "D") @requires_cftime def test_cftimeindex_shift_invalid_freq() -> None: index = xr.date_range("2000", periods=3, use_cftime=True) with pytest.raises(TypeError): - index.shift(1, 1) # type: ignore[arg-type] + index.shift(1, 1) @requires_cftime From 2fa10f6de8cc276ee8fd03f3af8db4ef59ea71b3 Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Tue, 11 Feb 2025 16:39:56 -0700 Subject: [PATCH 18/38] s/Illegal/Invalid/ in error message for cftime_range per code review issue #9886 Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> --- xarray/coding/cftime_offsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 352052d452e..289a8b464b4 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1190,7 +1190,7 @@ def _cftime_range( # Adapted from pandas.core.indexes.datetimes._generate_range. if count_not_none(start, end, periods, freq) != 3: raise ValueError( - "Illegal combination of the arguments 'start', 'end', 'periods', " + "Invalid combination of the arguments 'start', 'end', 'periods', " "and 'freq' specified." ) From 0e5ee066107cf12cb6a20779e5b28b3eb9238353 Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Fri, 14 Feb 2025 10:14:21 -0700 Subject: [PATCH 19/38] Update doc/whats-new.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix typo in (s/true/True) in what's new for issue #9886 change. Co-authored-by: Kai Mühlbauer --- doc/whats-new.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 513d652ab4d..a09867c5f69 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -30,7 +30,7 @@ Breaking changes Deprecations ~~~~~~~~~~~~ -xarray.cftime_range deprecated in favor of xarray.data_range(use_cftime=true) +xarray.cftime_range deprecated in favor of xarray.data_range(use_cftime=True) Bug fixes ~~~~~~~~~ From 7409c1beced2d0e20e192db21876396a06a6b3ce Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Fri, 14 Feb 2025 10:19:20 -0700 Subject: [PATCH 20/38] remove accidentally included documenation artifact --- doc/saved_on_disk.h5 | Bin 8488 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 doc/saved_on_disk.h5 diff --git a/doc/saved_on_disk.h5 b/doc/saved_on_disk.h5 deleted file mode 100644 index cd58f9e43c2850a73ffe304e863aa50124641983..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8488 zcmeHMYit`u5T5fnt`lD#l#tMpASa?#)RxqVOr=&$FOK6hp^ii2s-U*2G4UmJ<@kc_ zmL#C^5|4rqkRSYrgox-*gy07P)CvhIil~qX4@Dy4kq`ywi-bp!Dp3eKyF0FHB@&U^ zKh>>#o}Ibb+1YPrcJKC%wzsww7p*R`Ih{adL6*82A@Rr?hN*tk)!y1|)84Vwa+&Io zx2Q=zEwX`vDrIH(f1gzEM}`$gIsPkl6$vZK;Jm1Sqk{8xo8;AA(|ky(-BJa1CRP>5 z?~QoT+h1}-!c~BbYw8W|esQ^?@SbgoqM#`(L{$wvE@N#)^JXY0L5aE!dFjt%q6edZ zH-(*o0D0zE(lgD6sw$Lॲj7(3g*XuXUR4ks1W{foNS~%kdbdcl68WBYU+Ir+w za4;vCu0QAthv8;+17SuVTRS=fp>Ri6C=&Mjf&tK~P_9COXb|#s2Dm=z;x)*wf4-Y} zTHq7N!AA~}wG?GJ_`E9{y&~HZJ}a&#K8Vvb$7hI}R2hOLY~~o`G;^)-{&?)_Ib;2c zxiZ%eekSCde!QK{6Bz8v=hm1vs zu~e+%aI{*qt}^I(y!Yg!3r0P5hMf;<`_Oe2N?9VX9y*WeymrOq?xqXsuRSJZz-WaWmP}xFN~c!zOS2=K97B2V%WXcX-v43dDB5P+0|2ScJe&=%+pg#3XB4~b383Wh{1nu(^3%$3-P z#rusUIt5O=FZy*ax**fOB>Dv?TsSbA>(PhOl{%?_9yxfBUT=?RLgv=|`ju3 zaivVGJt)G0@wHg#-t)K}XNMw6Zc(5NpOvX}%(tb5hi?!OSk7WXZe2wsR*3SsCok;H zjf^q3@K6%a9ue7h?P66X3C~;XD?e6g%)$?V;~hIps$0}Tz(T-6z(U{#Mqs33=!Kj%5|Y#77rnX@MD#F0deok_S<<6}1LEc0ODumr&oZncw|DtS0?;FG z7D-Be$0YB{lRqGNEl(bmJdsN!RI{jsfQ5jCfQ5jCfQ5jCfQ5jCfQ5jCfQ5jCz>R}I zY1OueFW!A#|McMex!cx!uHX9c?A*wEC-v2XHN)QN8NDr1{d?#B86Ea+I{nH1xAf26 zo?dxsc1GXz$BE2qedBt`iOQEIPfzG4w;lZErzht13D;K(Q;A>o-`>}s8sB|P-@ftZ zFUDU!ukUj;$G29W(OY*Oo=VM4>KE@n`)By_gzlN!SoiAmDZQ-%zOQVV)Ze`9Jo3!5 zv-^f5(n^r=F8ph6j)OVD{l?imN30p#*{R*=i%Swk+5 GKKuvIu;}Rk From 7765d919109d2d7604bb0779aa4596abb9f440e4 Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Tue, 18 Feb 2025 14:57:07 -0700 Subject: [PATCH 21/38] update to use data_range instead of cftime_range Update documentation to steer users to xarray.date_range rather than deprecated xarray.cftime_range. Co-authored-by: Spencer Clark --- doc/user-guide/weather-climate.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/user-guide/weather-climate.rst b/doc/user-guide/weather-climate.rst index 11c2188a50c..a4b3823ffe2 100644 --- a/doc/user-guide/weather-climate.rst +++ b/doc/user-guide/weather-climate.rst @@ -100,7 +100,10 @@ coordinate with dates from a no-leap calendar and a Xarray also includes a :py:func:`~xarray.date_range` function, which enables creating a :py:class:`~xarray.CFTimeIndex` with regularly-spaced dates. For -instance, we can create the same dates and DataArray we created above using: +instance, we can create the same dates and DataArray we created above using +(note that ``use_cftime=True`` is not mandatory to return a +:py:class:`~xarray.CFTimeIndex` for non-standard calendars, but can be nice +to use to be explicit): .. ipython:: python From eb87222823b2df8d171e3e4d7ba01b4a48a8be20 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:57:27 +0000 Subject: [PATCH 22/38] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/user-guide/weather-climate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user-guide/weather-climate.rst b/doc/user-guide/weather-climate.rst index a4b3823ffe2..d56811aa2ad 100644 --- a/doc/user-guide/weather-climate.rst +++ b/doc/user-guide/weather-climate.rst @@ -101,7 +101,7 @@ coordinate with dates from a no-leap calendar and a Xarray also includes a :py:func:`~xarray.date_range` function, which enables creating a :py:class:`~xarray.CFTimeIndex` with regularly-spaced dates. For instance, we can create the same dates and DataArray we created above using -(note that ``use_cftime=True`` is not mandatory to return a +(note that ``use_cftime=True`` is not mandatory to return a :py:class:`~xarray.CFTimeIndex` for non-standard calendars, but can be nice to use to be explicit): From 899f123b634a76fb019ea1cb33d56330f946eed5 Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Tue, 18 Feb 2025 15:00:15 -0700 Subject: [PATCH 23/38] remove "bug fix" for #9886 listed under deprecations instead Co-authored-by: Spencer Clark --- doc/whats-new.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 80a1e294abf..87ba2a1f69b 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -50,9 +50,6 @@ Bug fixes - Use mean of min/max years as offset in calculation of datetime64 mean (:issue:`10019`, :pull:`10035`). By `Kai Mühlbauer `_. -- Deprecate xr.cftime_range() in favor of xr.date_range(use_cftime=True) - (:issue:`9886`, :pull:`10024`). - By `Josh Kihm `_. - Fix DataArray().drop_attrs(deep=False) and add support for attrs to DataArray()._replace(). (:issue:`10027`, :pull:`10030`). By `Jan Haacker `_. From 12a5c655dad36f9e257383d52a4ccb795ef449ae Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Tue, 18 Feb 2025 15:01:07 -0700 Subject: [PATCH 24/38] fix documentation typo Co-authored-by: Spencer Clark --- xarray/coding/cftime_offsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 289a8b464b4..8e5d820303b 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -898,7 +898,7 @@ def _generate_date_range_with_freq(start, end, periods, freq): periods : int, or None Number of elements in the sequence freq: str - Step size bewtween cftime.datetime objects. Not None. + Step size between cftime.datetime objects. Not None. Returns ------- From 6e7444a65373eb2e89d5fb87b2779e705b2d0a00 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Tue, 18 Feb 2025 15:10:05 -0700 Subject: [PATCH 25/38] update backwards compatibility section example to use xarray.core.utils.emit_user_level_warning rather than warnings.warn --- doc/contributing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/contributing.rst b/doc/contributing.rst index b930b443dbd..2a91759744b 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -580,9 +580,9 @@ a user passes ``old_arg``, we would instead catch it: def func(new_arg, old_arg=None): if old_arg is not None: - from warnings import warn + from xarray.core.utils import emit_user_level_warning - warn( + emit_user_level_warning( "`old_arg` has been deprecated, and in the future will raise an error." "Please use `new_arg` from now on.", DeprecationWarning, From 3adf065b5ca77e92ba6ac86b36c7fd779a84cd35 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Tue, 18 Feb 2025 15:14:32 -0700 Subject: [PATCH 26/38] update cftime_range deprecation warning to use emit_user_level_warning (issue #9886)" --- xarray/coding/cftime_offsets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 8e5d820303b..1a92027111a 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1126,8 +1126,8 @@ def cftime_range( -------- pandas.date_range """ - warnings.warn( - "cftime_range() is deprecated, please use date_range(use_cftime=True) instead.", + emit_user_level_warning( + "cftime_range() is deprecated, please use xarray.date_range(use_cftime=True) instead.", DeprecationWarning, stacklevel=2, ) From 40222b809414e9eb839a77f6818064546d097134 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Tue, 18 Feb 2025 15:19:40 -0700 Subject: [PATCH 27/38] add "linear" to internal generate_date_range* function names per code review (issue #9886) --- xarray/coding/cftime_offsets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 1a92027111a..1f8100f5ce7 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -865,7 +865,7 @@ def _get_normalized_cfdate(date, calendar, normalize): return normalize_date(cf_date) if normalize else cf_date -def _generate_date_range(start, end, periods): +def _generate_linear_date_range(start, end, periods): """Generate an equally-spaced sequence of cftime.datetime objects between and including two dates (whose length equals the number of periods).""" if TYPE_CHECKING: @@ -882,7 +882,7 @@ def _generate_date_range(start, end, periods): ) -def _generate_date_range_with_freq(start, end, periods, freq): +def _generate_linear_date_range_with_freq(start, end, periods, freq): """Generate a regular range of cftime.datetime objects with a given frequency. @@ -1198,10 +1198,10 @@ def _cftime_range( end = _get_normalized_cfdate(end, calendar, normalize) if freq is None: - dates = _generate_date_range(start, end, periods) + dates = _generate_linear_date_range(start, end, periods) else: dates = np.array( - list(_generate_date_range_with_freq(start, end, periods, freq)) + list(_generate_linear_date_range_with_freq(start, end, periods, freq)) ) if not TYPE_CHECKING and inclusive not in get_args(InclusiveOptions): From 16ca397d8883e404b1aa5def73ef951c9670cf5f Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Tue, 18 Feb 2025 15:22:26 -0700 Subject: [PATCH 28/38] clean up cftime_range docum Co-authored-by: Spencer Clark --- xarray/coding/cftime_offsets.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 1f8100f5ce7..da7380fd19b 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -953,9 +953,10 @@ def cftime_range( inclusive: InclusiveOptions = "both", calendar="standard", ) -> CFTimeIndex: - """DEPRECATED: use date_range(use_cftime=True) instead. + """Return a fixed frequency CFTimeIndex. - Return a fixed frequency CFTimeIndex. + .. deprecated:: 2025.02.0 + Use :py:func:`~xarray.date_range` with ``use_cftime=True`` instead. Parameters ---------- From 00045842777f218f79b08aa78c4df27e4005677b Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Tue, 18 Feb 2025 15:53:49 -0700 Subject: [PATCH 29/38] replace deprecated cftime_range with date_range(use_cftime=True) for most unit tests (#9886) --- xarray/tests/test_cftime_offsets.py | 90 ++++++++++++++--------------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/xarray/tests/test_cftime_offsets.py b/xarray/tests/test_cftime_offsets.py index 5b7a15f7ff1..fe53a7d9d8c 100644 --- a/xarray/tests/test_cftime_offsets.py +++ b/xarray/tests/test_cftime_offsets.py @@ -1255,13 +1255,12 @@ def test_cftime_range( assert np.max(np.abs(deltas)) < 0.001 -def test_cftime_range_name(): - with pytest.warns(DeprecationWarning): - result = cftime_range(start="2000", periods=4, name="foo") - assert result.name == "foo" +def test_date_range_name(): + result = date_range(start="2000", periods=4, name="foo") + assert result.name == "foo" - result = cftime_range(start="2000", periods=4) - assert result.name is None + result = date_range(start="2000", periods=4) + assert result.name is None @pytest.mark.parametrize( @@ -1275,15 +1274,14 @@ def test_cftime_range_name(): ("2000", "2001", 5, "YE", None), ], ) -def test_invalid_cftime_range_inputs( +def test_invalid_date_range_cftime_inputs( start: str | None, end: str | None, periods: int | None, freq: str | None, inclusive: Literal["up", None], ) -> None: - with pytest.raises(ValueError), pytest.warns(DeprecationWarning): - cftime_range(start, end, periods, freq, inclusive=inclusive) # type: ignore[arg-type] + date_range(start, end, periods, freq, inclusive=inclusive, use_cftime=True) # type: ignore[arg-type] _CALENDAR_SPECIFIC_MONTH_END_TESTS = [ @@ -1307,11 +1305,14 @@ def test_calendar_specific_month_end( year = 2000 # Use a leap-year to highlight calendar differences date_type = get_date_type(calendar) expected = [date_type(year, *args) for args in expected_month_day] - with pytest.warns(DeprecationWarning): - result = cftime_range( - start="2000-02", end="2001", freq="2ME", calendar=calendar - ).values - np.testing.assert_equal(result, expected) + result = date_range( + start="2000-02", + end="2001", + freq="2ME", + calendar=calendar, + use_cftime=True, + ).values + np.testing.assert_equal(result, expected) @pytest.mark.parametrize( @@ -1325,14 +1326,10 @@ def test_calendar_specific_month_end_negative_freq( year = 2000 # Use a leap-year to highlight calendar differences date_type = get_date_type(calendar) expected = [date_type(year, *args) for args in expected_month_day[::-1]] - with pytest.warns(DeprecationWarning): - result = cftime_range( - start="2001", - end="2000", - freq="-2ME", - calendar=calendar, - ).values - np.testing.assert_equal(result, expected) + result = date_range( + start="2001", end="2000", freq="-2ME", calendar=calendar, use_cftime=True + ).values + np.testing.assert_equal(result, expected) @pytest.mark.parametrize( @@ -1355,37 +1352,35 @@ def test_calendar_specific_month_end_negative_freq( def test_calendar_year_length( calendar: str, start: str, end: str, expected_number_of_days: int ) -> None: - with pytest.warns(DeprecationWarning): - result = cftime_range(start, end, freq="D", inclusive="left", calendar=calendar) - assert len(result) == expected_number_of_days + result = date_range( + start, end, freq="D", inclusive="left", calendar=calendar, use_cftime=True + ) + assert len(result) == expected_number_of_days @pytest.mark.parametrize("freq", ["YE", "ME", "D"]) -def test_dayofweek_after_cftime_range(freq: str) -> None: - with pytest.warns(DeprecationWarning): - result = cftime_range("2000-02-01", periods=3, freq=freq).dayofweek - # TODO: remove once requiring pandas 2.2+ - freq = _new_to_legacy_freq(freq) - expected = pd.date_range("2000-02-01", periods=3, freq=freq).dayofweek - np.testing.assert_array_equal(result, expected) +def test_dayofweek_after_cftime(freq: str) -> None: + result = date_range("2000-02-01", periods=3, freq=freq, use_cftime=True).dayofweek + # TODO: remove once requiring pandas 2.2+ + freq = _new_to_legacy_freq(freq) + expected = pd.date_range("2000-02-01", periods=3, freq=freq).dayofweek + np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize("freq", ["YE", "ME", "D"]) -def test_dayofyear_after_cftime_range(freq: str) -> None: - with pytest.warns(DeprecationWarning): - result = cftime_range("2000-02-01", periods=3, freq=freq).dayofyear - # TODO: remove once requiring pandas 2.2+ - freq = _new_to_legacy_freq(freq) - expected = pd.date_range("2000-02-01", periods=3, freq=freq).dayofyear - np.testing.assert_array_equal(result, expected) +def test_dayofyear_after_cftime(freq: str) -> None: + result = date_range("2000-02-01", periods=3, freq=freq, use_cftime=True).dayofyear + # TODO: remove once requiring pandas 2.2+ + freq = _new_to_legacy_freq(freq) + expected = pd.date_range("2000-02-01", periods=3, freq=freq).dayofyear + np.testing.assert_array_equal(result, expected) def test_cftime_range_standard_calendar_refers_to_gregorian() -> None: from cftime import DatetimeGregorian - with pytest.warns(DeprecationWarning): - (result,) = cftime_range("2000", periods=1) - assert isinstance(result, DatetimeGregorian) + (result,) = date_range("2000", periods=1, use_cftime=True) + assert isinstance(result, DatetimeGregorian) @pytest.mark.parametrize( @@ -1702,16 +1697,15 @@ def test_cftime_range_same_as_pandas(start, end, freq) -> None: ) def test_cftime_range_no_freq(start, end, periods): """ - Test whether cftime_range produces the same result as Pandas + Test whether date_range produces the same result as Pandas when freq is not provided, but start, end and periods are. """ # Generate date ranges using cftime_range - with pytest.warns(DeprecationWarning): - cftimeindex = cftime_range(start=start, end=end, periods=periods) - result = cftimeindex.to_datetimeindex(time_unit="ns") - expected = pd.date_range(start=start, end=end, periods=periods) + cftimeindex = date_range(start=start, end=end, periods=periods, use_cftime=True) + result = cftimeindex.to_datetimeindex(time_unit="ns") + expected = pd.date_range(start=start, end=end, periods=periods) - np.testing.assert_array_equal(result, expected) + np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize( From fec6a99a1d0c4578265e70c730b77671b4235b54 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Tue, 18 Feb 2025 16:20:28 -0700 Subject: [PATCH 30/38] copy and update cftime_range() documentation notes to date_range() (#9886) --- xarray/coding/cftime_offsets.py | 146 +++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 2 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index da7380fd19b..2cd2ae7f400 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1128,7 +1128,7 @@ def cftime_range( pandas.date_range """ emit_user_level_warning( - "cftime_range() is deprecated, please use xarray.date_range(use_cftime=True) instead.", + "cftime_range() is deprecated, please use xarray.date_range(..., use_cftime=True) instead.", DeprecationWarning, stacklevel=2, ) @@ -1270,12 +1270,154 @@ def date_range( If True, always return a CFTimeIndex. If False, return a pd.DatetimeIndex if possible or raise a ValueError. If None (default), return a pd.DatetimeIndex if possible, - otherwise return a CFTimeIndex. Defaults to False if `tz` is not None. + otherwise return a CFTimeIndex. Overridden to False if `tz` is not None. Returns ------- CFTimeIndex or pd.DatetimeIndex + Notes + ----- + When use_cftime=True, this function is an analog of ``pandas.date_range`` + for use in generating sequences of ``cftime.datetime`` objects. It supports most of the + features of ``pandas.date_range`` (e.g. specifying how the index is + ``closed`` on either side, or whether or not to ``normalize`` the start and + end bounds); however, there are some notable exceptions: + + - You cannot specify a ``tz`` (time zone) argument. + - Start or end dates specified as partial-datetime strings must use the + `ISO-8601 format `_. + - It supports many, but not all, frequencies supported by + ``pandas.date_range``. For example it does not currently support any of + the business-related or semi-monthly frequencies. + - Compound sub-monthly frequencies are not supported, e.g. '1H1min', as + these can easily be written in terms of the finest common resolution, + e.g. '61min'. + + Valid simple frequency strings for use with ``cftime``-calendars include + any multiples of the following. + + +--------+--------------------------+ + | Alias | Description | + +========+==========================+ + | YE | Year-end frequency | + +--------+--------------------------+ + | YS | Year-start frequency | + +--------+--------------------------+ + | QE | Quarter-end frequency | + +--------+--------------------------+ + | QS | Quarter-start frequency | + +--------+--------------------------+ + | ME | Month-end frequency | + +--------+--------------------------+ + | MS | Month-start frequency | + +--------+--------------------------+ + | D | Day frequency | + +--------+--------------------------+ + | h | Hour frequency | + +--------+--------------------------+ + | min | Minute frequency | + +--------+--------------------------+ + | s | Second frequency | + +--------+--------------------------+ + | ms | Millisecond frequency | + +--------+--------------------------+ + | us | Microsecond frequency | + +--------+--------------------------+ + + Any multiples of the following anchored offsets are also supported. + + +------------+--------------------------------------------------------------------+ + | Alias | Description | + +============+====================================================================+ + | Y(E,S)-JAN | Annual frequency, anchored at the (end, beginning) of January | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-FEB | Annual frequency, anchored at the (end, beginning) of February | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-MAR | Annual frequency, anchored at the (end, beginning) of March | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-APR | Annual frequency, anchored at the (end, beginning) of April | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-MAY | Annual frequency, anchored at the (end, beginning) of May | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-JUN | Annual frequency, anchored at the (end, beginning) of June | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-JUL | Annual frequency, anchored at the (end, beginning) of July | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-AUG | Annual frequency, anchored at the (end, beginning) of August | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-SEP | Annual frequency, anchored at the (end, beginning) of September | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-OCT | Annual frequency, anchored at the (end, beginning) of October | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-NOV | Annual frequency, anchored at the (end, beginning) of November | + +------------+--------------------------------------------------------------------+ + | Y(E,S)-DEC | Annual frequency, anchored at the (end, beginning) of December | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-JAN | Quarter frequency, anchored at the (end, beginning) of January | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-FEB | Quarter frequency, anchored at the (end, beginning) of February | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-MAR | Quarter frequency, anchored at the (end, beginning) of March | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-APR | Quarter frequency, anchored at the (end, beginning) of April | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-MAY | Quarter frequency, anchored at the (end, beginning) of May | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-JUN | Quarter frequency, anchored at the (end, beginning) of June | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-JUL | Quarter frequency, anchored at the (end, beginning) of July | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-AUG | Quarter frequency, anchored at the (end, beginning) of August | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-SEP | Quarter frequency, anchored at the (end, beginning) of September | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-OCT | Quarter frequency, anchored at the (end, beginning) of October | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-NOV | Quarter frequency, anchored at the (end, beginning) of November | + +------------+--------------------------------------------------------------------+ + | Q(E,S)-DEC | Quarter frequency, anchored at the (end, beginning) of December | + +------------+--------------------------------------------------------------------+ + + Finally, the following calendar aliases are supported. + + +--------------------------------+---------------------------------------+----------------------------+ + | Alias | Date type | Available use_cftime=False | + +================================+=======================================+============================+ + | standard, gregorian | ``cftime.DatetimeGregorian`` | True | + +--------------------------------+---------------------------------------+----------------------------+ + | proleptic_gregorian | ``cftime.DatetimeProlepticGregorian`` | True | + +--------------------------------+---------------------------------------+----------------------------+ + | noleap, 365_day | ``cftime.DatetimeNoLeap`` | False | + +--------------------------------+---------------------------------------+----------------------------+ + | all_leap, 366_day | ``cftime.DatetimeAllLeap`` | False | + +--------------------------------+---------------------------------------+----------------------------+ + | 360_day | ``cftime.Datetime360Day`` | False | + +--------------------------------+---------------------------------------+----------------------------+ + | julian | ``cftime.DatetimeJulian`` | False | + +--------------------------------+---------------------------------------+----------------------------+ + + As in the standard pandas function, three of the ``start``, ``end``, + ``periods``, or ``freq`` arguments must be specified at a given time, with + the other set to ``None`` if use_cftime=False. If use_cftime=True, exactly + 3 of the 4 can be set or 2 out of ``start``, ``end``, and``periods`` can be + set and ``freq`` will default to 'D'. See the `pandas documentation + `_ + for more examples of the behavior of ``date_range`` with each of the + parameters. + + Examples + -------- + This function returns a ``CFTimeIndex``, populated with ``cftime.datetime`` + objects associated with the specified calendar type, e.g. + + >>> xr.date_range( + ... start="2000", periods=6, freq="2MS", calendar="noleap", use_cftime=True + ... ) + CFTimeIndex([2000-01-01 00:00:00, 2000-03-01 00:00:00, 2000-05-01 00:00:00, + 2000-07-01 00:00:00, 2000-09-01 00:00:00, 2000-11-01 00:00:00], + dtype='object', length=6, calendar='noleap', freq='2MS') + See also -------- pandas.date_range From d611fbc74c116514997870d5c36587ba2ef1f6a2 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Tue, 18 Feb 2025 16:27:48 -0700 Subject: [PATCH 31/38] remove erroneous "stacklevel" argument from emit_user_level_warning call (#9886) --- xarray/coding/cftime_offsets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 2cd2ae7f400..d11a4804229 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1130,7 +1130,6 @@ def cftime_range( emit_user_level_warning( "cftime_range() is deprecated, please use xarray.date_range(..., use_cftime=True) instead.", DeprecationWarning, - stacklevel=2, ) return date_range( From acfcaf7f7625174edf37c307d7c430473ea08e03 Mon Sep 17 00:00:00 2001 From: maddogghoek Date: Tue, 18 Feb 2025 23:11:29 -0700 Subject: [PATCH 32/38] fix test_invalid_date_range_cftime_inputs to catch expected exception (#9886) --- xarray/tests/test_cftime_offsets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xarray/tests/test_cftime_offsets.py b/xarray/tests/test_cftime_offsets.py index fe53a7d9d8c..abec4c62080 100644 --- a/xarray/tests/test_cftime_offsets.py +++ b/xarray/tests/test_cftime_offsets.py @@ -1281,7 +1281,8 @@ def test_invalid_date_range_cftime_inputs( freq: str | None, inclusive: Literal["up", None], ) -> None: - date_range(start, end, periods, freq, inclusive=inclusive, use_cftime=True) # type: ignore[arg-type] + with pytest.raises(ValueError): + date_range(start, end, periods, freq, inclusive=inclusive, use_cftime=True) # type: ignore[arg-type] _CALENDAR_SPECIFIC_MONTH_END_TESTS = [ From 8bf8160046424b892b0248e9fcde4e41758df6d6 Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Thu, 20 Feb 2025 23:40:09 -0700 Subject: [PATCH 33/38] improve invalid input error message for date_range() Co-authored-by: Spencer Clark --- xarray/coding/cftime_offsets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index d11a4804229..6056e293ba1 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1190,8 +1190,10 @@ def _cftime_range( # Adapted from pandas.core.indexes.datetimes._generate_range. if count_not_none(start, end, periods, freq) != 3: raise ValueError( - "Invalid combination of the arguments 'start', 'end', 'periods', " - "and 'freq' specified." + "Exactly three of 'start', 'end', 'periods', or 'freq' must be " + "specified to generate a date range. Note that 'freq' defaults to " + "'D' in the event that any of 'start', 'end', or 'periods' are " + "None." ) start = _get_normalized_cfdate(start, calendar, normalize) From 773a7b2172de5830a71b10da44bd100e0370615f Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Thu, 20 Feb 2025 23:40:52 -0700 Subject: [PATCH 34/38] reformat deprecation note (#9886) Co-authored-by: Spencer Clark --- doc/whats-new.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 87ba2a1f69b..e85088839cf 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -37,7 +37,9 @@ Breaking changes Deprecations ~~~~~~~~~~~~ -xarray.cftime_range deprecated in favor of xarray.data_range(use_cftime=True) +- Deprecate :py:func:`~xarray.cftime_range` in favor of :py:func:`~xarray.date_range` with ``use_cftime=True`` + (:issue:`9886`, :pull:`10024`). + By `Josh Kihm `_. Bug fixes ~~~~~~~~~ From 3971ce0303c63954f0a8327ceb04544ab67a790b Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Thu, 20 Feb 2025 23:42:04 -0700 Subject: [PATCH 35/38] Minor documentation improvement for date_range() (#9886) Co-authored-by: Spencer Clark --- xarray/coding/cftime_offsets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 6056e293ba1..532ea101211 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1279,7 +1279,8 @@ def date_range( Notes ----- - When use_cftime=True, this function is an analog of ``pandas.date_range`` + When ``use_cftime=True``, or a calendar other than "standard", "gregorian", + or "proleptic_gregorian" is provided, this function is an analog of ``pandas.date_range`` for use in generating sequences of ``cftime.datetime`` objects. It supports most of the features of ``pandas.date_range`` (e.g. specifying how the index is ``closed`` on either side, or whether or not to ``normalize`` the start and From 8e432151a6d39818a2b22a307e658901db2525ab Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Thu, 20 Feb 2025 23:43:06 -0700 Subject: [PATCH 36/38] Fix date_range() documentation be more explicit about the valid combinations of start, end, periods, and freq. Co-authored-by: Spencer Clark --- xarray/coding/cftime_offsets.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 532ea101211..f089469a56b 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1399,12 +1399,10 @@ def date_range( | julian | ``cftime.DatetimeJulian`` | False | +--------------------------------+---------------------------------------+----------------------------+ - As in the standard pandas function, three of the ``start``, ``end``, - ``periods``, or ``freq`` arguments must be specified at a given time, with - the other set to ``None`` if use_cftime=False. If use_cftime=True, exactly - 3 of the 4 can be set or 2 out of ``start``, ``end``, and``periods`` can be - set and ``freq`` will default to 'D'. See the `pandas documentation - `_ + As in the standard pandas function, exactly three of ``start``, ``end``, + ``periods``, or ``freq`` are required to generate a date range. Note that + ``freq`` defaults to ``"D"`` in the event that any of ``start``, ``end``, + or ``periods`` are set to ``None``. See the `pandas documentation `_ for more examples of the behavior of ``date_range`` with each of the parameters. From 5043c85d1a4017f27b3022d8d3d9d0c1462e7b32 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 06:43:25 +0000 Subject: [PATCH 37/38] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/coding/cftime_offsets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index f089469a56b..ef4ca3a63d6 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1400,8 +1400,8 @@ def date_range( +--------------------------------+---------------------------------------+----------------------------+ As in the standard pandas function, exactly three of ``start``, ``end``, - ``periods``, or ``freq`` are required to generate a date range. Note that - ``freq`` defaults to ``"D"`` in the event that any of ``start``, ``end``, + ``periods``, or ``freq`` are required to generate a date range. Note that + ``freq`` defaults to ``"D"`` in the event that any of ``start``, ``end``, or ``periods`` are set to ``None``. See the `pandas documentation `_ for more examples of the behavior of ``date_range`` with each of the parameters. From 4d75b40106e33c3a4247405e1116292fb401852d Mon Sep 17 00:00:00 2001 From: Josh Kihm Date: Fri, 21 Feb 2025 10:34:12 -0700 Subject: [PATCH 38/38] remove long url from date_range() documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #9886 Co-authored-by: Kai Mühlbauer --- xarray/coding/cftime_offsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index ef4ca3a63d6..22ba0aa3ce1 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -1402,7 +1402,7 @@ def date_range( As in the standard pandas function, exactly three of ``start``, ``end``, ``periods``, or ``freq`` are required to generate a date range. Note that ``freq`` defaults to ``"D"`` in the event that any of ``start``, ``end``, - or ``periods`` are set to ``None``. See the `pandas documentation `_ + or ``periods`` are set to ``None``. See :py:func:`pandas.date_range`. for more examples of the behavior of ``date_range`` with each of the parameters.