Skip to content

Commit cabfec3

Browse files
Backport PR pandas-dev#46018: BUG: remove usage of _constructor._get_axis_number (fix when _constructor properties return callables) (pandas-dev#46228)
Co-authored-by: Joris Van den Bossche <[email protected]>
1 parent 2627a2f commit cabfec3

File tree

9 files changed

+38
-35
lines changed

9 files changed

+38
-35
lines changed

doc/source/whatsnew/v1.4.2.rst

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Fixed regressions
2323

2424
Bug fixes
2525
~~~~~~~~~
26+
- Fix some cases for subclasses that define their ``_constructor`` properties as general callables (:issue:`46018`)
2627
- Fixed "longtable" formatting in :meth:`.Styler.to_latex` when ``column_format`` is given in extended format (:issue:`46037`)
2728
-
2829

pandas/_testing/__init__.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -875,23 +875,28 @@ class SubclassedSeries(Series):
875875

876876
@property
877877
def _constructor(self):
878-
return SubclassedSeries
878+
# For testing, those properties return a generic callable, and not
879+
# the actual class. In this case that is equivalent, but it is to
880+
# ensure we don't rely on the property returning a class
881+
# See https://github.com/pandas-dev/pandas/pull/46018 and
882+
# https://github.com/pandas-dev/pandas/issues/32638 and linked issues
883+
return lambda *args, **kwargs: SubclassedSeries(*args, **kwargs)
879884

880885
@property
881886
def _constructor_expanddim(self):
882-
return SubclassedDataFrame
887+
return lambda *args, **kwargs: SubclassedDataFrame(*args, **kwargs)
883888

884889

885890
class SubclassedDataFrame(DataFrame):
886891
_metadata = ["testattr"]
887892

888893
@property
889894
def _constructor(self):
890-
return SubclassedDataFrame
895+
return lambda *args, **kwargs: SubclassedDataFrame(*args, **kwargs)
891896

892897
@property
893898
def _constructor_sliced(self):
894-
return SubclassedSeries
899+
return lambda *args, **kwargs: SubclassedSeries(*args, **kwargs)
895900

896901

897902
class SubclassedCategorical(Categorical):

pandas/core/frame.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -576,10 +576,10 @@ class DataFrame(NDFrame, OpsMixin):
576576
_mgr: BlockManager | ArrayManager
577577

578578
@property
579-
def _constructor(self) -> type[DataFrame]:
579+
def _constructor(self) -> Callable[..., DataFrame]:
580580
return DataFrame
581581

582-
_constructor_sliced: type[Series] = Series
582+
_constructor_sliced: Callable[..., Series] = Series
583583

584584
# ----------------------------------------------------------------------
585585
# Constructors

pandas/core/generic.py

+8-22
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ def _validate_dtype(cls, dtype) -> DtypeObj | None:
458458
# Construction
459459

460460
@property
461-
def _constructor(self: NDFrameT) -> type[NDFrameT]:
461+
def _constructor(self: NDFrameT) -> Callable[..., NDFrameT]:
462462
"""
463463
Used when a manipulation result has the same dimensions as the
464464
original.
@@ -793,17 +793,9 @@ def swapaxes(self: NDFrameT, axis1, axis2, copy=True) -> NDFrameT:
793793
if copy:
794794
new_values = new_values.copy()
795795

796-
# ignore needed because of NDFrame constructor is different than
797-
# DataFrame/Series constructors.
798796
return self._constructor(
799-
# error: Argument 1 to "NDFrame" has incompatible type "ndarray"; expected
800-
# "Union[ArrayManager, BlockManager]"
801-
# error: Argument 2 to "NDFrame" has incompatible type "*Generator[Index,
802-
# None, None]"; expected "bool" [arg-type]
803-
# error: Argument 2 to "NDFrame" has incompatible type "*Generator[Index,
804-
# None, None]"; expected "Optional[Mapping[Hashable, Any]]"
805-
new_values, # type: ignore[arg-type]
806-
*new_axes, # type: ignore[arg-type]
797+
new_values,
798+
*new_axes,
807799
).__finalize__(self, method="swapaxes")
808800

809801
@final
@@ -2100,11 +2092,7 @@ def __array_wrap__(
21002092
# ptp also requires the item_from_zerodim
21012093
return res
21022094
d = self._construct_axes_dict(self._AXIS_ORDERS, copy=False)
2103-
# error: Argument 1 to "NDFrame" has incompatible type "ndarray";
2104-
# expected "BlockManager"
2105-
return self._constructor(res, **d).__finalize__( # type: ignore[arg-type]
2106-
self, method="__array_wrap__"
2107-
)
2095+
return self._constructor(res, **d).__finalize__(self, method="__array_wrap__")
21082096

21092097
@final
21102098
def __array_ufunc__(
@@ -6612,8 +6600,10 @@ def replace(
66126600

66136601
if isinstance(to_replace, (tuple, list)):
66146602
if isinstance(self, ABCDataFrame):
6603+
from pandas import Series
6604+
66156605
result = self.apply(
6616-
self._constructor_sliced._replace_single,
6606+
Series._replace_single,
66176607
args=(to_replace, method, inplace, limit),
66186608
)
66196609
if inplace:
@@ -9137,11 +9127,7 @@ def _where(
91379127

91389128
# we are the same shape, so create an actual object for alignment
91399129
else:
9140-
# error: Argument 1 to "NDFrame" has incompatible type "ndarray";
9141-
# expected "BlockManager"
9142-
other = self._constructor(
9143-
other, **self._construct_axes_dict() # type: ignore[arg-type]
9144-
)
9130+
other = self._constructor(other, **self._construct_axes_dict())
91459131

91469132
if axis is None:
91479133
axis = 0

pandas/core/groupby/groupby.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1733,7 +1733,7 @@ def _cumcount_array(self, ascending: bool = True) -> np.ndarray:
17331733

17341734
@final
17351735
@property
1736-
def _obj_1d_constructor(self) -> type[Series]:
1736+
def _obj_1d_constructor(self) -> Callable:
17371737
# GH28330 preserve subclassed Series/DataFrames
17381738
if isinstance(self.obj, DataFrame):
17391739
return self.obj._constructor_sliced
@@ -2151,7 +2151,7 @@ def size(self) -> DataFrame | Series:
21512151
result = self.grouper.size()
21522152

21532153
# GH28330 preserve subclassed Series/DataFrames through calls
2154-
if issubclass(self.obj._constructor, Series):
2154+
if isinstance(self.obj, Series):
21552155
result = self._obj_1d_constructor(result, name=self.obj.name)
21562156
else:
21572157
result = self._obj_1d_constructor(result)

pandas/core/reshape/concat.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from collections import abc
77
from typing import (
88
TYPE_CHECKING,
9+
Callable,
910
Hashable,
1011
Iterable,
1112
Literal,
@@ -467,7 +468,9 @@ def __init__(
467468

468469
# Standardize axis parameter to int
469470
if isinstance(sample, ABCSeries):
470-
axis = sample._constructor_expanddim._get_axis_number(axis)
471+
from pandas import DataFrame
472+
473+
axis = DataFrame._get_axis_number(axis)
471474
else:
472475
axis = sample._get_axis_number(axis)
473476

@@ -539,7 +542,7 @@ def __init__(
539542
self.new_axes = self._get_new_axes()
540543

541544
def get_result(self):
542-
cons: type[DataFrame | Series]
545+
cons: Callable[..., DataFrame | Series]
543546
sample: DataFrame | Series
544547

545548
# series only

pandas/core/series.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -521,11 +521,11 @@ def _init_dict(
521521
# ----------------------------------------------------------------------
522522

523523
@property
524-
def _constructor(self) -> type[Series]:
524+
def _constructor(self) -> Callable[..., Series]:
525525
return Series
526526

527527
@property
528-
def _constructor_expanddim(self) -> type[DataFrame]:
528+
def _constructor_expanddim(self) -> Callable[..., DataFrame]:
529529
"""
530530
Used when a manipulation result has one higher dimension as the
531531
original, such as Series.to_frame()

pandas/tests/frame/test_subclass.py

+8
Original file line numberDiff line numberDiff line change
@@ -730,3 +730,11 @@ def test_equals_subclass(self):
730730
df2 = tm.SubclassedDataFrame({"a": [1, 2, 3]})
731731
assert df1.equals(df2)
732732
assert df2.equals(df1)
733+
734+
def test_replace_list_method(self):
735+
# https://github.com/pandas-dev/pandas/pull/46018
736+
df = tm.SubclassedDataFrame({"A": [0, 1, 2]})
737+
result = df.replace([1, 2], method="ffill")
738+
expected = tm.SubclassedDataFrame({"A": [0, 0, 0]})
739+
assert isinstance(result, tm.SubclassedDataFrame)
740+
tm.assert_frame_equal(result, expected)

pandas/tests/groupby/test_groupby_subclass.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def test_groupby_preserves_subclass(obj, groupby_func):
4646
# Reduction or transformation kernels should preserve type
4747
slices = {"ngroup", "cumcount", "size"}
4848
if isinstance(obj, DataFrame) and groupby_func in slices:
49-
assert isinstance(result1, obj._constructor_sliced)
49+
assert isinstance(result1, tm.SubclassedSeries)
5050
else:
5151
assert isinstance(result1, type(obj))
5252

0 commit comments

Comments
 (0)