Skip to content

DEPR: allowing non-class returns for _constructor etc #52420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7a28708
DEPR: allowing non-class returns for _constructor etc
jbrockmendel Apr 4, 2023
3d9d7cd
lint fixup
jbrockmendel Apr 4, 2023
894ad3c
Merge branch 'main' into depr-constructor
jbrockmendel Apr 11, 2023
9968cf9
"fix new test"
jbrockmendel Apr 11, 2023
373a7c7
Merge branch 'main' into depr-constructor
jbrockmendel Apr 13, 2023
9756d5e
Merge branch 'main' into depr-constructor
jbrockmendel Apr 19, 2023
eb39b47
Merge branch 'main' into depr-constructor
jbrockmendel Apr 19, 2023
c259ac8
Merge branch 'main' into depr-constructor
jbrockmendel Apr 20, 2023
0b46c98
Merge branch 'main' into depr-constructor
jbrockmendel Apr 21, 2023
45c9218
Merge branch 'main' into depr-constructor
jbrockmendel Apr 28, 2023
0d01eea
Merge branch 'main' into depr-constructor
jbrockmendel May 4, 2023
918a707
Merge branch 'main' into depr-constructor
jbrockmendel May 8, 2023
24a9041
Merge branch 'main' into depr-constructor
jbrockmendel Jul 18, 2023
0afc695
Merge branch 'main' into depr-constructor
jbrockmendel Jul 26, 2023
10ded72
Merge branch 'main' into depr-constructor
jbrockmendel Jul 27, 2023
90cb220
Merge branch 'main' into depr-constructor
jbrockmendel Jul 28, 2023
c824432
Merge branch 'main' into depr-constructor
jbrockmendel Aug 23, 2023
d1f48d8
Merge branch 'main' into depr-constructor
jbrockmendel Aug 31, 2023
99d1770
Merge branch 'main' into depr-constructor
jbrockmendel Sep 1, 2023
521402c
Merge branch 'main' into depr-constructor
jbrockmendel Sep 1, 2023
d7c3510
Merge branch 'main' into depr-constructor
jbrockmendel Sep 1, 2023
97bbb51
Merge branch 'main' into depr-constructor
jbrockmendel Sep 18, 2023
9cd4be4
Merge branch 'main' into depr-constructor
jbrockmendel Oct 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v2.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
-
Expand Down
45 changes: 24 additions & 21 deletions pandas/_testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Iterable,
cast,
)
import warnings

import numpy as np

Expand Down Expand Up @@ -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):
Expand Down
17 changes: 17 additions & 0 deletions pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,23 @@ def _constructor(self) -> Callable[..., DataFrame]:

_constructor_sliced: Callable[..., Series] = Series

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(),
)
Comment on lines +672 to +677
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this raise a warning all the time, with no option to silence it?

just wondering if there's a way to do this without having to always raise a warning, and if not, if the warning should advise to just use filterwarnings to suppress the message because there's no workaround

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this raise a warning all the time, with no option to silence it?

Yes. I tried having it check and only raise if _constructor_foo doesn't return a class but couldn't get that working.

just wondering if there's a way to do this without having to always raise a warning, and if not, if the warning should advise to just use filterwarnings to suppress the message because there's no workaround

Makes sense. Or as mentioned in the OP this could be doc-only.

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

Expand Down
17 changes: 17 additions & 0 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,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
Expand Down
38 changes: 20 additions & 18 deletions pandas/tests/frame/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 29 additions & 23 deletions pandas/tests/frame/test_subclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]})

Expand All @@ -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)
Expand Down
11 changes: 7 additions & 4 deletions pandas/tests/reshape/merge/test_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
11 changes: 7 additions & 4 deletions pandas/tests/reshape/merge/test_merge_ordered.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 6 additions & 4 deletions pandas/tests/series/methods/test_to_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions pandas/tests/series/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down