From 8c2b5fb097741bef61d0e36e3d08dd6e3d80b3fb Mon Sep 17 00:00:00 2001 From: jgao8 <208881996+jgao8@users.noreply.github.com> Date: Wed, 1 Oct 2025 18:00:59 +0000 Subject: [PATCH 1/7] improve error message when specifying a previously-deprecated frequency alias --- pandas/_libs/tslibs/offsets.pyx | 14 ++++++++++++++ pandas/tests/tseries/offsets/test_offsets.py | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index a0d85fc44eb96..d54a1833bba38 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -5276,6 +5276,18 @@ def _get_offset(name: str) -> BaseOffset: return _offset_map[name] +deprec_to_valid_alias = { + "H": "h", + "BH": "bh", + "CBH": "cbh", + "T": "min", + "S": "s", + "L": "ms", + "U": "us", + "N": "ns", +} + + cpdef to_offset(freq, bint is_period=False): """ Return DateOffset object from string or datetime.timedelta object. @@ -5389,6 +5401,8 @@ cpdef to_offset(freq, bint is_period=False): # If n==0, then stride_sign is already incorporated # into the offset offset *= stride_sign + elif name in deprec_to_valid_alias: + raise ValueError(f"Did you mean '{deprec_to_valid_alias[name]}'?") else: stride = int(stride) offset = _get_offset(name) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 26b182fb4e9b1..c5f3788506117 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -1157,6 +1157,11 @@ def test_offset_multiplication( tm.assert_series_equal(resultarray, expectedarray) +def test_offset_deprecated_error(): + with pytest.raises(ValueError, match=r"Did you mean 'h'?"): + date_range("2012-01-01", periods=3, freq="H") + + def test_dateoffset_operations_on_dataframes(performance_warning): # GH 47953 df = DataFrame({"T": [Timestamp("2019-04-30")], "D": [DateOffset(months=1)]}) From 920945dfec3c98df63c0aa89578d26687d3b0c84 Mon Sep 17 00:00:00 2001 From: jgao8 <208881996+jgao8@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:03:46 -0400 Subject: [PATCH 2/7] update location of checking deprecated freq --- pandas/_libs/tslibs/offsets.pyx | 7 +++++-- pandas/tests/tseries/offsets/test_offsets.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index d54a1833bba38..0a0defb6d5a53 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -5371,6 +5371,11 @@ cpdef to_offset(freq, bint is_period=False): tups = zip(split[0::4], split[1::4], split[2::4]) for n, (sep, stride, name) in enumerate(tups): + if name in deprec_to_valid_alias: + raise ValueError(INVALID_FREQ_ERR_MSG.format( + f"{name}. Did you mean {deprec_to_valid_alias[name]}?") + ) + name = _warn_about_deprecated_aliases(name, is_period) _validate_to_offset_alias(name, is_period) if is_period: @@ -5401,8 +5406,6 @@ cpdef to_offset(freq, bint is_period=False): # If n==0, then stride_sign is already incorporated # into the offset offset *= stride_sign - elif name in deprec_to_valid_alias: - raise ValueError(f"Did you mean '{deprec_to_valid_alias[name]}'?") else: stride = int(stride) offset = _get_offset(name) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index c5f3788506117..1710cd8fff18f 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -1158,7 +1158,7 @@ def test_offset_multiplication( def test_offset_deprecated_error(): - with pytest.raises(ValueError, match=r"Did you mean 'h'?"): + with pytest.raises(ValueError, match=r"Did you mean h?"): date_range("2012-01-01", periods=3, freq="H") From 326909b6528b2897da215d1604f0c83f0598e7a0 Mon Sep 17 00:00:00 2001 From: jgao8 <208881996+jgao8@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:24:19 -0400 Subject: [PATCH 3/7] address PR comments --- pandas/_libs/tslibs/offsets.pyx | 54 ++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 0a0defb6d5a53..9a5bf135f4603 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -5188,6 +5188,27 @@ INVALID_FREQ_ERR_MSG = "Invalid frequency: {0}" _offset_map = {} +deprec_to_valid_alias = { + "H": "h", + "BH": "bh", + "CBH": "cbh", + "T": "min", + "S": "s", + "L": "ms", + "U": "us", + "N": "ns", +} + + +def raise_invalid_freq(freq: str, extra_message: str | None = None) -> None: + msg = f"Invalid frequency: {freq}." + if extra_message is not None: + msg += " " + extra_message + if freq in deprec_to_valid_alias: + msg += " " + "Did you mean {deprec_to_valid_alias[name]}?" + raise ValueError(msg) + + def _warn_about_deprecated_aliases(name: str, is_period: bool) -> str: if name in _lite_rule_alias: return name @@ -5236,7 +5257,7 @@ def _validate_to_offset_alias(alias: str, is_period: bool) -> None: if (alias.upper() != alias and alias.lower() not in {"s", "ms", "us", "ns"} and alias.upper().split("-")[0].endswith(("S", "E"))): - raise ValueError(INVALID_FREQ_ERR_MSG.format(alias)) + raise ValueError(raise_invalid_freq(freq=alias)) if ( is_period and alias in c_OFFSET_TO_PERIOD_FREQSTR and @@ -5267,8 +5288,9 @@ def _get_offset(name: str) -> BaseOffset: offset = klass._from_name(*split[1:]) except (ValueError, TypeError, KeyError) as err: # bad prefix or suffix - raise ValueError(INVALID_FREQ_ERR_MSG.format( - f"{name}, failed to parse with error message: {repr(err)}") + raise_invalid_freq( + freq=name, + extra_message=f"Failed to parse with error message: {repr(err)}." ) # cache _offset_map[name] = offset @@ -5276,18 +5298,6 @@ def _get_offset(name: str) -> BaseOffset: return _offset_map[name] -deprec_to_valid_alias = { - "H": "h", - "BH": "bh", - "CBH": "cbh", - "T": "min", - "S": "s", - "L": "ms", - "U": "us", - "N": "ns", -} - - cpdef to_offset(freq, bint is_period=False): """ Return DateOffset object from string or datetime.timedelta object. @@ -5371,11 +5381,6 @@ cpdef to_offset(freq, bint is_period=False): tups = zip(split[0::4], split[1::4], split[2::4]) for n, (sep, stride, name) in enumerate(tups): - if name in deprec_to_valid_alias: - raise ValueError(INVALID_FREQ_ERR_MSG.format( - f"{name}. Did you mean {deprec_to_valid_alias[name]}?") - ) - name = _warn_about_deprecated_aliases(name, is_period) _validate_to_offset_alias(name, is_period) if is_period: @@ -5416,9 +5421,10 @@ cpdef to_offset(freq, bint is_period=False): else: result = result + offset except (ValueError, TypeError) as err: - raise ValueError(INVALID_FREQ_ERR_MSG.format( - f"{freq}, failed to parse with error message: {repr(err)}") - ) from err + raise_invalid_freq( + freq=freq, + extra_message=f"Failed to parse with error message: {repr(err)}" + ) # TODO(3.0?) once deprecation of "d" is enforced, the check for it here # can be removed @@ -5434,7 +5440,7 @@ cpdef to_offset(freq, bint is_period=False): result = None if result is None: - raise ValueError(INVALID_FREQ_ERR_MSG.format(freq)) + raise_invalid_freq(freq=freq) try: has_period_dtype_code = hasattr(result, "_period_dtype_code") From 86a89a9a00eb2b976aea394465bd3d181aaaadca Mon Sep 17 00:00:00 2001 From: jgao8 <208881996+jgao8@users.noreply.github.com> Date: Wed, 1 Oct 2025 18:00:59 +0000 Subject: [PATCH 4/7] improve error message when specifying a previously-deprecated frequency alias --- pandas/_libs/tslibs/offsets.pyx | 14 ++++++++++++++ pandas/tests/tseries/offsets/test_offsets.py | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index dfc1fd0fe5630..7e4f4fb6c0170 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -5276,6 +5276,18 @@ def _get_offset(name: str) -> BaseOffset: return _offset_map[name] +deprec_to_valid_alias = { + "H": "h", + "BH": "bh", + "CBH": "cbh", + "T": "min", + "S": "s", + "L": "ms", + "U": "us", + "N": "ns", +} + + cpdef to_offset(freq, bint is_period=False): """ Return DateOffset object from string or datetime.timedelta object. @@ -5389,6 +5401,8 @@ cpdef to_offset(freq, bint is_period=False): # If n==0, then stride_sign is already incorporated # into the offset offset *= stride_sign + elif name in deprec_to_valid_alias: + raise ValueError(f"Did you mean '{deprec_to_valid_alias[name]}'?") else: stride = int(stride) offset = _get_offset(name) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 26b182fb4e9b1..c5f3788506117 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -1157,6 +1157,11 @@ def test_offset_multiplication( tm.assert_series_equal(resultarray, expectedarray) +def test_offset_deprecated_error(): + with pytest.raises(ValueError, match=r"Did you mean 'h'?"): + date_range("2012-01-01", periods=3, freq="H") + + def test_dateoffset_operations_on_dataframes(performance_warning): # GH 47953 df = DataFrame({"T": [Timestamp("2019-04-30")], "D": [DateOffset(months=1)]}) From 51c55cd89696ebeaaa449a20434f7e308cfdccac Mon Sep 17 00:00:00 2001 From: jgao8 <208881996+jgao8@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:03:46 -0400 Subject: [PATCH 5/7] update location of checking deprecated freq --- pandas/_libs/tslibs/offsets.pyx | 7 +++++-- pandas/tests/tseries/offsets/test_offsets.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 7e4f4fb6c0170..cd04e167ffe41 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -5371,6 +5371,11 @@ cpdef to_offset(freq, bint is_period=False): tups = zip(split[0::4], split[1::4], split[2::4], strict=False) for n, (sep, stride, name) in enumerate(tups): + if name in deprec_to_valid_alias: + raise ValueError(INVALID_FREQ_ERR_MSG.format( + f"{name}. Did you mean {deprec_to_valid_alias[name]}?") + ) + name = _warn_about_deprecated_aliases(name, is_period) _validate_to_offset_alias(name, is_period) if is_period: @@ -5401,8 +5406,6 @@ cpdef to_offset(freq, bint is_period=False): # If n==0, then stride_sign is already incorporated # into the offset offset *= stride_sign - elif name in deprec_to_valid_alias: - raise ValueError(f"Did you mean '{deprec_to_valid_alias[name]}'?") else: stride = int(stride) offset = _get_offset(name) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index c5f3788506117..1710cd8fff18f 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -1158,7 +1158,7 @@ def test_offset_multiplication( def test_offset_deprecated_error(): - with pytest.raises(ValueError, match=r"Did you mean 'h'?"): + with pytest.raises(ValueError, match=r"Did you mean h?"): date_range("2012-01-01", periods=3, freq="H") From 931610a9ba84f120ed90a23d14677c4d40771587 Mon Sep 17 00:00:00 2001 From: jgao8 <208881996+jgao8@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:24:19 -0400 Subject: [PATCH 6/7] address PR comments --- pandas/_libs/tslibs/offsets.pyx | 54 ++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index cd04e167ffe41..cb2d6d3c85132 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -5188,6 +5188,27 @@ INVALID_FREQ_ERR_MSG = "Invalid frequency: {0}" _offset_map = {} +deprec_to_valid_alias = { + "H": "h", + "BH": "bh", + "CBH": "cbh", + "T": "min", + "S": "s", + "L": "ms", + "U": "us", + "N": "ns", +} + + +def raise_invalid_freq(freq: str, extra_message: str | None = None) -> None: + msg = f"Invalid frequency: {freq}." + if extra_message is not None: + msg += " " + extra_message + if freq in deprec_to_valid_alias: + msg += " " + "Did you mean {deprec_to_valid_alias[name]}?" + raise ValueError(msg) + + def _warn_about_deprecated_aliases(name: str, is_period: bool) -> str: if name in _lite_rule_alias: return name @@ -5236,7 +5257,7 @@ def _validate_to_offset_alias(alias: str, is_period: bool) -> None: if (alias.upper() != alias and alias.lower() not in {"s", "ms", "us", "ns"} and alias.upper().split("-")[0].endswith(("S", "E"))): - raise ValueError(INVALID_FREQ_ERR_MSG.format(alias)) + raise ValueError(raise_invalid_freq(freq=alias)) if ( is_period and alias in c_OFFSET_TO_PERIOD_FREQSTR and @@ -5267,8 +5288,9 @@ def _get_offset(name: str) -> BaseOffset: offset = klass._from_name(*split[1:]) except (ValueError, TypeError, KeyError) as err: # bad prefix or suffix - raise ValueError(INVALID_FREQ_ERR_MSG.format( - f"{name}, failed to parse with error message: {repr(err)}") + raise_invalid_freq( + freq=name, + extra_message=f"Failed to parse with error message: {repr(err)}." ) # cache _offset_map[name] = offset @@ -5276,18 +5298,6 @@ def _get_offset(name: str) -> BaseOffset: return _offset_map[name] -deprec_to_valid_alias = { - "H": "h", - "BH": "bh", - "CBH": "cbh", - "T": "min", - "S": "s", - "L": "ms", - "U": "us", - "N": "ns", -} - - cpdef to_offset(freq, bint is_period=False): """ Return DateOffset object from string or datetime.timedelta object. @@ -5371,11 +5381,6 @@ cpdef to_offset(freq, bint is_period=False): tups = zip(split[0::4], split[1::4], split[2::4], strict=False) for n, (sep, stride, name) in enumerate(tups): - if name in deprec_to_valid_alias: - raise ValueError(INVALID_FREQ_ERR_MSG.format( - f"{name}. Did you mean {deprec_to_valid_alias[name]}?") - ) - name = _warn_about_deprecated_aliases(name, is_period) _validate_to_offset_alias(name, is_period) if is_period: @@ -5416,9 +5421,10 @@ cpdef to_offset(freq, bint is_period=False): else: result = result + offset except (ValueError, TypeError) as err: - raise ValueError(INVALID_FREQ_ERR_MSG.format( - f"{freq}, failed to parse with error message: {repr(err)}") - ) from err + raise_invalid_freq( + freq=freq, + extra_message=f"Failed to parse with error message: {repr(err)}" + ) # TODO(3.0?) once deprecation of "d" is enforced, the check for it here # can be removed @@ -5434,7 +5440,7 @@ cpdef to_offset(freq, bint is_period=False): result = None if result is None: - raise ValueError(INVALID_FREQ_ERR_MSG.format(freq)) + raise_invalid_freq(freq=freq) try: has_period_dtype_code = hasattr(result, "_period_dtype_code") From 7636cd4f3fd2ce5455f3722354e226d71862ab24 Mon Sep 17 00:00:00 2001 From: jgao8 <208881996+jgao8@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:15:34 -0400 Subject: [PATCH 7/7] fix message and test case --- pandas/_libs/tslibs/offsets.pyx | 4 ++-- pandas/tests/tseries/offsets/test_offsets.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 9a5bf135f4603..bfee228b632b1 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -5203,9 +5203,9 @@ deprec_to_valid_alias = { def raise_invalid_freq(freq: str, extra_message: str | None = None) -> None: msg = f"Invalid frequency: {freq}." if extra_message is not None: - msg += " " + extra_message + msg += f" {extra_message}" if freq in deprec_to_valid_alias: - msg += " " + "Did you mean {deprec_to_valid_alias[name]}?" + msg += f" Did you mean {deprec_to_valid_alias[freq]}?" raise ValueError(msg) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 1710cd8fff18f..28badd877fccb 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -1158,7 +1158,7 @@ def test_offset_multiplication( def test_offset_deprecated_error(): - with pytest.raises(ValueError, match=r"Did you mean h?"): + with pytest.raises(ValueError, match="Did you mean h"): date_range("2012-01-01", periods=3, freq="H")