diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst index 93ca2541d7ecd..13b40089dc809 100644 --- a/doc/source/whatsnew/v2.2.0.rst +++ b/doc/source/whatsnew/v2.2.0.rst @@ -249,6 +249,7 @@ Other Deprecations - Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_pickle` except ``path``. (:issue:`54229`) - Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_string` except ``buf``. (:issue:`54229`) - Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_xml` except ``path_or_buffer``. (:issue:`54229`) +- Deprecated allowing subclasses :meth:`DataFrame._constructor`, :meth:`Series._constructor`, :meth:`DataFrame._constructor_sliced`, and :meth:`Series._constructor_expanddim` to return a function, in a future version these will be expected to return a class (:issue:`51772`) - Deprecated automatic downcasting of object-dtype results in :meth:`Series.replace` and :meth:`DataFrame.replace`, explicitly call ``result = result.infer_objects(copy=False)`` instead. To opt in to the future version, use ``pd.set_option("future.no_silent_downcasting", True)`` (:issue:`54710`) - Deprecated downcasting behavior in :meth:`Series.where`, :meth:`DataFrame.where`, :meth:`Series.mask`, :meth:`DataFrame.mask`, :meth:`Series.clip`, :meth:`DataFrame.clip`; in a future version these will not infer object-dtype columns to non-object dtype, or all-round floats to integer dtype. Call ``result.infer_objects(copy=False)`` on the result for object inference, or explicitly cast floats to ints. To opt in to the future version, use ``pd.set_option("future.no_silent_downcasting", True)`` (:issue:`53656`) - Deprecated including the groups in computations when using :meth:`DataFrameGroupBy.apply` and :meth:`DataFrameGroupBy.resample`; pass ``include_groups=False`` to exclude the groups (:issue:`7155`) diff --git a/pandas/_testing/__init__.py b/pandas/_testing/__init__.py index 73835252c0329..f941383ff1093 100644 --- a/pandas/_testing/__init__.py +++ b/pandas/_testing/__init__.py @@ -15,6 +15,7 @@ ContextManager, cast, ) +import warnings import numpy as np @@ -828,33 +829,35 @@ def makeCustomDataframe( return DataFrame(data, index, columns, dtype=dtype) -class SubclassedSeries(Series): - _metadata = ["testattr", "name"] +with warnings.catch_warnings(): + warnings.simplefilter("ignore") - @property - def _constructor(self): - # For testing, those properties return a generic callable, and not - # the actual class. In this case that is equivalent, but it is to - # ensure we don't rely on the property returning a class - # See https://github.com/pandas-dev/pandas/pull/46018 and - # https://github.com/pandas-dev/pandas/issues/32638 and linked issues - return lambda *args, **kwargs: SubclassedSeries(*args, **kwargs) + class SubclassedSeries(Series): + _metadata = ["testattr", "name"] - @property - def _constructor_expanddim(self): - return lambda *args, **kwargs: SubclassedDataFrame(*args, **kwargs) + @property + def _constructor(self): + # For testing, those properties return a generic callable, and not + # the actual class. In this case that is equivalent, but it is to + # ensure we don't rely on the property returning a class + # See https://github.com/pandas-dev/pandas/pull/46018 and + # https://github.com/pandas-dev/pandas/issues/32638 and linked issues + return lambda *args, **kwargs: SubclassedSeries(*args, **kwargs) + @property + def _constructor_expanddim(self): + return lambda *args, **kwargs: SubclassedDataFrame(*args, **kwargs) -class SubclassedDataFrame(DataFrame): - _metadata = ["testattr"] + class SubclassedDataFrame(DataFrame): + _metadata = ["testattr"] - @property - def _constructor(self): - return lambda *args, **kwargs: SubclassedDataFrame(*args, **kwargs) + @property + def _constructor(self): + return lambda *args, **kwargs: SubclassedDataFrame(*args, **kwargs) - @property - def _constructor_sliced(self): - return lambda *args, **kwargs: SubclassedSeries(*args, **kwargs) + @property + def _constructor_sliced(self): + return lambda *args, **kwargs: SubclassedSeries(*args, **kwargs) class SubclassedCategorical(Categorical): diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 09c43822e11e4..14f737d9a0396 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -666,6 +666,23 @@ def _constructor_sliced_from_mgr(self, mgr, axes): assert axes is mgr.axes return self._constructor_sliced(ser, copy=False) + def __init_subclass__(cls, /, **kwargs) -> None: + super().__init_subclass__(**kwargs) + if cls._constructor is not DataFrame._constructor: + warnings.warn( + "pandas subclass support is deprecating allowing _constructor " + "that does not return a class.", + FutureWarning, + stacklevel=find_stack_level(), + ) + if cls._constructor_sliced is not DataFrame._constructor_sliced: + warnings.warn( + "pandas subclass support is deprecating allowing " + "_constructor_sliced that does not return a class.", + FutureWarning, + stacklevel=find_stack_level(), + ) + # ---------------------------------------------------------------------- # Constructors diff --git a/pandas/core/series.py b/pandas/core/series.py index fdd03debf6de4..a06d854b3b829 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -583,6 +583,23 @@ def _init_dict( # ---------------------------------------------------------------------- + def __init_subclass__(cls, /, **kwargs) -> None: + super().__init_subclass__(**kwargs) + if cls._constructor is not Series._constructor: + warnings.warn( + "pandas subclass support is deprecating allowing _constructor " + "that does not return a class.", + FutureWarning, + stacklevel=find_stack_level(), + ) + if cls._constructor_expanddim is not Series._constructor_expanddim: + warnings.warn( + "pandas subclass support is deprecating allowing " + "_constructor_expanddim that does not return a class.", + FutureWarning, + stacklevel=find_stack_level(), + ) + @property def _constructor(self) -> Callable[..., Series]: return Series diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index 09a5cda4b3458..42eabf320889a 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -2085,30 +2085,32 @@ def test_frame_sub_nullable_int(any_int_ea_dtype): def test_frame_op_subclass_nonclass_constructor(): # GH#43201 subclass._constructor is a function, not the subclass itself + msg = "pandas subclass support is deprecating" + with tm.assert_produces_warning(FutureWarning, match=msg): - class SubclassedSeries(Series): - @property - def _constructor(self): - return SubclassedSeries + class SubclassedSeries(Series): + @property + def _constructor(self): + return SubclassedSeries - @property - def _constructor_expanddim(self): - return SubclassedDataFrame + @property + def _constructor_expanddim(self): + return SubclassedDataFrame - class SubclassedDataFrame(DataFrame): - _metadata = ["my_extra_data"] + class SubclassedDataFrame(DataFrame): + _metadata = ["my_extra_data"] - def __init__(self, my_extra_data, *args, **kwargs) -> None: - self.my_extra_data = my_extra_data - super().__init__(*args, **kwargs) + def __init__(self, my_extra_data, *args, **kwargs) -> None: + self.my_extra_data = my_extra_data + super().__init__(*args, **kwargs) - @property - def _constructor(self): - return functools.partial(type(self), self.my_extra_data) + @property + def _constructor(self): + return functools.partial(type(self), self.my_extra_data) - @property - def _constructor_sliced(self): - return SubclassedSeries + @property + def _constructor_sliced(self): + return SubclassedSeries sdf = SubclassedDataFrame("some_data", {"A": [1, 2, 3], "B": [4, 5, 6]}) result = sdf * 2 diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 37ca52eba6451..36c92efcafe73 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -13,10 +13,13 @@ @pytest.fixture() def gpd_style_subclass_df(): - class SubclassedDataFrame(DataFrame): - @property - def _constructor(self): - return SubclassedDataFrame + msg = "pandas subclass support is deprecating" + with tm.assert_produces_warning(FutureWarning, match=msg): + + class SubclassedDataFrame(DataFrame): + @property + def _constructor(self): + return SubclassedDataFrame return SubclassedDataFrame({"a": [1, 2, 3]}) @@ -26,31 +29,34 @@ def test_frame_subclassing_and_slicing(self): # Subclass frame and ensure it returns the right class on slicing it # In reference to PR 9632 - class CustomSeries(Series): - @property - def _constructor(self): - return CustomSeries + msg = "pandas subclass support is deprecating" + with tm.assert_produces_warning(FutureWarning, match=msg): - def custom_series_function(self): - return "OK" + class CustomSeries(Series): + @property + def _constructor(self): + return CustomSeries - class CustomDataFrame(DataFrame): - """ - Subclasses pandas DF, fills DF with simulation results, adds some - custom plotting functions. - """ + def custom_series_function(self): + return "OK" - def __init__(self, *args, **kw) -> None: - super().__init__(*args, **kw) + class CustomDataFrame(DataFrame): + """ + Subclasses pandas DF, fills DF with simulation results, adds some + custom plotting functions. + """ - @property - def _constructor(self): - return CustomDataFrame + def __init__(self, *args, **kw) -> None: + super().__init__(*args, **kw) + + @property + def _constructor(self): + return CustomDataFrame - _constructor_sliced = CustomSeries + _constructor_sliced = CustomSeries - def custom_frame_function(self): - return "OK" + def custom_frame_function(self): + return "OK" data = {"col1": range(10), "col2": range(10)} cdf = CustomDataFrame(data) diff --git a/pandas/tests/reshape/merge/test_merge.py b/pandas/tests/reshape/merge/test_merge.py index 4d779349b5c14..d44d3a36140ec 100644 --- a/pandas/tests/reshape/merge/test_merge.py +++ b/pandas/tests/reshape/merge/test_merge.py @@ -689,10 +689,13 @@ def test_merge_nan_right2(self): tm.assert_frame_equal(result, expected) def test_merge_type(self, df, df2): - class NotADataFrame(DataFrame): - @property - def _constructor(self): - return NotADataFrame + msg = "pandas subclass support is deprecating" + with tm.assert_produces_warning(FutureWarning, match=msg): + + class NotADataFrame(DataFrame): + @property + def _constructor(self): + return NotADataFrame nad = NotADataFrame(df) result = nad.merge(df2, on="key1") diff --git a/pandas/tests/reshape/merge/test_merge_ordered.py b/pandas/tests/reshape/merge/test_merge_ordered.py index 1d0d4e3eb554b..c30b173c6c21e 100644 --- a/pandas/tests/reshape/merge/test_merge_ordered.py +++ b/pandas/tests/reshape/merge/test_merge_ordered.py @@ -71,10 +71,13 @@ def test_multigroup(self, left, right): assert result["group"].notna().all() def test_merge_type(self, left, right): - class NotADataFrame(DataFrame): - @property - def _constructor(self): - return NotADataFrame + msg = "pandas subclass support is deprecating" + with tm.assert_produces_warning(FutureWarning, match=msg): + + class NotADataFrame(DataFrame): + @property + def _constructor(self): + return NotADataFrame nad = NotADataFrame(left) result = nad.merge(right, on="key") diff --git a/pandas/tests/series/methods/test_to_frame.py b/pandas/tests/series/methods/test_to_frame.py index 01e547aa34b47..25ee1f18d2c87 100644 --- a/pandas/tests/series/methods/test_to_frame.py +++ b/pandas/tests/series/methods/test_to_frame.py @@ -42,11 +42,13 @@ def test_to_frame(self, datetime_series): def test_to_frame_expanddim(self): # GH#9762 + msg = "pandas subclass support is deprecating" + with tm.assert_produces_warning(FutureWarning, match=msg): - class SubclassedSeries(Series): - @property - def _constructor_expanddim(self): - return SubclassedFrame + class SubclassedSeries(Series): + @property + def _constructor_expanddim(self): + return SubclassedFrame class SubclassedFrame(DataFrame): pass diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index e9eb906a9cf10..058be9898f6c2 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -75,12 +75,16 @@ def test_flex_method_equivalence(self, opname, ts): def test_flex_method_subclass_metadata_preservation(self, all_arithmetic_operators): # GH 13208 - class MySeries(Series): - _metadata = ["x"] - @property - def _constructor(self): - return MySeries + msg = "pandas subclass support is deprecating" + with tm.assert_produces_warning(FutureWarning, match=msg): + + class MySeries(Series): + _metadata = ["x"] + + @property + def _constructor(self): + return MySeries opname = all_arithmetic_operators op = getattr(Series, opname) diff --git a/pandas/tests/series/test_subclass.py b/pandas/tests/series/test_subclass.py index a3550c6de6780..0290ca6db2236 100644 --- a/pandas/tests/series/test_subclass.py +++ b/pandas/tests/series/test_subclass.py @@ -1,3 +1,5 @@ +import warnings + import numpy as np import pytest @@ -60,16 +62,19 @@ def test_equals(self): assert s2.equals(s1) -class SubclassedSeries(pd.Series): - @property - def _constructor(self): - def _new(*args, **kwargs): - # some constructor logic that accesses the Series' name - if self.name == "test": - return pd.Series(*args, **kwargs) - return SubclassedSeries(*args, **kwargs) +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + class SubclassedSeries(pd.Series): + @property + def _constructor(self): + def _new(*args, **kwargs): + # some constructor logic that accesses the Series' name + if self.name == "test": + return pd.Series(*args, **kwargs) + return SubclassedSeries(*args, **kwargs) - return _new + return _new def test_constructor_from_dict():