From 39f59c4378b8c2b58fd0c979a36ccf8ecd518810 Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Thu, 18 Jan 2024 21:14:39 +0100 Subject: [PATCH 01/10] raise ValueError in asfreq for invalid period freq --- pandas/core/resample.py | 10 ++++++++++ pandas/tests/resample/test_period_index.py | 20 ++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 924f9e6d49040..322a08480e2fa 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -2827,6 +2827,16 @@ def asfreq( if how is None: how = "E" + if isinstance(freq, str): + freq = to_offset(freq, is_period=True) + if hasattr(freq, "_period_dtype_code"): + freq = freq_to_period_freqstr(freq.n, freq.name) + else: + raise ValueError( + f"Invalid offset: '{freq.name}' for converting time series " + f"with PeriodIndex." + ) + if isinstance(freq, BaseOffset): if hasattr(freq, "_period_dtype_code"): freq = freq_to_period_freqstr(freq.n, freq.name) diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py index 7b3edbad6666b..f55be2d021179 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -1032,14 +1032,30 @@ def test_resample_lowercase_frequency_deprecated( offsets.BusinessHour(2), ], ) - def test_asfreq_invalid_period_freq(self, offset, frame_or_series): - # GH#9586 + def test_asfreq_invalid_period_offset(self, offset, frame_or_series): + # GH#55785 msg = f"Invalid offset: '{offset.base}' for converting time series " obj = frame_or_series(range(5), index=period_range("2020-01-01", periods=5)) with pytest.raises(ValueError, match=msg): obj.asfreq(freq=offset) + @pytest.mark.parametrize( + "freq", + [ + "2BMS", + "2YS-MAR", + "2bh", + ], + ) + def test_asfreq_invalid_period_freq(self, freq, frame_or_series): + # GH#55785 + msg = f"Invalid offset: '{freq[1:]}' for converting time series " + + obj = frame_or_series(range(5), index=period_range("2020-01-01", periods=5)) + with pytest.raises(ValueError, match=msg): + obj.asfreq(freq=freq) + @pytest.mark.parametrize( "freq,freq_depr", From ff1c6e789280d24b80d672824fd0265a975094fe Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Thu, 25 Jan 2024 20:08:11 +0100 Subject: [PATCH 02/10] correct PeriodIndex.asfreq, fix test, add tests --- pandas/core/indexes/period.py | 19 +++++++++- pandas/core/resample.py | 10 ------ .../indexes/period/methods/test_asfreq.py | 35 ++++++++++++++++++- pandas/tests/resample/test_period_index.py | 16 --------- 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index b2f1933800fd3..2cd3c1a6ff701 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -16,8 +16,13 @@ Period, Resolution, Tick, + to_offset, ) -from pandas._libs.tslibs.dtypes import OFFSET_TO_PERIOD_FREQSTR +from pandas._libs.tslibs.dtypes import ( + OFFSET_TO_PERIOD_FREQSTR, + freq_to_period_freqstr, +) +from pandas._libs.tslibs.period import INVALID_FREQ_ERR_MSG from pandas.util._decorators import ( cache_readonly, doc, @@ -205,6 +210,18 @@ def _resolution_obj(self) -> Resolution: **_shared_doc_kwargs, ) def asfreq(self, freq=None, how: str = "E") -> Self: + if isinstance(freq, str): + offset = to_offset(freq, is_period=True) + if hasattr(offset, "_period_dtype_code"): + freq = freq_to_period_freqstr(offset.n, offset.name) + elif offset.name == freq.replace(f"{offset.n}", ""): + raise ValueError( + f"Invalid offset: '{offset.name}' for converting time series " + f"with PeriodIndex." + ) + else: + raise ValueError(INVALID_FREQ_ERR_MSG.format(f"{freq}")) + arr = self._data.asfreq(freq, how) return type(self)._simple_new(arr, name=self.name) diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 76a020a82e7ad..082196abc17c2 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -2827,16 +2827,6 @@ def asfreq( if how is None: how = "E" - if isinstance(freq, str): - freq = to_offset(freq, is_period=True) - if hasattr(freq, "_period_dtype_code"): - freq = freq_to_period_freqstr(freq.n, freq.name) - else: - raise ValueError( - f"Invalid offset: '{freq.name}' for converting time series " - f"with PeriodIndex." - ) - if isinstance(freq, BaseOffset): if hasattr(freq, "_period_dtype_code"): freq = freq_to_period_freqstr(freq.n, freq.name) diff --git a/pandas/tests/indexes/period/methods/test_asfreq.py b/pandas/tests/indexes/period/methods/test_asfreq.py index ed078a3e8fb8b..72ae08775aeac 100644 --- a/pandas/tests/indexes/period/methods/test_asfreq.py +++ b/pandas/tests/indexes/period/methods/test_asfreq.py @@ -70,7 +70,7 @@ def test_asfreq(self): msg = "How must be one of S or E" with pytest.raises(ValueError, match=msg): - pi7.asfreq("T", "foo") + pi7.asfreq("min", "foo") result1 = pi1.asfreq("3M") result2 = pi1.asfreq("M") expected = period_range(freq="M", start="2001-12", end="2001-12") @@ -136,3 +136,36 @@ def test_asfreq_with_different_n(self): excepted = Series([1, 2], index=PeriodIndex(["2020-02", "2020-04"], freq="M")) tm.assert_series_equal(result, excepted) + + @pytest.mark.parametrize( + "freq", + [ + "2BMS", + "2YS-MAR", + "2bh", + ], + ) + def test_pi_asfreq_invalid_offset(self, freq): + # GH#55785 + msg = f"Invalid offset: '{freq[1:]}' for converting time series " + + pi = PeriodIndex(["2020-01-01", "2021-01-01"], freq="M") + with pytest.raises(ValueError, match=msg): + pi.asfreq(freq=freq) + + @pytest.mark.parametrize( + "freq", + [ + "2BME", + "2YE-MAR", + "2BM", + "2QE", + ], + ) + def test_pi_asfreq_invalid_frequency(self, freq): + # GH#55785 + msg = f"Invalid frequency: {freq}" + + pi = PeriodIndex(["2020-01-01", "2021-01-01"], freq="M") + with pytest.raises(ValueError, match=msg): + pi.asfreq(freq=freq) diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py index f55be2d021179..610eeb6dcf32f 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -1040,22 +1040,6 @@ def test_asfreq_invalid_period_offset(self, offset, frame_or_series): with pytest.raises(ValueError, match=msg): obj.asfreq(freq=offset) - @pytest.mark.parametrize( - "freq", - [ - "2BMS", - "2YS-MAR", - "2bh", - ], - ) - def test_asfreq_invalid_period_freq(self, freq, frame_or_series): - # GH#55785 - msg = f"Invalid offset: '{freq[1:]}' for converting time series " - - obj = frame_or_series(range(5), index=period_range("2020-01-01", periods=5)) - with pytest.raises(ValueError, match=msg): - obj.asfreq(freq=freq) - @pytest.mark.parametrize( "freq,freq_depr", From fff7c9d46eaaecd64760c50e4e5f1f17305e4c6d Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Thu, 1 Feb 2024 10:34:42 +0100 Subject: [PATCH 03/10] move raising ValueError to to_offset --- pandas/_libs/tslibs/offsets.pyx | 5 ++++- pandas/core/indexes/period.py | 12 ------------ pandas/tests/indexes/period/methods/test_asfreq.py | 13 ------------- 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index e1615a1ceb818..3e4cd879a7710 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -4846,7 +4846,7 @@ cpdef to_offset(freq, bint is_period=False): ) elif PyDelta_Check(freq): - return delta_to_tick(freq) + delta = delta_to_tick(freq) elif isinstance(freq, str): delta = None @@ -4964,6 +4964,9 @@ cpdef to_offset(freq, bint is_period=False): if delta is None: raise ValueError(INVALID_FREQ_ERR_MSG.format(freq)) + if is_period and not hasattr(delta, "_period_dtype_code"): + raise ValueError(INVALID_FREQ_ERR_MSG.format(freq)) + return delta diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 2cd3c1a6ff701..c92a2ac4a8505 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -210,18 +210,6 @@ def _resolution_obj(self) -> Resolution: **_shared_doc_kwargs, ) def asfreq(self, freq=None, how: str = "E") -> Self: - if isinstance(freq, str): - offset = to_offset(freq, is_period=True) - if hasattr(offset, "_period_dtype_code"): - freq = freq_to_period_freqstr(offset.n, offset.name) - elif offset.name == freq.replace(f"{offset.n}", ""): - raise ValueError( - f"Invalid offset: '{offset.name}' for converting time series " - f"with PeriodIndex." - ) - else: - raise ValueError(INVALID_FREQ_ERR_MSG.format(f"{freq}")) - arr = self._data.asfreq(freq, how) return type(self)._simple_new(arr, name=self.name) diff --git a/pandas/tests/indexes/period/methods/test_asfreq.py b/pandas/tests/indexes/period/methods/test_asfreq.py index 72ae08775aeac..e8d09ca8db2e0 100644 --- a/pandas/tests/indexes/period/methods/test_asfreq.py +++ b/pandas/tests/indexes/period/methods/test_asfreq.py @@ -143,19 +143,6 @@ def test_asfreq_with_different_n(self): "2BMS", "2YS-MAR", "2bh", - ], - ) - def test_pi_asfreq_invalid_offset(self, freq): - # GH#55785 - msg = f"Invalid offset: '{freq[1:]}' for converting time series " - - pi = PeriodIndex(["2020-01-01", "2021-01-01"], freq="M") - with pytest.raises(ValueError, match=msg): - pi.asfreq(freq=freq) - - @pytest.mark.parametrize( - "freq", - [ "2BME", "2YE-MAR", "2BM", From a389d1106e71409cf9f4243bbbe065626a352f18 Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Thu, 1 Feb 2024 10:48:44 +0100 Subject: [PATCH 04/10] fix pre-commit errors --- pandas/core/indexes/period.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index c92a2ac4a8505..b2f1933800fd3 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -16,13 +16,8 @@ Period, Resolution, Tick, - to_offset, ) -from pandas._libs.tslibs.dtypes import ( - OFFSET_TO_PERIOD_FREQSTR, - freq_to_period_freqstr, -) -from pandas._libs.tslibs.period import INVALID_FREQ_ERR_MSG +from pandas._libs.tslibs.dtypes import OFFSET_TO_PERIOD_FREQSTR from pandas.util._decorators import ( cache_readonly, doc, From d2ea9144dc0e09922831cb4852bf2fd4e6f77040 Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Fri, 2 Feb 2024 21:26:33 +0100 Subject: [PATCH 05/10] correct def to_offset, dt64arr_to_periodarr and fix tests --- pandas/_libs/tslibs/offsets.pyx | 5 ++++- pandas/core/arrays/period.py | 7 +------ pandas/tests/dtypes/test_dtypes.py | 10 +++++----- .../indexes/datetimes/methods/test_to_period.py | 2 +- pandas/tests/indexes/period/methods/test_asfreq.py | 14 +++++++++++++- pandas/tests/scalar/period/test_asfreq.py | 5 ++--- pandas/tests/scalar/period/test_period.py | 11 ++++++----- 7 files changed, 32 insertions(+), 22 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 3e4cd879a7710..2df1ee64da590 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -4965,7 +4965,10 @@ cpdef to_offset(freq, bint is_period=False): raise ValueError(INVALID_FREQ_ERR_MSG.format(freq)) if is_period and not hasattr(delta, "_period_dtype_code"): - raise ValueError(INVALID_FREQ_ERR_MSG.format(freq)) + if isinstance(freq, str): + raise ValueError(f"{delta.name} is not supported as period frequency") + else: + raise ValueError(f"{freq} is not supported as period frequency") return delta diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 3a245d353dca2..5584ed6ce2e77 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -1186,12 +1186,7 @@ def dt64arr_to_periodarr( reso = get_unit_from_dtype(data.dtype) freq = Period._maybe_convert_freq(freq) - try: - base = freq._period_dtype_code - except (AttributeError, TypeError) as err: - # AttributeError: _period_dtype_code might not exist - # TypeError: _period_dtype_code might intentionally raise - raise TypeError(f"{freq.name} is not supported as period frequency") from err + base = freq._period_dtype_code return c_dt64arr_to_periodarr(data.view("i8"), base, tz, reso=reso), freq diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index c52d49034f74e..4977dcce92167 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -445,12 +445,12 @@ def test_construction(self): def test_cannot_use_custom_businessday(self): # GH#52534 - msg = "CustomBusinessDay is not supported as period frequency" + msg = "C is not supported as period frequency" + msg1 = "CustomBusinessDay is not supported as period frequency" msg2 = r"PeriodDtype\[B\] is deprecated" - with pytest.raises(TypeError, match=msg): - with tm.assert_produces_warning(FutureWarning, match=msg2): - PeriodDtype("C") - with pytest.raises(TypeError, match=msg): + with pytest.raises(ValueError, match=msg): + PeriodDtype("C") + with pytest.raises(TypeError, match=msg1): with tm.assert_produces_warning(FutureWarning, match=msg2): PeriodDtype(pd.offsets.CustomBusinessDay()) diff --git a/pandas/tests/indexes/datetimes/methods/test_to_period.py b/pandas/tests/indexes/datetimes/methods/test_to_period.py index 00c0216a9b3b5..de8d32f64cde2 100644 --- a/pandas/tests/indexes/datetimes/methods/test_to_period.py +++ b/pandas/tests/indexes/datetimes/methods/test_to_period.py @@ -221,5 +221,5 @@ def test_to_period_offsets_not_supported(self, freq): # GH#56243 msg = f"{freq[1:]} is not supported as period frequency" ts = date_range("1/1/2012", periods=4, freq=freq) - with pytest.raises(TypeError, match=msg): + with pytest.raises(ValueError, match=msg): ts.to_period() diff --git a/pandas/tests/indexes/period/methods/test_asfreq.py b/pandas/tests/indexes/period/methods/test_asfreq.py index e8d09ca8db2e0..556f8b40475b2 100644 --- a/pandas/tests/indexes/period/methods/test_asfreq.py +++ b/pandas/tests/indexes/period/methods/test_asfreq.py @@ -143,9 +143,21 @@ def test_asfreq_with_different_n(self): "2BMS", "2YS-MAR", "2bh", + ], + ) + def test_pi_asfreq_not_supported_frequency(self, freq): + # GH#55785 + msg = f"{freq[1:]} is not supported as period frequency" + + pi = PeriodIndex(["2020-01-01", "2021-01-01"], freq="M") + with pytest.raises(ValueError, match=msg): + pi.asfreq(freq=freq) + + @pytest.mark.parametrize( + "freq", + [ "2BME", "2YE-MAR", - "2BM", "2QE", ], ) diff --git a/pandas/tests/scalar/period/test_asfreq.py b/pandas/tests/scalar/period/test_asfreq.py index 4489c307172d7..73c4d8061c257 100644 --- a/pandas/tests/scalar/period/test_asfreq.py +++ b/pandas/tests/scalar/period/test_asfreq.py @@ -820,10 +820,9 @@ def test_asfreq_MS(self): assert initial.asfreq(freq="M", how="S") == Period("2013-01", "M") - msg = INVALID_FREQ_ERR_MSG + msg = "MS is not supported as period frequency" with pytest.raises(ValueError, match=msg): initial.asfreq(freq="MS", how="S") - msg = "MonthBegin is not supported as period frequency" - with pytest.raises(TypeError, match=msg): + with pytest.raises(ValueError, match=msg): Period("2013-01", "MS") diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index d819e903a0bae..b546aa7c8f264 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -41,20 +41,21 @@ class TestPeriodDisallowedFreqs: def test_offsets_not_supported(self, freq, freq_msg): # GH#55785 msg = f"{freq_msg} is not supported as period frequency" - with pytest.raises(TypeError, match=msg): + with pytest.raises(ValueError, match=msg): Period(year=2014, freq=freq) def test_custom_business_day_freq_raises(self): # GH#52534 - msg = "CustomBusinessDay is not supported as period frequency" - with pytest.raises(TypeError, match=msg): + msg = "C is not supported as period frequency" + with pytest.raises(ValueError, match=msg): Period("2023-04-10", freq="C") + msg = "CustomBusinessDay is not supported as period frequency" with pytest.raises(TypeError, match=msg): Period("2023-04-10", freq=offsets.CustomBusinessDay()) def test_invalid_frequency_error_message(self): - msg = "WeekOfMonth is not supported as period frequency" - with pytest.raises(TypeError, match=msg): + msg = "WOM-1MON is not supported as period frequency" + with pytest.raises(ValueError, match=msg): Period("2012-01-02", freq="WOM-1MON") def test_invalid_frequency_period_error_message(self): From 3735e238b376b7979bcfff1bcc646900fb2ccf38 Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Fri, 2 Feb 2024 22:30:03 +0100 Subject: [PATCH 06/10] fix tests --- pandas/tests/scalar/period/test_period.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index b546aa7c8f264..3372ad38e6719 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -41,7 +41,7 @@ class TestPeriodDisallowedFreqs: def test_offsets_not_supported(self, freq, freq_msg): # GH#55785 msg = f"{freq_msg} is not supported as period frequency" - with pytest.raises(ValueError, match=msg): + with pytest.raises(TypeError, match=msg): Period(year=2014, freq=freq) def test_custom_business_day_freq_raises(self): From b1e6ba69d6c7017223469808a1c13dde6652b4f2 Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Mon, 5 Feb 2024 23:28:25 +0100 Subject: [PATCH 07/10] correct def to_offset, fix tests --- pandas/_libs/tslibs/offsets.pyx | 6 +++--- pandas/core/arrays/period.py | 4 ++-- pandas/plotting/_matplotlib/timeseries.py | 5 ++++- pandas/tests/dtypes/test_dtypes.py | 4 ++-- .../tests/indexes/period/methods/test_asfreq.py | 17 +++++++++++++++++ pandas/tests/scalar/period/test_period.py | 8 ++++---- 6 files changed, 32 insertions(+), 12 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 2df1ee64da590..0c1191b6cde70 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -4271,9 +4271,7 @@ cdef class CustomBusinessDay(BusinessDay): @property def _period_dtype_code(self): # GH#52534 - raise TypeError( - "CustomBusinessDay is not supported as period frequency" - ) + raise ValueError(f"{self.base} is not supported as period frequency") _apply_array = BaseOffset._apply_array @@ -4838,6 +4836,8 @@ cpdef to_offset(freq, bint is_period=False): return None if isinstance(freq, BaseOffset): + if is_period and not hasattr(freq, "_period_dtype_code"): + raise ValueError(f"{freq.base} is not supported as period frequency") return freq if isinstance(freq, tuple): diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 5584ed6ce2e77..8bbc4976675c8 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -733,8 +733,8 @@ def asfreq(self, freq=None, how: str = "E") -> Self: '2015-01'], dtype='period[M]') """ how = libperiod.validate_end_alias(how) - if isinstance(freq, BaseOffset): - freq = freq_to_period_freqstr(freq.n, freq.name) + if isinstance(freq, BaseOffset) and hasattr(freq, "_period_dtype_code"): + freq = PeriodDtype(freq)._freqstr freq = Period._maybe_convert_freq(freq) base1 = self._dtype._dtype_code diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py index 067bcf0b01ccb..d4587a6ccf90a 100644 --- a/pandas/plotting/_matplotlib/timeseries.py +++ b/pandas/plotting/_matplotlib/timeseries.py @@ -205,7 +205,10 @@ def _get_ax_freq(ax: Axes): def _get_period_alias(freq: timedelta | BaseOffset | str) -> str | None: - freqstr = to_offset(freq, is_period=True).rule_code + if isinstance(freq, BaseOffset): + freqstr = freq.name + else: + freqstr = to_offset(freq, is_period=True).rule_code return get_period_alias(freqstr) diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index 4977dcce92167..c6da01636247d 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -446,11 +446,11 @@ def test_construction(self): def test_cannot_use_custom_businessday(self): # GH#52534 msg = "C is not supported as period frequency" - msg1 = "CustomBusinessDay is not supported as period frequency" + msg1 = " is not supported as period frequency" msg2 = r"PeriodDtype\[B\] is deprecated" with pytest.raises(ValueError, match=msg): PeriodDtype("C") - with pytest.raises(TypeError, match=msg1): + with pytest.raises(ValueError, match=msg1): with tm.assert_produces_warning(FutureWarning, match=msg2): PeriodDtype(pd.offsets.CustomBusinessDay()) diff --git a/pandas/tests/indexes/period/methods/test_asfreq.py b/pandas/tests/indexes/period/methods/test_asfreq.py index 556f8b40475b2..dffd3fcedc2b0 100644 --- a/pandas/tests/indexes/period/methods/test_asfreq.py +++ b/pandas/tests/indexes/period/methods/test_asfreq.py @@ -7,6 +7,8 @@ ) import pandas._testing as tm +from pandas.tseries import offsets + class TestPeriodIndex: def test_asfreq(self): @@ -168,3 +170,18 @@ def test_pi_asfreq_invalid_frequency(self, freq): pi = PeriodIndex(["2020-01-01", "2021-01-01"], freq="M") with pytest.raises(ValueError, match=msg): pi.asfreq(freq=freq) + + @pytest.mark.parametrize( + "freq", + [ + offsets.MonthBegin(2), + offsets.BusinessMonthEnd(2), + ], + ) + def test_pi_asfreq_invalid_baseoffset(self, freq): + # GH#56945 + msg = f"{freq.base} is not supported as period frequency" + + pi = PeriodIndex(["2020-01-01", "2021-01-01"], freq="M") + with pytest.raises(ValueError, match=msg): + pi.asfreq(freq=freq) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 3372ad38e6719..d3192e97b1b7c 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -40,8 +40,8 @@ class TestPeriodDisallowedFreqs: ) def test_offsets_not_supported(self, freq, freq_msg): # GH#55785 - msg = f"{freq_msg} is not supported as period frequency" - with pytest.raises(TypeError, match=msg): + msg = f"{freq.base} is not supported as period frequency" + with pytest.raises(ValueError, match=msg): Period(year=2014, freq=freq) def test_custom_business_day_freq_raises(self): @@ -49,8 +49,8 @@ def test_custom_business_day_freq_raises(self): msg = "C is not supported as period frequency" with pytest.raises(ValueError, match=msg): Period("2023-04-10", freq="C") - msg = "CustomBusinessDay is not supported as period frequency" - with pytest.raises(TypeError, match=msg): + msg = f"{offsets.CustomBusinessDay().base} is not supported as period frequency" + with pytest.raises(ValueError, match=msg): Period("2023-04-10", freq=offsets.CustomBusinessDay()) def test_invalid_frequency_error_message(self): From c41557a31955c5f6b156367f6d179bab33e65d59 Mon Sep 17 00:00:00 2001 From: Natalia Mokeeva Date: Tue, 6 Feb 2024 15:05:56 +0100 Subject: [PATCH 08/10] add a note to v2.2.1.rst --- doc/source/whatsnew/v2.2.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v2.2.1.rst b/doc/source/whatsnew/v2.2.1.rst index d3065cdd8b624..2a84d09703355 100644 --- a/doc/source/whatsnew/v2.2.1.rst +++ b/doc/source/whatsnew/v2.2.1.rst @@ -21,7 +21,7 @@ Fixed regressions Bug fixes ~~~~~~~~~ -- +- Bug in :meth:`PeriodIndex.asfreq` silently converting frequencies which are not supported as period frequencies instead of raising an error (:issue:`56945`) .. --------------------------------------------------------------------------- .. _whatsnew_221.other: From deea30da06b6f225d17ba271579e086eff0c5664 Mon Sep 17 00:00:00 2001 From: MarcoGorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:36:20 +0000 Subject: [PATCH 09/10] try simplifying / clarifying --- pandas/_libs/tslibs/offsets.pyx | 28 +++++++++---------- pandas/plotting/_matplotlib/timeseries.py | 6 +--- .../indexes/period/methods/test_asfreq.py | 6 ++-- pandas/tests/scalar/period/test_period.py | 3 +- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 0c1191b6cde70..e96a905367f69 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -4835,21 +4835,19 @@ cpdef to_offset(freq, bint is_period=False): if freq is None: return None - if isinstance(freq, BaseOffset): - if is_period and not hasattr(freq, "_period_dtype_code"): - raise ValueError(f"{freq.base} is not supported as period frequency") - return freq - if isinstance(freq, tuple): raise TypeError( f"to_offset does not support tuples {freq}, pass as a string instead" ) + if isinstance(freq, BaseOffset): + result = freq + elif PyDelta_Check(freq): - delta = delta_to_tick(freq) + result = delta_to_tick(freq) elif isinstance(freq, str): - delta = None + result = None stride_sign = None try: @@ -4950,27 +4948,27 @@ cpdef to_offset(freq, bint is_period=False): offset = _get_offset(prefix) offset = offset * int(np.fabs(stride) * stride_sign) - if delta is None: - delta = offset + if result is None: + result = offset else: - delta = delta + offset + 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)}") ) else: - delta = None + result = None - if delta is None: + if result is None: raise ValueError(INVALID_FREQ_ERR_MSG.format(freq)) - if is_period and not hasattr(delta, "_period_dtype_code"): + if is_period and not hasattr(result, "_period_dtype_code"): if isinstance(freq, str): - raise ValueError(f"{delta.name} is not supported as period frequency") + raise ValueError(f"{result.name} is not supported as period frequency") else: raise ValueError(f"{freq} is not supported as period frequency") - return delta + return result # ---------------------------------------------------------------------- diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py index d4587a6ccf90a..e15a9ee38be5e 100644 --- a/pandas/plotting/_matplotlib/timeseries.py +++ b/pandas/plotting/_matplotlib/timeseries.py @@ -205,11 +205,7 @@ def _get_ax_freq(ax: Axes): def _get_period_alias(freq: timedelta | BaseOffset | str) -> str | None: - if isinstance(freq, BaseOffset): - freqstr = freq.name - else: - freqstr = to_offset(freq, is_period=True).rule_code - + freqstr = to_offset(freq, is_period=True).rule_code return get_period_alias(freqstr) diff --git a/pandas/tests/indexes/period/methods/test_asfreq.py b/pandas/tests/indexes/period/methods/test_asfreq.py index dffd3fcedc2b0..865bae69d91c7 100644 --- a/pandas/tests/indexes/period/methods/test_asfreq.py +++ b/pandas/tests/indexes/period/methods/test_asfreq.py @@ -1,3 +1,5 @@ +import re + import pytest from pandas import ( @@ -72,7 +74,7 @@ def test_asfreq(self): msg = "How must be one of S or E" with pytest.raises(ValueError, match=msg): - pi7.asfreq("min", "foo") + pi7.asfreq("T", "foo") result1 = pi1.asfreq("3M") result2 = pi1.asfreq("M") expected = period_range(freq="M", start="2001-12", end="2001-12") @@ -180,7 +182,7 @@ def test_pi_asfreq_invalid_frequency(self, freq): ) def test_pi_asfreq_invalid_baseoffset(self, freq): # GH#56945 - msg = f"{freq.base} is not supported as period frequency" + msg = re.escape(f"{freq} is not supported as period frequency") pi = PeriodIndex(["2020-01-01", "2021-01-01"], freq="M") with pytest.raises(ValueError, match=msg): diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index d3192e97b1b7c..2c3a0816737fc 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -3,6 +3,7 @@ datetime, timedelta, ) +import re import numpy as np import pytest @@ -40,7 +41,7 @@ class TestPeriodDisallowedFreqs: ) def test_offsets_not_supported(self, freq, freq_msg): # GH#55785 - msg = f"{freq.base} is not supported as period frequency" + msg = re.escape(f"{freq} is not supported as period frequency") with pytest.raises(ValueError, match=msg): Period(year=2014, freq=freq) From 4cbcc5e27eeb8143f8dd445aee9bef3c1c900108 Mon Sep 17 00:00:00 2001 From: MarcoGorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Wed, 7 Feb 2024 12:13:03 +0000 Subject: [PATCH 10/10] revert _get_period_alias simplification --- pandas/plotting/_matplotlib/timeseries.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py index e15a9ee38be5e..d4587a6ccf90a 100644 --- a/pandas/plotting/_matplotlib/timeseries.py +++ b/pandas/plotting/_matplotlib/timeseries.py @@ -205,7 +205,11 @@ def _get_ax_freq(ax: Axes): def _get_period_alias(freq: timedelta | BaseOffset | str) -> str | None: - freqstr = to_offset(freq, is_period=True).rule_code + if isinstance(freq, BaseOffset): + freqstr = freq.name + else: + freqstr = to_offset(freq, is_period=True).rule_code + return get_period_alias(freqstr)