From d6a767c3fc5b1a53c1ef4f6d9a2ee2fc0cc47863 Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Thu, 8 Jun 2023 00:13:00 +0200 Subject: [PATCH 1/4] =?UTF-8?q?BUG:=20add=20=E2=80=99T=E2=80=99=20to=20Uni?= =?UTF-8?q?tChoices=20to=20fix=20incorrect=20unit=20validation=20in=20to?= =?UTF-8?q?=5Ftimedelta()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pandas/_libs/tslibs/timedeltas.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/_libs/tslibs/timedeltas.pyi b/pandas/_libs/tslibs/timedeltas.pyi index 0d5afbfe963f1..51d3878095532 100644 --- a/pandas/_libs/tslibs/timedeltas.pyi +++ b/pandas/_libs/tslibs/timedeltas.pyi @@ -38,6 +38,7 @@ UnitChoices = Literal[ "min", "minutes", "t", + "T", "s", "seconds", "sec", From ae2f84341ad5a5d0a7f19803fe819b5f51de8d9f Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Thu, 8 Jun 2023 20:31:07 +0200 Subject: [PATCH 2/4] deprecate units T, t, L, l and fix tests --- pandas/_libs/tslibs/dtypes.pyx | 4 ++++ pandas/_libs/tslibs/timedeltas.pyi | 1 + pandas/_libs/tslibs/timedeltas.pyx | 2 +- pandas/core/tools/timedeltas.py | 7 +++++-- pandas/tests/indexes/timedeltas/test_timedelta_range.py | 4 ++-- pandas/tests/resample/test_timedelta.py | 8 ++++---- pandas/tests/scalar/timedelta/test_timedelta.py | 4 ---- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pandas/_libs/tslibs/dtypes.pyx b/pandas/_libs/tslibs/dtypes.pyx index 2a49ea6760e72..8144555f740d7 100644 --- a/pandas/_libs/tslibs/dtypes.pyx +++ b/pandas/_libs/tslibs/dtypes.pyx @@ -379,10 +379,14 @@ cpdef NPY_DATETIMEUNIT abbrev_to_npy_unit(str abbrev): return NPY_DATETIMEUNIT.NPY_FR_h elif abbrev == "m": return NPY_DATETIMEUNIT.NPY_FR_m + elif abbrev == "T" or abbrev == "t": + return NPY_DATETIMEUNIT.NPY_FR_m elif abbrev == "s": return NPY_DATETIMEUNIT.NPY_FR_s elif abbrev == "ms": return NPY_DATETIMEUNIT.NPY_FR_ms + elif abbrev == "L" or abbrev == "l": + return NPY_DATETIMEUNIT.NPY_FR_ms elif abbrev == "us": return NPY_DATETIMEUNIT.NPY_FR_us elif abbrev == "ns": diff --git a/pandas/_libs/tslibs/timedeltas.pyi b/pandas/_libs/tslibs/timedeltas.pyi index 51d3878095532..a3a2f98fd8b08 100644 --- a/pandas/_libs/tslibs/timedeltas.pyi +++ b/pandas/_libs/tslibs/timedeltas.pyi @@ -49,6 +49,7 @@ UnitChoices = Literal[ "milli", "millis", "l", + "L", "us", "microseconds", "microsecond", diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 047b5e861da2c..4f84891fbf5f3 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -722,7 +722,7 @@ cpdef inline str parse_timedelta_unit(str unit): """ if unit is None: return "ns" - elif unit == "M": + elif unit in {"M", "t", "T", "l", "L"}: return unit try: return timedelta_abbrevs[unit.lower()] diff --git a/pandas/core/tools/timedeltas.py b/pandas/core/tools/timedeltas.py index ad366e58c2f06..bd4a8e6c18b8e 100644 --- a/pandas/core/tools/timedeltas.py +++ b/pandas/core/tools/timedeltas.py @@ -110,9 +110,9 @@ def to_timedelta( * 'W' * 'D' / 'days' / 'day' * 'hours' / 'hour' / 'hr' / 'h' - * 'm' / 'minute' / 'min' / 'minutes' / 'T' + * 'm' / 'minute' / 'min' / 'minutes' * 'S' / 'seconds' / 'sec' / 'second' - * 'ms' / 'milliseconds' / 'millisecond' / 'milli' / 'millis' / 'L' + * 'ms' / 'milliseconds' / 'millisecond' / 'milli' / 'millis' * 'us' / 'microseconds' / 'microsecond' / 'micro' / 'micros' / 'U' * 'ns' / 'nanoseconds' / 'nano' / 'nanos' / 'nanosecond' / 'N' @@ -181,6 +181,9 @@ def to_timedelta( "represent unambiguous timedelta values durations." ) + if unit in {"t", "T", "l", "L"}: + raise ValueError("Units 't', 'T', 'l' and 'L' are no longer supported.") + if arg is None: return arg elif isinstance(arg, ABCSeries): diff --git a/pandas/tests/indexes/timedeltas/test_timedelta_range.py b/pandas/tests/indexes/timedeltas/test_timedelta_range.py index 05fdddd7a4f4f..1abe566dc3bc2 100644 --- a/pandas/tests/indexes/timedeltas/test_timedelta_range.py +++ b/pandas/tests/indexes/timedeltas/test_timedelta_range.py @@ -38,8 +38,8 @@ def test_timedelta_range(self): result = timedelta_range("1 days, 00:00:02", periods=5, freq="2D") tm.assert_index_equal(result, expected) - expected = to_timedelta(np.arange(50), unit="T") * 30 - result = timedelta_range("0 days", freq="30T", periods=50) + expected = to_timedelta(np.arange(50), unit="min") * 30 + result = timedelta_range("0 days", freq="30min", periods=50) tm.assert_index_equal(result, expected) @pytest.mark.parametrize( diff --git a/pandas/tests/resample/test_timedelta.py b/pandas/tests/resample/test_timedelta.py index 79e024ef84c90..d87e6ca2f69b9 100644 --- a/pandas/tests/resample/test_timedelta.py +++ b/pandas/tests/resample/test_timedelta.py @@ -48,17 +48,17 @@ def test_resample_as_freq_with_subperiod(): def test_resample_with_timedeltas(): expected = DataFrame({"A": np.arange(1480)}) expected = expected.groupby(expected.index // 30).sum() - expected.index = timedelta_range("0 days", freq="30T", periods=50) + expected.index = timedelta_range("0 days", freq="30min", periods=50) df = DataFrame( - {"A": np.arange(1480)}, index=pd.to_timedelta(np.arange(1480), unit="T") + {"A": np.arange(1480)}, index=pd.to_timedelta(np.arange(1480), unit="min") ) - result = df.resample("30T").sum() + result = df.resample("30min").sum() tm.assert_frame_equal(result, expected) s = df["A"] - result = s.resample("30T").sum() + result = s.resample("30min").sum() tm.assert_series_equal(result, expected["A"]) diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index 4cb77b2f1d065..722a68a1dce71 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -492,11 +492,9 @@ def test_nat_converters(self): "minute", "min", "minutes", - "t", "Minute", "Min", "Minutes", - "T", ] ] + [ @@ -520,13 +518,11 @@ def test_nat_converters(self): "millisecond", "milli", "millis", - "l", "MS", "Milliseconds", "Millisecond", "Milli", "Millis", - "L", ] ] + [ From d8c7fd7a5029ee3b6b53e53b0bb29c498625060b Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Sun, 11 Jun 2023 01:26:03 +0200 Subject: [PATCH 3/4] raise FutureWarning instead of ValueError --- doc/source/whatsnew/v2.1.0.rst | 1 + pandas/_libs/tslibs/dtypes.pyx | 4 ---- pandas/_libs/tslibs/timedeltas.pyi | 4 ++-- pandas/_libs/tslibs/timedeltas.pyx | 4 +++- pandas/core/tools/timedeltas.py | 17 +++++++++++++---- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index b9ad494172bdf..675bfefdd03a4 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -279,6 +279,7 @@ Deprecations - Deprecated constructing :class:`SparseArray` from scalar data, pass a sequence instead (:issue:`53039`) - Deprecated option "mode.use_inf_as_na", convert inf entries to ``NaN`` before instead (:issue:`51684`) - Deprecated positional indexing on :class:`Series` with :meth:`Series.__getitem__` and :meth:`Series.__setitem__`, in a future version ``ser[item]`` will *always* interpret ``item`` as a label, not a position (:issue:`50617`) +- Deprecated strings ``T``, ``t``, ``L`` and ``l`` denoting units in :func:`to_timedelta` (:issue:`52536`) .. --------------------------------------------------------------------------- .. _whatsnew_210.performance: diff --git a/pandas/_libs/tslibs/dtypes.pyx b/pandas/_libs/tslibs/dtypes.pyx index 8144555f740d7..2a49ea6760e72 100644 --- a/pandas/_libs/tslibs/dtypes.pyx +++ b/pandas/_libs/tslibs/dtypes.pyx @@ -379,14 +379,10 @@ cpdef NPY_DATETIMEUNIT abbrev_to_npy_unit(str abbrev): return NPY_DATETIMEUNIT.NPY_FR_h elif abbrev == "m": return NPY_DATETIMEUNIT.NPY_FR_m - elif abbrev == "T" or abbrev == "t": - return NPY_DATETIMEUNIT.NPY_FR_m elif abbrev == "s": return NPY_DATETIMEUNIT.NPY_FR_s elif abbrev == "ms": return NPY_DATETIMEUNIT.NPY_FR_ms - elif abbrev == "L" or abbrev == "l": - return NPY_DATETIMEUNIT.NPY_FR_ms elif abbrev == "us": return NPY_DATETIMEUNIT.NPY_FR_us elif abbrev == "ns": diff --git a/pandas/_libs/tslibs/timedeltas.pyi b/pandas/_libs/tslibs/timedeltas.pyi index a3a2f98fd8b08..448aed0eebe4c 100644 --- a/pandas/_libs/tslibs/timedeltas.pyi +++ b/pandas/_libs/tslibs/timedeltas.pyi @@ -37,8 +37,8 @@ UnitChoices = Literal[ "minute", "min", "minutes", - "t", "T", + "t", "s", "seconds", "sec", @@ -48,8 +48,8 @@ UnitChoices = Literal[ "millisecond", "milli", "millis", - "l", "L", + "l", "us", "microseconds", "microsecond", diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 4f84891fbf5f3..421a0dfda8e44 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -123,6 +123,7 @@ cdef dict timedelta_abbrevs = { "minute": "m", "min": "m", "minutes": "m", + "T": "m", "t": "m", "s": "s", "seconds": "s", @@ -133,6 +134,7 @@ cdef dict timedelta_abbrevs = { "millisecond": "ms", "milli": "ms", "millis": "ms", + "L": "ms", "l": "ms", "us": "us", "microseconds": "us", @@ -722,7 +724,7 @@ cpdef inline str parse_timedelta_unit(str unit): """ if unit is None: return "ns" - elif unit in {"M", "t", "T", "l", "L"}: + elif unit == "M": return unit try: return timedelta_abbrevs[unit.lower()] diff --git a/pandas/core/tools/timedeltas.py b/pandas/core/tools/timedeltas.py index bd4a8e6c18b8e..58bb0099459fe 100644 --- a/pandas/core/tools/timedeltas.py +++ b/pandas/core/tools/timedeltas.py @@ -7,6 +7,7 @@ TYPE_CHECKING, overload, ) +import warnings import numpy as np @@ -19,6 +20,7 @@ Timedelta, parse_timedelta_unit, ) +from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.common import is_list_like from pandas.core.dtypes.generic import ( @@ -110,14 +112,17 @@ def to_timedelta( * 'W' * 'D' / 'days' / 'day' * 'hours' / 'hour' / 'hr' / 'h' - * 'm' / 'minute' / 'min' / 'minutes' + * 'm' / 'minute' / 'min' / 'minutes' / 'T' * 'S' / 'seconds' / 'sec' / 'second' - * 'ms' / 'milliseconds' / 'millisecond' / 'milli' / 'millis' + * 'ms' / 'milliseconds' / 'millisecond' / 'milli' / 'millis' / 'L' * 'us' / 'microseconds' / 'microsecond' / 'micro' / 'micros' / 'U' * 'ns' / 'nanoseconds' / 'nano' / 'nanos' / 'nanosecond' / 'N' Must not be specified when `arg` context strings and ``errors="raise"``. + .. deprecated:: 2.1.0 + Units 'T' and 'L' are deprecated and will be removed in a future version. + errors : {'ignore', 'raise', 'coerce'}, default 'raise' - If 'raise', then invalid parsing will raise an exception. - If 'coerce', then invalid parsing will be set as NaT. @@ -181,8 +186,12 @@ def to_timedelta( "represent unambiguous timedelta values durations." ) - if unit in {"t", "T", "l", "L"}: - raise ValueError("Units 't', 'T', 'l' and 'L' are no longer supported.") + if unit in {"T", "t", "L", "l"}: + warnings.warn( + f"Unit {unit} is deprecated and will be removed in a future version.", + FutureWarning, + stacklevel=find_stack_level(), + ) if arg is None: return arg From 9042122a76d21559294add91f44a22ae05122b71 Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Mon, 19 Jun 2023 10:08:49 +0200 Subject: [PATCH 4/4] correct the to_timedelta definition and add a test for the warning --- pandas/_libs/tslibs/timedeltas.pyx | 6 ------ pandas/core/tools/timedeltas.py | 14 +++++++------- .../timedeltas/test_timedelta_range.py | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index ab8d46cc7e7ca..047b5e861da2c 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1745,12 +1745,6 @@ class Timedelta(_Timedelta): "Units 'M', 'Y', and 'y' are no longer supported, as they do not " "represent unambiguous timedelta values durations." ) - if unit in {"T", "t", "L", "l"}: - warnings.warn( - "Units 'T' and'L' are deprecated and will be removed in a future version.", - FutureWarning, - stacklevel=2, - ) # GH 30543 if pd.Timedelta already passed, return it # check that only value is passed diff --git a/pandas/core/tools/timedeltas.py b/pandas/core/tools/timedeltas.py index ce9c21965ae89..95946f0a159fd 100644 --- a/pandas/core/tools/timedeltas.py +++ b/pandas/core/tools/timedeltas.py @@ -174,6 +174,13 @@ def to_timedelta( TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'], dtype='timedelta64[ns]', freq=None) """ + if unit in {"T", "t", "L", "l"}: + warnings.warn( + f"Unit '{unit}' is deprecated and will be removed in a future version.", + FutureWarning, + stacklevel=find_stack_level(), + ) + if unit is not None: unit = parse_timedelta_unit(unit) @@ -186,13 +193,6 @@ def to_timedelta( "represent unambiguous timedelta values durations." ) - if unit in {"T", "t", "L", "l"}: - warnings.warn( - "Units 'T' and'L' are deprecated and will be removed in a future version.", - FutureWarning, - stacklevel=find_stack_level(), - ) - if arg is None: return arg elif isinstance(arg, ABCSeries): diff --git a/pandas/tests/indexes/timedeltas/test_timedelta_range.py b/pandas/tests/indexes/timedeltas/test_timedelta_range.py index 1abe566dc3bc2..85ca88d60a6ba 100644 --- a/pandas/tests/indexes/timedeltas/test_timedelta_range.py +++ b/pandas/tests/indexes/timedeltas/test_timedelta_range.py @@ -42,6 +42,25 @@ def test_timedelta_range(self): result = timedelta_range("0 days", freq="30min", periods=50) tm.assert_index_equal(result, expected) + @pytest.mark.parametrize( + "depr_unit, unit", + [ + ("T", "minute"), + ("t", "minute"), + ("L", "millisecond"), + ("l", "millisecond"), + ], + ) + def test_timedelta_units_T_L_deprecated(self, depr_unit, unit): + depr_msg = ( + f"Unit '{depr_unit}' is deprecated." + ) + + expected = to_timedelta(np.arange(5), unit=unit) + with tm.assert_produces_warning(FutureWarning, match=depr_msg): + result = to_timedelta(np.arange(5), unit=depr_unit) + tm.assert_index_equal(result, expected) + @pytest.mark.parametrize( "periods, freq", [(3, "2D"), (5, "D"), (6, "19H12T"), (7, "16H"), (9, "12H")] )