diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst index 2bc031e4b5f61..38f39270d12c7 100644 --- a/doc/source/whatsnew/v2.2.0.rst +++ b/doc/source/whatsnew/v2.2.0.rst @@ -103,6 +103,7 @@ Other enhancements - :meth:`ExtensionArray.duplicated` added to allow extension type implementations of the ``duplicated`` method (:issue:`55255`) - DataFrame.apply now allows the usage of numba (via ``engine="numba"``) to JIT compile the passed function, allowing for potential speedups (:issue:`54666`) - Implement masked algorithms for :meth:`Series.value_counts` (:issue:`54984`) +- Improved error message when constructing :class:`Period` with invalid offsets such as "QS" (:issue:`55785`) .. --------------------------------------------------------------------------- .. _whatsnew_220.notable_bug_fixes: diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index df3a2e3ecde48..0150aeadbd0ab 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -4177,7 +4177,7 @@ cdef class CustomBusinessDay(BusinessDay): def _period_dtype_code(self): # GH#52534 raise TypeError( - "CustomBusinessDay cannot be used with Period or PeriodDtype" + "CustomBusinessDay is not supported as period frequency" ) _apply_array = BaseOffset._apply_array diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 700f500c1d8b8..d1f925f3a0b48 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -2726,6 +2726,14 @@ class Period(_Period): if freq is not None: freq = cls._maybe_convert_freq(freq) + try: + period_dtype_code = freq._period_dtype_code + except (AttributeError, TypeError): + # AttributeError: _period_dtype_code might not exist + # TypeError: _period_dtype_code might intentionally raise + raise TypeError( + f"{(type(freq).__name__)} is not supported as period frequency" + ) nanosecond = 0 if ordinal is not None and value is not None: @@ -2758,7 +2766,7 @@ class Period(_Period): elif is_period_object(value): other = value - if freq is None or freq._period_dtype_code == other._dtype._dtype_code: + if freq is None or period_dtype_code == other._dtype._dtype_code: ordinal = other.ordinal freq = other.freq else: diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index 27994708d2bdb..04b3feabb9854 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -444,7 +444,7 @@ def test_construction(self): def test_cannot_use_custom_businessday(self): # GH#52534 - msg = "CustomBusinessDay cannot be used with Period or PeriodDtype" + msg = "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): diff --git a/pandas/tests/indexes/period/test_constructors.py b/pandas/tests/indexes/period/test_constructors.py index f1db5ab28be30..505050a2089d8 100644 --- a/pandas/tests/indexes/period/test_constructors.py +++ b/pandas/tests/indexes/period/test_constructors.py @@ -553,6 +553,21 @@ def test_map_with_string_constructor(self): # lastly, values should compare equal tm.assert_index_equal(res, expected) + @pytest.mark.parametrize( + "freq, freq_msg", + [ + (offsets.BYearBegin(), "BYearBegin"), + (offsets.YearBegin(2), "YearBegin"), + (offsets.QuarterBegin(startingMonth=12), "QuarterBegin"), + (offsets.BusinessMonthEnd(2), "BusinessMonthEnd"), + ], + ) + 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): + Period(year=2014, freq=freq) + class TestShallowCopy: def test_shallow_copy_empty(self): diff --git a/pandas/tests/scalar/period/test_asfreq.py b/pandas/tests/scalar/period/test_asfreq.py index 597282e10052e..7164de0a228d9 100644 --- a/pandas/tests/scalar/period/test_asfreq.py +++ b/pandas/tests/scalar/period/test_asfreq.py @@ -825,7 +825,8 @@ def test_asfreq_MS(self): with pytest.raises(ValueError, match=msg): initial.asfreq(freq="MS", how="S") - with pytest.raises(ValueError, match=msg): + msg = "MonthBegin is not supported as period frequency" + with pytest.raises(TypeError, match=msg): Period("2013-01", "MS") assert _period_code_map.get("MS") is None diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 8cc3ace52a4d4..448c2091e14f6 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -38,7 +38,7 @@ class TestPeriodConstruction: def test_custom_business_day_freq_raises(self): # GH#52534 - msg = "CustomBusinessDay cannot be used with Period or PeriodDtype" + msg = "CustomBusinessDay is not supported as period frequency" with pytest.raises(TypeError, match=msg): Period("2023-04-10", freq="C") with pytest.raises(TypeError, match=msg): @@ -1628,8 +1628,8 @@ def test_negone_ordinals(): def test_invalid_frequency_error_message(): - msg = "Invalid frequency: " - with pytest.raises(ValueError, match=msg): + msg = "WeekOfMonth is not supported as period frequency" + with pytest.raises(TypeError, match=msg): Period("2012-01-02", freq="WOM-1MON")