From 7a28708c90a0b1e09e5d3431a57410fc3512fb07 Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 4 Apr 2023 12:52:43 -0700 Subject: [PATCH 1/4] DEPR: allowing non-class returns for _constructor etc --- doc/source/whatsnew/v2.1.0.rst | 1 + pandas/_testing/__init__.py | 45 ++++++++-------- pandas/core/frame.py | 17 ++++++ pandas/core/series.py | 17 ++++++ pandas/tests/frame/test_arithmetic.py | 38 +++++++------- pandas/tests/frame/test_subclass.py | 52 +++++++++++-------- pandas/tests/reshape/merge/test_merge.py | 11 ++-- .../tests/reshape/merge/test_merge_ordered.py | 11 ++-- pandas/tests/series/methods/test_to_frame.py | 10 ++-- pandas/tests/series/test_arithmetic.py | 14 +++-- 10 files changed, 137 insertions(+), 79 deletions(-) diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 024f17d36e067..38f0d250ea1e3 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -170,6 +170,7 @@ Deprecations - Deprecated passing a dictionary to :meth:`.SeriesGroupBy.agg`; pass a list of aggregations instead (:issue:`50684`) - Deprecated logical operations (``|``, ``&``, ``^``) between pandas objects and dtype-less sequences (e.g. ``list``, ``tuple``), wrap a sequence in a :class:`Series` or numpy array before operating instead (:issue:`51521`) - Deprecated the methods :meth:`Series.bool` and :meth:`DataFrame.bool` (:issue:`51749`) +- 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 :meth:`DataFrame.swapaxes` and :meth:`Series.swapaxes`, use :meth:`DataFrame.transpose` or :meth:`Series.transpose` instead (:issue:`51946`) - Deprecated parameter ``convert_type`` in :meth:`Series.apply` (:issue:`52140`) - diff --git a/pandas/_testing/__init__.py b/pandas/_testing/__init__.py index 410cf7c6cbe3a..12749fcf46542 100644 --- a/pandas/_testing/__init__.py +++ b/pandas/_testing/__init__.py @@ -16,6 +16,7 @@ Iterable, cast, ) +import warnings import numpy as np @@ -823,33 +824,35 @@ def makeMissingDataframe(density: float = 0.9, random_state=None) -> DataFrame: return df -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 74873abac0758..4973a0bc1e013 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -645,6 +645,23 @@ def _constructor(self) -> Callable[..., DataFrame]: _constructor_sliced: Callable[..., Series] = Series + def __init_subclass__(cls, /, **kwargs): + 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 22c8d8b047280..80ab90b014e53 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -579,6 +579,23 @@ def _init_dict( # ---------------------------------------------------------------------- + def __init_subclass__(cls, /, **kwargs): + 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 090b3d64e7c41..d66b652021cf4 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -2044,30 +2044,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 5c44a957b9373..ceb7e0c268bc5 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -15,10 +15,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]}) @@ -28,31 +31,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 6f2b327c37067..4038cace33f5e 100644 --- a/pandas/tests/reshape/merge/test_merge.py +++ b/pandas/tests/reshape/merge/test_merge.py @@ -664,10 +664,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 d0d75ded14693..6693cd2d580fc 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -79,12 +79,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) From 3d9d7cd14b90ef6faf13edfc751054fcd97fdedf Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 4 Apr 2023 13:59:10 -0700 Subject: [PATCH 2/4] lint fixup --- pandas/core/frame.py | 2 +- pandas/core/series.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 4973a0bc1e013..6706e8347d824 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -645,7 +645,7 @@ def _constructor(self) -> Callable[..., DataFrame]: _constructor_sliced: Callable[..., Series] = Series - def __init_subclass__(cls, /, **kwargs): + def __init_subclass__(cls, /, **kwargs) -> None: super().__init_subclass__(**kwargs) if cls._constructor is not DataFrame._constructor: warnings.warn( diff --git a/pandas/core/series.py b/pandas/core/series.py index 80ab90b014e53..3494979cb3068 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -579,7 +579,7 @@ def _init_dict( # ---------------------------------------------------------------------- - def __init_subclass__(cls, /, **kwargs): + def __init_subclass__(cls, /, **kwargs) -> None: super().__init_subclass__(**kwargs) if cls._constructor is not Series._constructor: warnings.warn( From 9968cf997e19585ba2768392de3ddfc621faaf8e Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 11 Apr 2023 14:10:49 -0700 Subject: [PATCH 3/4] "fix new test" --- pandas/tests/series/test_subclass.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) 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(): From d7c35106724b2a6be383b037eeebbcc6090013fa Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 1 Sep 2023 10:28:59 -0700 Subject: [PATCH 4/4] Merge branch 'main' into depr-constructor