Skip to content

Commit fb748be

Browse files
spencerkclarkdcheriankeewis
authored
Add inclusive argument to cftime_range and date_range and deprecate closed argument (#7373)
* Add inclusive argument to cftime_range and date_range; deprecate closed * Add documentation of deprecation * Update xarray/coding/cftime_offsets.py Co-authored-by: Justus Magin <[email protected]> * [skip-ci] [test-upstream] Update xarray/coding/cftime_offsets.py * [test-upstream] Use parameter instead of argument when describing function signature * [test-upstream] Add references to pandas PRs for _NoDefault * [test-upstream] Move _NoDefault to pdcompat; fix doc build * Apply suggestions from code review Co-authored-by: Spencer Clark <[email protected]> * Update xarray/coding/cftime_offsets.py Co-authored-by: Spencer Clark <[email protected]> --------- Co-authored-by: Deepak Cherian <[email protected]> Co-authored-by: Justus Magin <[email protected]>
1 parent f46cd70 commit fb748be

File tree

6 files changed

+203
-32
lines changed

6 files changed

+203
-32
lines changed

doc/whats-new.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ Breaking changes
4545

4646
Deprecations
4747
~~~~~~~~~~~~
48-
48+
- Following pandas, the `closed` parameters of :py:func:`cftime_range` and
49+
:py:func:`date_range` are deprecated in favor of the `inclusive` parameters,
50+
and will be removed in a future version of xarray (:issue:`6985`:,
51+
:pull:`7373`). By `Spencer Clark <https://github.com/spencerkclark>`_.
4952

5053
Bug fixes
5154
~~~~~~~~~

xarray/coding/cftime_offsets.py

+86-14
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
import re
4545
from datetime import datetime, timedelta
4646
from functools import partial
47-
from typing import ClassVar
47+
from typing import TYPE_CHECKING, ClassVar
4848

4949
import numpy as np
5050
import pandas as pd
@@ -57,14 +57,19 @@
5757
format_cftime_datetime,
5858
)
5959
from xarray.core.common import _contains_datetime_like_objects, is_np_datetime_like
60-
from xarray.core.pdcompat import count_not_none
60+
from xarray.core.pdcompat import NoDefault, count_not_none, no_default
61+
from xarray.core.utils import emit_user_level_warning
6162

6263
try:
6364
import cftime
6465
except ImportError:
6566
cftime = None
6667

6768

69+
if TYPE_CHECKING:
70+
from xarray.core.types import InclusiveOptions, SideOptions
71+
72+
6873
def get_date_type(calendar, use_cftime=True):
6974
"""Return the cftime date type for a given calendar name."""
7075
if cftime is None:
@@ -849,14 +854,49 @@ def _generate_range(start, end, periods, offset):
849854
current = next_date
850855

851856

857+
def _translate_closed_to_inclusive(closed):
858+
"""Follows code added in pandas #43504."""
859+
emit_user_level_warning(
860+
"Following pandas, the `closed` parameter is deprecated in "
861+
"favor of the `inclusive` parameter, and will be removed in "
862+
"a future version of xarray.",
863+
FutureWarning,
864+
)
865+
if closed is None:
866+
inclusive = "both"
867+
elif closed in ("left", "right"):
868+
inclusive = closed
869+
else:
870+
raise ValueError(
871+
f"Argument `closed` must be either 'left', 'right', or None. "
872+
f"Got {closed!r}."
873+
)
874+
return inclusive
875+
876+
877+
def _infer_inclusive(closed, inclusive):
878+
"""Follows code added in pandas #43504."""
879+
if closed is not no_default and inclusive is not None:
880+
raise ValueError(
881+
"Following pandas, deprecated argument `closed` cannot be "
882+
"passed if argument `inclusive` is not None."
883+
)
884+
if closed is not no_default:
885+
inclusive = _translate_closed_to_inclusive(closed)
886+
elif inclusive is None:
887+
inclusive = "both"
888+
return inclusive
889+
890+
852891
def cftime_range(
853892
start=None,
854893
end=None,
855894
periods=None,
856895
freq="D",
857896
normalize=False,
858897
name=None,
859-
closed=None,
898+
closed: NoDefault | SideOptions = no_default,
899+
inclusive: None | InclusiveOptions = None,
860900
calendar="standard",
861901
):
862902
"""Return a fixed frequency CFTimeIndex.
@@ -875,9 +915,20 @@ def cftime_range(
875915
Normalize start/end dates to midnight before generating date range.
876916
name : str, default: None
877917
Name of the resulting index
878-
closed : {"left", "right"} or None, default: None
918+
closed : {None, "left", "right"}, default: "NO_DEFAULT"
879919
Make the interval closed with respect to the given frequency to the
880920
"left", "right", or both sides (None).
921+
922+
.. deprecated:: 2023.02.0
923+
Following pandas, the ``closed`` parameter is deprecated in favor
924+
of the ``inclusive`` parameter, and will be removed in a future
925+
version of xarray.
926+
927+
inclusive : {None, "both", "neither", "left", "right"}, default None
928+
Include boundaries; whether to set each bound as closed or open.
929+
930+
.. versionadded:: 2023.02.0
931+
881932
calendar : str, default: "standard"
882933
Calendar type for the datetimes.
883934
@@ -1047,18 +1098,25 @@ def cftime_range(
10471098
offset = to_offset(freq)
10481099
dates = np.array(list(_generate_range(start, end, periods, offset)))
10491100

1050-
left_closed = False
1051-
right_closed = False
1101+
inclusive = _infer_inclusive(closed, inclusive)
10521102

1053-
if closed is None:
1103+
if inclusive == "neither":
1104+
left_closed = False
1105+
right_closed = False
1106+
elif inclusive == "left":
10541107
left_closed = True
1108+
right_closed = False
1109+
elif inclusive == "right":
1110+
left_closed = False
10551111
right_closed = True
1056-
elif closed == "left":
1112+
elif inclusive == "both":
10571113
left_closed = True
1058-
elif closed == "right":
10591114
right_closed = True
10601115
else:
1061-
raise ValueError("Closed must be either 'left', 'right' or None")
1116+
raise ValueError(
1117+
f"Argument `inclusive` must be either 'both', 'neither', "
1118+
f"'left', 'right', or None. Got {inclusive}."
1119+
)
10621120

10631121
if not left_closed and len(dates) and start is not None and dates[0] == start:
10641122
dates = dates[1:]
@@ -1076,7 +1134,8 @@ def date_range(
10761134
tz=None,
10771135
normalize=False,
10781136
name=None,
1079-
closed=None,
1137+
closed: NoDefault | SideOptions = no_default,
1138+
inclusive: None | InclusiveOptions = None,
10801139
calendar="standard",
10811140
use_cftime=None,
10821141
):
@@ -1103,9 +1162,20 @@ def date_range(
11031162
Normalize start/end dates to midnight before generating date range.
11041163
name : str, default: None
11051164
Name of the resulting index
1106-
closed : {"left", "right"} or None, default: None
1165+
closed : {None, "left", "right"}, default: "NO_DEFAULT"
11071166
Make the interval closed with respect to the given frequency to the
11081167
"left", "right", or both sides (None).
1168+
1169+
.. deprecated:: 2023.02.0
1170+
Following pandas, the `closed` parameter is deprecated in favor
1171+
of the `inclusive` parameter, and will be removed in a future
1172+
version of xarray.
1173+
1174+
inclusive : {None, "both", "neither", "left", "right"}, default: None
1175+
Include boundaries; whether to set each bound as closed or open.
1176+
1177+
.. versionadded:: 2023.02.0
1178+
11091179
calendar : str, default: "standard"
11101180
Calendar type for the datetimes.
11111181
use_cftime : boolean, optional
@@ -1129,6 +1199,8 @@ def date_range(
11291199
if tz is not None:
11301200
use_cftime = False
11311201

1202+
inclusive = _infer_inclusive(closed, inclusive)
1203+
11321204
if _is_standard_calendar(calendar) and use_cftime is not True:
11331205
try:
11341206
return pd.date_range(
@@ -1139,7 +1211,7 @@ def date_range(
11391211
tz=tz,
11401212
normalize=normalize,
11411213
name=name,
1142-
closed=closed,
1214+
inclusive=inclusive,
11431215
)
11441216
except pd.errors.OutOfBoundsDatetime as err:
11451217
if use_cftime is False:
@@ -1158,7 +1230,7 @@ def date_range(
11581230
freq=freq,
11591231
normalize=normalize,
11601232
name=name,
1161-
closed=closed,
1233+
inclusive=inclusive,
11621234
calendar=calendar,
11631235
)
11641236

xarray/core/pdcompat.py

+26
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,36 @@
3535
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3636
from __future__ import annotations
3737

38+
from enum import Enum
39+
from typing import Literal
40+
3841

3942
def count_not_none(*args) -> int:
4043
"""Compute the number of non-None arguments.
4144
4245
Copied from pandas.core.common.count_not_none (not part of the public API)
4346
"""
4447
return sum(arg is not None for arg in args)
48+
49+
50+
class _NoDefault(Enum):
51+
"""Used by pandas to specify a default value for a deprecated argument.
52+
Copied from pandas._libs.lib._NoDefault.
53+
54+
See also:
55+
- pandas-dev/pandas#30788
56+
- pandas-dev/pandas#40684
57+
- pandas-dev/pandas#40715
58+
- pandas-dev/pandas#47045
59+
"""
60+
61+
no_default = "NO_DEFAULT"
62+
63+
def __repr__(self) -> str:
64+
return "<no_default>"
65+
66+
67+
no_default = (
68+
_NoDefault.no_default
69+
) # Sentinel indicating the default value following pandas
70+
NoDefault = Literal[_NoDefault.no_default] # For typing following pandas

xarray/core/types.py

+1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ def dtype(self) -> np.dtype:
168168

169169
CoarsenBoundaryOptions = Literal["exact", "trim", "pad"]
170170
SideOptions = Literal["left", "right"]
171+
InclusiveOptions = Literal["both", "neither", "left", "right"]
171172

172173
ScaleOptions = Literal["linear", "symlog", "log", "logit", None]
173174
HueStyleOptions = Literal["continuous", "discrete", None]

0 commit comments

Comments
 (0)