Skip to content

Commit b3ec1af

Browse files
BUG: remove usage of _constructor._get_axis_number (fix when _constructor properties return callables) (pandas-dev#46018)
1 parent 1ada3b7 commit b3ec1af

File tree

9 files changed

+41
-40
lines changed

9 files changed

+41
-40
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
@@ -803,23 +803,28 @@ class SubclassedSeries(Series):
803803

804804
@property
805805
def _constructor(self):
806-
return SubclassedSeries
806+
# For testing, those properties return a generic callable, and not
807+
# the actual class. In this case that is equivalent, but it is to
808+
# ensure we don't rely on the property returning a class
809+
# See https://github.com/pandas-dev/pandas/pull/46018 and
810+
# https://github.com/pandas-dev/pandas/issues/32638 and linked issues
811+
return lambda *args, **kwargs: SubclassedSeries(*args, **kwargs)
807812

808813
@property
809814
def _constructor_expanddim(self):
810-
return SubclassedDataFrame
815+
return lambda *args, **kwargs: SubclassedDataFrame(*args, **kwargs)
811816

812817

813818
class SubclassedDataFrame(DataFrame):
814819
_metadata = ["testattr"]
815820

816821
@property
817822
def _constructor(self):
818-
return SubclassedDataFrame
823+
return lambda *args, **kwargs: SubclassedDataFrame(*args, **kwargs)
819824

820825
@property
821826
def _constructor_sliced(self):
822-
return SubclassedSeries
827+
return lambda *args, **kwargs: SubclassedSeries(*args, **kwargs)
823828

824829

825830
class SubclassedCategorical(Categorical):

pandas/core/frame.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -581,10 +581,10 @@ class DataFrame(NDFrame, OpsMixin):
581581
_mgr: BlockManager | ArrayManager
582582

583583
@property
584-
def _constructor(self) -> type[DataFrame]:
584+
def _constructor(self) -> Callable[..., DataFrame]:
585585
return DataFrame
586586

587-
_constructor_sliced: type[Series] = Series
587+
_constructor_sliced: Callable[..., Series] = Series
588588

589589
# ----------------------------------------------------------------------
590590
# Constructors

pandas/core/generic.py

+11-27
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def _validate_dtype(cls, dtype) -> DtypeObj | None:
444444
# Construction
445445

446446
@property
447-
def _constructor(self: NDFrameT) -> type[NDFrameT]:
447+
def _constructor(self: NDFrameT) -> Callable[..., NDFrameT]:
448448
"""
449449
Used when a manipulation result has the same dimensions as the
450450
original.
@@ -779,17 +779,9 @@ def swapaxes(self: NDFrameT, axis1, axis2, copy=True) -> NDFrameT:
779779
if copy:
780780
new_values = new_values.copy()
781781

782-
# ignore needed because of NDFrame constructor is different than
783-
# DataFrame/Series constructors.
784782
return self._constructor(
785-
# error: Argument 1 to "NDFrame" has incompatible type "ndarray"; expected
786-
# "Union[ArrayManager, BlockManager]"
787-
# error: Argument 2 to "NDFrame" has incompatible type "*Generator[Index,
788-
# None, None]"; expected "bool" [arg-type]
789-
# error: Argument 2 to "NDFrame" has incompatible type "*Generator[Index,
790-
# None, None]"; expected "Optional[Mapping[Hashable, Any]]"
791-
new_values, # type: ignore[arg-type]
792-
*new_axes, # type: ignore[arg-type]
783+
new_values,
784+
*new_axes,
793785
).__finalize__(self, method="swapaxes")
794786

795787
@final
@@ -2088,11 +2080,7 @@ def __array_wrap__(
20882080
# ptp also requires the item_from_zerodim
20892081
return res
20902082
d = self._construct_axes_dict(self._AXIS_ORDERS, copy=False)
2091-
# error: Argument 1 to "NDFrame" has incompatible type "ndarray";
2092-
# expected "BlockManager"
2093-
return self._constructor(res, **d).__finalize__( # type: ignore[arg-type]
2094-
self, method="__array_wrap__"
2095-
)
2083+
return self._constructor(res, **d).__finalize__(self, method="__array_wrap__")
20962084

20972085
@final
20982086
def __array_ufunc__(
@@ -5923,11 +5911,9 @@ def astype(
59235911
# GH 19920: retain column metadata after concat
59245912
result = concat(results, axis=1, copy=False)
59255913
# GH#40810 retain subclass
5926-
# Incompatible types in assignment (expression has type "NDFrameT",
5927-
# variable has type "DataFrame")
5928-
# Argument 1 to "NDFrame" has incompatible type "DataFrame"; expected
5929-
# "Union[ArrayManager, SingleArrayManager, BlockManager, SingleBlockManager]"
5930-
result = self._constructor(result) # type: ignore[arg-type,assignment]
5914+
# error: Incompatible types in assignment
5915+
# (expression has type "NDFrameT", variable has type "DataFrame")
5916+
result = self._constructor(result) # type: ignore[assignment]
59315917
result.columns = self.columns
59325918
result = result.__finalize__(self, method="astype")
59335919
# https://github.com/python/mypy/issues/8354
@@ -6613,8 +6599,10 @@ def replace(
66136599

66146600
if isinstance(to_replace, (tuple, list)):
66156601
if isinstance(self, ABCDataFrame):
6602+
from pandas import Series
6603+
66166604
result = self.apply(
6617-
self._constructor_sliced._replace_single,
6605+
Series._replace_single,
66186606
args=(to_replace, method, inplace, limit),
66196607
)
66206608
if inplace:
@@ -9139,11 +9127,7 @@ def _where(
91399127

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

91489132
if axis is None:
91499133
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
@@ -2158,7 +2158,7 @@ def size(self) -> DataFrame | Series:
21582158
)
21592159

21602160
# GH28330 preserve subclassed Series/DataFrames through calls
2161-
if issubclass(self.obj._constructor, Series):
2161+
if isinstance(self.obj, Series):
21622162
result = self._obj_1d_constructor(result, name=self.obj.name)
21632163
else:
21642164
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
@@ -526,11 +526,11 @@ def _init_dict(
526526
# ----------------------------------------------------------------------
527527

528528
@property
529-
def _constructor(self) -> type[Series]:
529+
def _constructor(self) -> Callable[..., Series]:
530530
return Series
531531

532532
@property
533-
def _constructor_expanddim(self) -> type[DataFrame]:
533+
def _constructor_expanddim(self) -> Callable[..., DataFrame]:
534534
"""
535535
Used when a manipulation result has one higher dimension as the
536536
original, such as Series.to_frame()

pandas/tests/frame/test_subclass.py

+8
Original file line numberDiff line numberDiff line change
@@ -737,3 +737,11 @@ def test_equals_subclass(self):
737737
df2 = tm.SubclassedDataFrame({"a": [1, 2, 3]})
738738
assert df1.equals(df2)
739739
assert df2.equals(df1)
740+
741+
def test_replace_list_method(self):
742+
# https://github.com/pandas-dev/pandas/pull/46018
743+
df = tm.SubclassedDataFrame({"A": [0, 1, 2]})
744+
result = df.replace([1, 2], method="ffill")
745+
expected = tm.SubclassedDataFrame({"A": [0, 0, 0]})
746+
assert isinstance(result, tm.SubclassedDataFrame)
747+
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)