Repository: pandas-dev/pandas
Pull request #56813 (2024-01-10 16:40:03)

Backport PR #56594 on branch 2.2.x (DEPR: the method is_anchored() for offsets)
Backport PR #56594: DEPR: the method is_anchored() for offsets


Patch:
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index 9a9ac769a4..b5df0a319b 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -665,11 +665,13 @@ Other Deprecations
 - Deprecated :func:`pd.core.internals.api.make_block`, use public APIs instead (:issue:`40226`)
 - Deprecated :func:`read_gbq` and :meth:`DataFrame.to_gbq`. Use ``pandas_gbq.read_gbq`` and ``pandas_gbq.to_gbq`` instead https://pandas-gbq.readthedocs.io/en/latest/api.html (:issue:`55525`)
 - Deprecated :meth:`.DataFrameGroupBy.fillna` and :meth:`.SeriesGroupBy.fillna`; use :meth:`.DataFrameGroupBy.ffill`, :meth:`.DataFrameGroupBy.bfill` for forward and backward filling or :meth:`.DataFrame.fillna` to fill with a single value (or the Series equivalents) (:issue:`55718`)
+- Deprecated :meth:`DateOffset.is_anchored`, use ``obj.n == 1`` for non-Tick subclasses (for Tick this was always False) (:issue:`55388`)
 - Deprecated :meth:`DatetimeArray.__init__` and :meth:`TimedeltaArray.__init__`, use :func:`array` instead (:issue:`55623`)
 - Deprecated :meth:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`)
 - Deprecated :meth:`Series.ravel`, the underlying array is already 1D, so ravel is not necessary (:issue:`52511`)
 - Deprecated :meth:`Series.resample` and :meth:`DataFrame.resample` with a :class:`PeriodIndex` (and the 'convention' keyword), convert to :class:`DatetimeIndex` (with ``.to_timestamp()``) before resampling instead (:issue:`53481`)
 - Deprecated :meth:`Series.view`, use :meth:`Series.astype` instead to change the dtype (:issue:`20251`)
+- Deprecated :meth:`offsets.Tick.is_anchored`, use ``False`` instead (:issue:`55388`)
 - Deprecated ``core.internals`` members ``Block``, ``ExtensionBlock``, and ``DatetimeTZBlock``, use public APIs instead (:issue:`55139`)
 - Deprecated ``year``, ``month``, ``quarter``, ``day``, ``hour``, ``minute``, and ``second`` keywords in the :class:`PeriodIndex` constructor, use :meth:`PeriodIndex.from_fields` instead (:issue:`55960`)
 - Deprecated accepting a type as an argument in :meth:`Index.view`, call without any arguments instead (:issue:`55709`)
diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx
index b3788b6003..3a339171d0 100644
--- a/pandas/_libs/tslibs/offsets.pyx
+++ b/pandas/_libs/tslibs/offsets.pyx
@@ -756,11 +756,14 @@ cdef class BaseOffset:
         raise ValueError(f"{self} is a non-fixed frequency")
 
     def is_anchored(self) -> bool:
-        # TODO: Does this make sense for the general case?  It would help
-        # if there were a canonical docstring for what is_anchored means.
+        # GH#55388
         """
         Return boolean whether the frequency is a unit frequency (n=1).
 
+        .. deprecated:: 2.2.0
+            is_anchored is deprecated and will be removed in a future version.
+            Use ``obj.n == 1`` instead.
+
         Examples
         --------
         >>> pd.DateOffset().is_anchored()
@@ -768,6 +771,12 @@ cdef class BaseOffset:
         >>> pd.DateOffset(2).is_anchored()
         False
         """
+        warnings.warn(
+            f"{type(self).__name__}.is_anchored is deprecated and will be removed "
+            f"in a future version, please use \'obj.n == 1\' instead.",
+            FutureWarning,
+            stacklevel=find_stack_level(),
+        )
         return self.n == 1
 
     # ------------------------------------------------------------------
@@ -954,6 +963,27 @@ cdef class Tick(SingleConstructorOffset):
         return True
 
     def is_anchored(self) -> bool:
+        # GH#55388
+        """
+        Return False.
+
+        .. deprecated:: 2.2.0
+            is_anchored is deprecated and will be removed in a future version.
+            Use ``False`` instead.
+
+        Examples
+        --------
+        >>> pd.offsets.Hour().is_anchored()
+        False
+        >>> pd.offsets.Hour(2).is_anchored()
+        False
+        """
+        warnings.warn(
+            f"{type(self).__name__}.is_anchored is deprecated and will be removed "
+            f"in a future version, please use False instead.",
+            FutureWarning,
+            stacklevel=find_stack_level(),
+        )
         return False
 
     # This is identical to BaseOffset.__hash__, but has to be redefined here
@@ -2663,6 +2693,13 @@ cdef class QuarterOffset(SingleConstructorOffset):
         return f"{self._prefix}-{month}"
 
     def is_anchored(self) -> bool:
+        warnings.warn(
+            f"{type(self).__name__}.is_anchored is deprecated and will be removed "
+            f"in a future version, please use \'obj.n == 1 "
+            f"and obj.startingMonth is not None\' instead.",
+            FutureWarning,
+            stacklevel=find_stack_level(),
+        )
         return self.n == 1 and self.startingMonth is not None
 
     def is_on_offset(self, dt: datetime) -> bool:
@@ -3308,6 +3345,13 @@ cdef class Week(SingleConstructorOffset):
         self._cache = state.pop("_cache", {})
 
     def is_anchored(self) -> bool:
+        warnings.warn(
+            f"{type(self).__name__}.is_anchored is deprecated and will be removed "
+            f"in a future version, please use \'obj.n == 1 "
+            f"and obj.weekday is not None\' instead.",
+            FutureWarning,
+            stacklevel=find_stack_level(),
+        )
         return self.n == 1 and self.weekday is not None
 
     @apply_wraps
@@ -3597,6 +3641,12 @@ cdef class FY5253Mixin(SingleConstructorOffset):
         self.variation = state.pop("variation")
 
     def is_anchored(self) -> bool:
+        warnings.warn(
+            f"{type(self).__name__}.is_anchored is deprecated and will be removed "
+            f"in a future version, please use \'obj.n == 1\' instead.",
+            FutureWarning,
+            stacklevel=find_stack_level(),
+        )
         return (
             self.n == 1 and self.startingMonth is not None and self.weekday is not None
         )
diff --git a/pyproject.toml b/pyproject.toml
index 8724a25909..2f70ade7b3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,9 +30,9 @@ authors = [
 license = {file = 'LICENSE'}
 requires-python = '>=3.9'
 dependencies = [
-  "numpy>=1.22.4; python_version<'3.11'",
-  "numpy>=1.23.2; python_version=='3.11'",
-  "numpy>=1.26.0; python_version>='3.12'",
+  "numpy>=1.22.4,<2; python_version<'3.11'",
+  "numpy>=1.23.2,<2; python_version=='3.11'",
+  "numpy>=1.26.0,<2; python_version>='3.12'",
   "python-dateutil>=2.8.2",
   "pytz>=2020.1",
   "tzdata>=2022.7"


Test patch:
diff --git a/pandas/tests/indexes/interval/test_interval_range.py b/pandas/tests/indexes/interval/test_interval_range.py
index d4d4a09c44..e8de59f84b 100644
--- a/pandas/tests/indexes/interval/test_interval_range.py
+++ b/pandas/tests/indexes/interval/test_interval_range.py
@@ -84,9 +84,7 @@ class TestIntervalRange:
         tm.assert_index_equal(result, expected)
 
         # GH 20976: linspace behavior defined from start/end/periods
-        if not breaks.freq.is_anchored() and tz is None:
-            # matches expected only for non-anchored offsets and tz naive
-            # (anchored/DST transitions cause unequal spacing in expected)
+        if not breaks.freq.n == 1 and tz is None:
             result = interval_range(
                 start=start, end=end, periods=periods, name=name, closed=closed
             )
diff --git a/pandas/tests/tseries/offsets/test_business_quarter.py b/pandas/tests/tseries/offsets/test_business_quarter.py
index 44a7f16ab0..6d7a115054 100644
--- a/pandas/tests/tseries/offsets/test_business_quarter.py
+++ b/pandas/tests/tseries/offsets/test_business_quarter.py
@@ -9,6 +9,7 @@ from datetime import datetime
 
 import pytest
 
+import pandas._testing as tm
 from pandas.tests.tseries.offsets.common import (
     assert_is_on_offset,
     assert_offset_equal,
@@ -54,9 +55,12 @@ class TestBQuarterBegin:
         assert repr(BQuarterBegin(startingMonth=1)) == expected
 
     def test_is_anchored(self):
-        assert BQuarterBegin(startingMonth=1).is_anchored()
-        assert BQuarterBegin().is_anchored()
-        assert not BQuarterBegin(2, startingMonth=1).is_anchored()
+        msg = "BQuarterBegin.is_anchored is deprecated "
+
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            assert BQuarterBegin(startingMonth=1).is_anchored()
+            assert BQuarterBegin().is_anchored()
+            assert not BQuarterBegin(2, startingMonth=1).is_anchored()
 
     def test_offset_corner_case(self):
         # corner
@@ -177,9 +181,12 @@ class TestBQuarterEnd:
         assert repr(BQuarterEnd(startingMonth=1)) == expected
 
     def test_is_anchored(self):
-        assert BQuarterEnd(startingMonth=1).is_anchored()
-        assert BQuarterEnd().is_anchored()
-        assert not BQuarterEnd(2, startingMonth=1).is_anchored()
+        msg = "BQuarterEnd.is_anchored is deprecated "
+
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            assert BQuarterEnd(startingMonth=1).is_anchored()
+            assert BQuarterEnd().is_anchored()
+            assert not BQuarterEnd(2, startingMonth=1).is_anchored()
 
     def test_offset_corner_case(self):
         # corner
diff --git a/pandas/tests/tseries/offsets/test_fiscal.py b/pandas/tests/tseries/offsets/test_fiscal.py
index 7f8c34bc68..824e66a1dd 100644
--- a/pandas/tests/tseries/offsets/test_fiscal.py
+++ b/pandas/tests/tseries/offsets/test_fiscal.py
@@ -7,6 +7,7 @@ from dateutil.relativedelta import relativedelta
 import pytest
 
 from pandas import Timestamp
+import pandas._testing as tm
 from pandas.tests.tseries.offsets.common import (
     WeekDay,
     assert_is_on_offset,
@@ -295,15 +296,18 @@ class TestFY5253NearestEndMonth:
 
 class TestFY5253LastOfMonthQuarter:
     def test_is_anchored(self):
-        assert makeFY5253LastOfMonthQuarter(
-            startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4
-        ).is_anchored()
-        assert makeFY5253LastOfMonthQuarter(
-            weekday=WeekDay.SAT, startingMonth=3, qtr_with_extra_week=4
-        ).is_anchored()
-        assert not makeFY5253LastOfMonthQuarter(
-            2, startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4
-        ).is_anchored()
+        msg = "FY5253Quarter.is_anchored is deprecated "
+
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            assert makeFY5253LastOfMonthQuarter(
+                startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4
+            ).is_anchored()
+            assert makeFY5253LastOfMonthQuarter(
+                weekday=WeekDay.SAT, startingMonth=3, qtr_with_extra_week=4
+            ).is_anchored()
+            assert not makeFY5253LastOfMonthQuarter(
+                2, startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4
+            ).is_anchored()
 
     def test_equality(self):
         assert makeFY5253LastOfMonthQuarter(
diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py
index ddf56e68b1..62afb8b83d 100644
--- a/pandas/tests/tseries/offsets/test_offsets.py
+++ b/pandas/tests/tseries/offsets/test_offsets.py
@@ -625,8 +625,11 @@ class TestDateOffset:
         assert (dt + DateOffset(2)) == datetime(2008, 1, 4)
 
     def test_is_anchored(self):
-        assert not DateOffset(2).is_anchored()
-        assert DateOffset(1).is_anchored()
+        msg = "DateOffset.is_anchored is deprecated "
+
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            assert not DateOffset(2).is_anchored()
+            assert DateOffset(1).is_anchored()
 
     def test_copy(self):
         assert DateOffset(months=2).copy() == DateOffset(months=2)
diff --git a/pandas/tests/tseries/offsets/test_quarter.py b/pandas/tests/tseries/offsets/test_quarter.py
index d183645da5..5fd3ba0a5f 100644
--- a/pandas/tests/tseries/offsets/test_quarter.py
+++ b/pandas/tests/tseries/offsets/test_quarter.py
@@ -9,6 +9,7 @@ from datetime import datetime
 
 import pytest
 
+import pandas._testing as tm
 from pandas.tests.tseries.offsets.common import (
     assert_is_on_offset,
     assert_offset_equal,
@@ -53,9 +54,12 @@ class TestQuarterBegin:
         assert repr(QuarterBegin(startingMonth=1)) == expected
 
     def test_is_anchored(self):
-        assert QuarterBegin(startingMonth=1).is_anchored()
-        assert QuarterBegin().is_anchored()
-        assert not QuarterBegin(2, startingMonth=1).is_anchored()
+        msg = "QuarterBegin.is_anchored is deprecated "
+
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            assert QuarterBegin(startingMonth=1).is_anchored()
+            assert QuarterBegin().is_anchored()
+            assert not QuarterBegin(2, startingMonth=1).is_anchored()
 
     def test_offset_corner_case(self):
         # corner
@@ -161,9 +165,12 @@ class TestQuarterEnd:
         assert repr(QuarterEnd(startingMonth=1)) == expected
 
     def test_is_anchored(self):
-        assert QuarterEnd(startingMonth=1).is_anchored()
-        assert QuarterEnd().is_anchored()
-        assert not QuarterEnd(2, startingMonth=1).is_anchored()
+        msg = "QuarterEnd.is_anchored is deprecated "
+
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            assert QuarterEnd(startingMonth=1).is_anchored()
+            assert QuarterEnd().is_anchored()
+            assert not QuarterEnd(2, startingMonth=1).is_anchored()
 
     def test_offset_corner_case(self):
         # corner
diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py
index b68b91826b..399b7038d3 100644
--- a/pandas/tests/tseries/offsets/test_ticks.py
+++ b/pandas/tests/tseries/offsets/test_ticks.py
@@ -339,7 +339,10 @@ def test_tick_equalities(cls):
 
 @pytest.mark.parametrize("cls", tick_classes)
 def test_tick_offset(cls):
-    assert not cls().is_anchored()
+    msg = f"{cls.__name__}.is_anchored is deprecated "
+
+    with tm.assert_produces_warning(FutureWarning, match=msg):
+        assert not cls().is_anchored()
 
 
 @pytest.mark.parametrize("cls", tick_classes)
diff --git a/pandas/tests/tseries/offsets/test_week.py b/pandas/tests/tseries/offsets/test_week.py
index f42ff091af..0cd6f76976 100644
--- a/pandas/tests/tseries/offsets/test_week.py
+++ b/pandas/tests/tseries/offsets/test_week.py
@@ -21,6 +21,7 @@ from pandas._libs.tslibs.offsets import (
     WeekOfMonth,
 )
 
+import pandas._testing as tm
 from pandas.tests.tseries.offsets.common import (
     WeekDay,
     assert_is_on_offset,
@@ -42,10 +43,13 @@ class TestWeek:
             Week(weekday=-1)
 
     def test_is_anchored(self):
-        assert Week(weekday=0).is_anchored()
-        assert not Week().is_anchored()
-        assert not Week(2, weekday=2).is_anchored()
-        assert not Week(2).is_anchored()
+        msg = "Week.is_anchored is deprecated "
+
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            assert Week(weekday=0).is_anchored()
+            assert not Week().is_anchored()
+            assert not Week(2, weekday=2).is_anchored()
+            assert not Week(2).is_anchored()
 
     offset_cases = []
     # not business week