Skip to content

Commit

Permalink
Backport PR #57553 on branch 2.2.x (API: avoid passing Manager to sub…
Browse files Browse the repository at this point in the history
…class init) (#58008)

* Backport PR #57553: API: avoid passing Manager to subclass __init__

* whatsnew, type ignores

* merge 2.2.2 file from main

* rebase on 2.2.x whatsnew
  • Loading branch information
jbrockmendel authored Apr 1, 2024
1 parent f455401 commit 810b2d0
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 32 deletions.
45 changes: 29 additions & 16 deletions pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,26 +656,37 @@ class DataFrame(NDFrame, OpsMixin):
def _constructor(self) -> Callable[..., DataFrame]:
return DataFrame

def _constructor_from_mgr(self, mgr, axes):
if self._constructor is DataFrame:
# we are pandas.DataFrame (or a subclass that doesn't override _constructor)
return DataFrame._from_mgr(mgr, axes=axes)
else:
assert axes is mgr.axes
def _constructor_from_mgr(self, mgr, axes) -> DataFrame:
df = DataFrame._from_mgr(mgr, axes=axes)

if type(self) is DataFrame:
# This would also work `if self._constructor is DataFrame`, but
# this check is slightly faster, benefiting the most-common case.
return df

elif type(self).__name__ == "GeoDataFrame":
# Shim until geopandas can override their _constructor_from_mgr
# bc they have different behavior for Managers than for DataFrames
return self._constructor(mgr)

# We assume that the subclass __init__ knows how to handle a
# pd.DataFrame object.
return self._constructor(df)

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

def _sliced_from_mgr(self, mgr, axes) -> Series:
return Series._from_mgr(mgr, axes)
def _constructor_sliced_from_mgr(self, mgr, axes) -> Series:
ser = Series._from_mgr(mgr, axes)
ser._name = None # caller is responsible for setting real name

def _constructor_sliced_from_mgr(self, mgr, axes):
if self._constructor_sliced is Series:
ser = self._sliced_from_mgr(mgr, axes)
ser._name = None # caller is responsible for setting real name
if type(self) is DataFrame:
# This would also work `if self._constructor_sliced is Series`, but
# this check is slightly faster, benefiting the most-common case.
return ser
assert axes is mgr.axes
return self._constructor_sliced(mgr)

# We assume that the subclass __init__ knows how to handle a
# pd.Series object.
return self._constructor_sliced(ser)

# ----------------------------------------------------------------------
# Constructors
Expand Down Expand Up @@ -1403,7 +1414,8 @@ def _get_values_for_csv(
na_rep=na_rep,
quoting=quoting,
)
return self._constructor_from_mgr(mgr, axes=mgr.axes)
# error: Incompatible return value type (got "DataFrame", expected "Self")
return self._constructor_from_mgr(mgr, axes=mgr.axes) # type: ignore[return-value]

# ----------------------------------------------------------------------

Expand Down Expand Up @@ -5077,7 +5089,8 @@ def predicate(arr: ArrayLike) -> bool:
return True

mgr = self._mgr._get_data_subset(predicate).copy(deep=None)
return self._constructor_from_mgr(mgr, axes=mgr.axes).__finalize__(self)
# error: Incompatible return value type (got "DataFrame", expected "Self")
return self._constructor_from_mgr(mgr, axes=mgr.axes).__finalize__(self) # type: ignore[return-value]

def insert(
self,
Expand Down
1 change: 1 addition & 0 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ def _as_manager(self, typ: str, copy: bool_t = True) -> Self:
# fastpath of passing a manager doesn't check the option/manager class
return self._constructor_from_mgr(new_mgr, axes=new_mgr.axes).__finalize__(self)

@final
@classmethod
def _from_mgr(cls, mgr: Manager, axes: list[Index]) -> Self:
"""
Expand Down
3 changes: 2 additions & 1 deletion pandas/core/resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -2548,7 +2548,8 @@ def _take_new_index(
if axis == 1:
raise NotImplementedError("axis 1 is not supported")
new_mgr = obj._mgr.reindex_indexer(new_axis=new_index, indexer=indexer, axis=1)
return obj._constructor_from_mgr(new_mgr, axes=new_mgr.axes)
# error: Incompatible return value type (got "DataFrame", expected "NDFrameT")
return obj._constructor_from_mgr(new_mgr, axes=new_mgr.axes) # type: ignore[return-value]
else:
raise ValueError("'obj' should be either a Series or a DataFrame")

Expand Down
34 changes: 19 additions & 15 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,14 +662,17 @@ def _constructor(self) -> Callable[..., Series]:
return Series

def _constructor_from_mgr(self, mgr, axes):
if self._constructor is Series:
# we are pandas.Series (or a subclass that doesn't override _constructor)
ser = Series._from_mgr(mgr, axes=axes)
ser._name = None # caller is responsible for setting real name
ser = Series._from_mgr(mgr, axes=axes)
ser._name = None # caller is responsible for setting real name

if type(self) is Series:
# This would also work `if self._constructor is Series`, but
# this check is slightly faster, benefiting the most-common case.
return ser
else:
assert axes is mgr.axes
return self._constructor(mgr)

# We assume that the subclass __init__ knows how to handle a
# pd.Series object.
return self._constructor(ser)

@property
def _constructor_expanddim(self) -> Callable[..., DataFrame]:
Expand All @@ -681,18 +684,19 @@ def _constructor_expanddim(self) -> Callable[..., DataFrame]:

return DataFrame

def _expanddim_from_mgr(self, mgr, axes) -> DataFrame:
def _constructor_expanddim_from_mgr(self, mgr, axes):
from pandas.core.frame import DataFrame

return DataFrame._from_mgr(mgr, axes=mgr.axes)
df = DataFrame._from_mgr(mgr, axes=mgr.axes)

def _constructor_expanddim_from_mgr(self, mgr, axes):
from pandas.core.frame import DataFrame
if type(self) is Series:
# This would also work `if self._constructor_expanddim is DataFrame`,
# but this check is slightly faster, benefiting the most-common case.
return df

if self._constructor_expanddim is DataFrame:
return self._expanddim_from_mgr(mgr, axes)
assert axes is mgr.axes
return self._constructor_expanddim(mgr)
# We assume that the subclass __init__ knows how to handle a
# pd.DataFrame object.
return self._constructor_expanddim(df)

# types
@property
Expand Down
11 changes: 11 additions & 0 deletions pandas/tests/frame/test_subclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ def _constructor(self):


class TestDataFrameSubclassing:
def test_no_warning_on_mgr(self):
# GH#57032
df = tm.SubclassedDataFrame(
{"X": [1, 2, 3], "Y": [1, 2, 3]}, index=["a", "b", "c"]
)
with tm.assert_produces_warning(None):
# df.isna() goes through _constructor_from_mgr, which we want to
# *not* pass a Manager do __init__
df.isna()
df["X"].isna()

def test_frame_subclassing_and_slicing(self):
# Subclass frame and ensure it returns the right class on slicing it
# In reference to PR 9632
Expand Down

0 comments on commit 810b2d0

Please sign in to comment.