diff --git a/ci/code_checks.sh b/ci/code_checks.sh index a310b71d59da6..3a941deb2c68d 100755 --- a/ci/code_checks.sh +++ b/ci/code_checks.sh @@ -74,6 +74,7 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then -i "pandas.Series.dt PR01" `# Accessors are implemented as classes, but we do not document the Parameters section` \ -i "pandas.Period.freq GL08" \ -i "pandas.Period.ordinal GL08" \ + -i "pandas.errors.IncompatibleFrequency SA01,SS06,EX01" \ -i "pandas.core.groupby.DataFrameGroupBy.plot PR02" \ -i "pandas.core.groupby.SeriesGroupBy.plot PR02" \ -i "pandas.core.resample.Resampler.quantile PR01,PR07" \ diff --git a/doc/source/reference/testing.rst b/doc/source/reference/testing.rst index 1f164d1aa98b4..2c9c2dcae0f69 100644 --- a/doc/source/reference/testing.rst +++ b/doc/source/reference/testing.rst @@ -36,6 +36,7 @@ Exceptions and warnings errors.DuplicateLabelError errors.EmptyDataError errors.IncompatibilityWarning + errors.IncompatibleFrequency errors.IndexingError errors.InvalidColumnName errors.InvalidComparison diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index be7a07dface0a..8f70dd4cfe4e2 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -414,6 +414,7 @@ Other API changes - Index set operations (like union or intersection) will now ignore the dtype of an empty ``RangeIndex`` or empty ``Index`` with object dtype when determining the dtype of the resulting Index (:issue:`60797`) +- :class:`IncompatibleFrequency` now subclasses ``TypeError`` instead of ``ValueError``. As a result, joins with mismatched frequencies now cast to object like other non-comparable joins, and arithmetic with indexes with mismatched frequencies align (:issue:`55782`) - Comparison operations between :class:`Index` and :class:`Series` now consistently return :class:`Series` regardless of which object is on the left or right (:issue:`36759`) - Numpy functions like ``np.isinf`` that return a bool dtype when called on a :class:`Index` object now return a bool-dtype :class:`Index` instead of ``np.ndarray`` (:issue:`52676`) diff --git a/pandas/_libs/tslibs/period.pyi b/pandas/_libs/tslibs/period.pyi index 22f3bdbe668de..5cb9f891b312a 100644 --- a/pandas/_libs/tslibs/period.pyi +++ b/pandas/_libs/tslibs/period.pyi @@ -15,7 +15,7 @@ from pandas._typing import ( INVALID_FREQ_ERR_MSG: str DIFFERENT_FREQ: str -class IncompatibleFrequency(ValueError): ... +class IncompatibleFrequency(TypeError): ... def periodarr_to_dt64arr( periodarr: npt.NDArray[np.int64], # const int64_t[:] diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 350216cf89ce4..df5c17745b8a4 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1625,7 +1625,11 @@ DIFFERENT_FREQ = ("Input has different freq={other_freq} " "from {cls}(freq={own_freq})") -class IncompatibleFrequency(ValueError): +class IncompatibleFrequency(TypeError): + """ + Raised when trying to compare or operate between Periods with different + frequencies. + """ pass diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index f8d4dd4c78bcb..50fecc96f8186 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -544,7 +544,7 @@ def _validate_comparison_value(self, other): other = self._scalar_type(other) try: self._check_compatible_with(other) - except (TypeError, IncompatibleFrequency) as err: + except TypeError as err: # e.g. tzawareness mismatch raise InvalidComparison(other) from err @@ -558,7 +558,7 @@ def _validate_comparison_value(self, other): try: other = self._validate_listlike(other, allow_object=True) self._check_compatible_with(other) - except (TypeError, IncompatibleFrequency) as err: + except TypeError as err: if is_object_dtype(getattr(other, "dtype", None)): # We will have to operate element-wise pass diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index fb395f4f7bb1a..a50a1764961ed 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -38,7 +38,6 @@ no_default, ) from pandas._libs.tslibs import ( - IncompatibleFrequency, OutOfBoundsDatetime, Timestamp, tz_compare, @@ -3139,7 +3138,7 @@ def _union(self, other: Index, sort: bool | None): # test_union_same_value_duplicated_in_both fails) try: return self._outer_indexer(other)[0] - except (TypeError, IncompatibleFrequency): + except TypeError: # incomparable objects; should only be for object dtype value_list = list(lvals) diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index d1ca056ffcb19..a60a75369d0b4 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -9,6 +9,7 @@ from pandas._config.config import OptionError from pandas._libs.tslibs import ( + IncompatibleFrequency, OutOfBoundsDatetime, OutOfBoundsTimedelta, ) @@ -917,6 +918,7 @@ class InvalidComparison(Exception): "DuplicateLabelError", "EmptyDataError", "IncompatibilityWarning", + "IncompatibleFrequency", "IndexingError", "IntCastingNaNError", "InvalidColumnName", diff --git a/pandas/tests/indexes/period/test_indexing.py b/pandas/tests/indexes/period/test_indexing.py index 00e8262ddfa4c..75382cb735288 100644 --- a/pandas/tests/indexes/period/test_indexing.py +++ b/pandas/tests/indexes/period/test_indexing.py @@ -502,7 +502,7 @@ def test_get_indexer2(self): ) msg = "Input has different freq=None from PeriodArray\\(freq=h\\)" - with pytest.raises(ValueError, match=msg): + with pytest.raises(libperiod.IncompatibleFrequency, match=msg): idx.get_indexer(target, "nearest", tolerance="1 minute") tm.assert_numpy_array_equal( diff --git a/pandas/tests/indexes/period/test_join.py b/pandas/tests/indexes/period/test_join.py index 3e659c1a63266..9f733b358f772 100644 --- a/pandas/tests/indexes/period/test_join.py +++ b/pandas/tests/indexes/period/test_join.py @@ -1,7 +1,4 @@ import numpy as np -import pytest - -from pandas._libs.tslibs import IncompatibleFrequency from pandas import ( DataFrame, @@ -51,8 +48,9 @@ def test_join_does_not_recur(self): tm.assert_index_equal(res, expected) def test_join_mismatched_freq_raises(self): + # pre-GH#55782 this raises IncompatibleFrequency index = period_range("1/1/2000", "1/20/2000", freq="D") index3 = period_range("1/1/2000", "1/20/2000", freq="2D") - msg = r".*Input has different freq=2D from Period\(freq=D\)" - with pytest.raises(IncompatibleFrequency, match=msg): - index.join(index3) + result = index.join(index3) + expected = index.astype(object).join(index3.astype(object)) + tm.assert_index_equal(result, expected) diff --git a/pandas/tests/indexes/period/test_period.py b/pandas/tests/indexes/period/test_period.py index 77b8e76894647..d465225da7f24 100644 --- a/pandas/tests/indexes/period/test_period.py +++ b/pandas/tests/indexes/period/test_period.py @@ -1,6 +1,8 @@ import numpy as np import pytest +from pandas.errors import IncompatibleFrequency + from pandas import ( Index, NaT, @@ -198,7 +200,7 @@ def test_maybe_convert_timedelta(): offset = offsets.BusinessDay() msg = r"Input has different freq=B from PeriodIndex\(freq=D\)" - with pytest.raises(ValueError, match=msg): + with pytest.raises(IncompatibleFrequency, match=msg): pi._maybe_convert_timedelta(offset) diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index e7d284bd47e21..35a9742d653db 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -10,7 +10,6 @@ import pytest from pandas._libs import lib -from pandas._libs.tslibs import IncompatibleFrequency import pandas as pd from pandas import ( @@ -172,10 +171,6 @@ def test_add_series_with_period_index(self): result = ts + _permute(ts[::2]) tm.assert_series_equal(result, expected) - msg = "Input has different freq=D from Period\\(freq=Y-DEC\\)" - with pytest.raises(IncompatibleFrequency, match=msg): - ts + ts.asfreq("D", how="end") - @pytest.mark.parametrize( "target_add,input_value,expected_value", [