diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index 2f462b16ddf78..ae84ad08270f6 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -362,6 +362,7 @@ Numeric - Bug in :class:`Series` flex arithmetic methods where the result when operating with a ``list``, ``tuple`` or ``np.ndarray`` would have an incorrect name (:issue:`36760`) - Bug in :class:`IntegerArray` multiplication with ``timedelta`` and ``np.timedelta64`` objects (:issue:`36870`) - Bug in :meth:`DataFrame.diff` with ``datetime64`` dtypes including ``NaT`` values failing to fill ``NaT`` results correctly (:issue:`32441`) +- Bug in :class:`DataFrame` arithmetic ops incorrectly accepting keyword arguments (:issue:`36843`) Conversion ^^^^^^^^^^ diff --git a/pandas/core/ops/__init__.py b/pandas/core/ops/__init__.py index 0de842e8575af..b656aef64cde9 100644 --- a/pandas/core/ops/__init__.py +++ b/pandas/core/ops/__init__.py @@ -533,18 +533,13 @@ def _maybe_align_series_as_frame(frame: "DataFrame", series: "Series", axis: int return type(frame)(rvalues, index=frame.index, columns=frame.columns) -def arith_method_FRAME(cls: Type["DataFrame"], op, special: bool): - # This is the only function where `special` can be either True or False +def flex_arith_method_FRAME(cls: Type["DataFrame"], op, special: bool): + assert not special op_name = _get_op_name(op, special) default_axis = None if special else "columns" na_op = get_array_op(op) - - if op_name in _op_descriptions: - # i.e. include "add" but not "__add__" - doc = _make_flex_doc(op_name, "dataframe") - else: - doc = _arith_doc_FRAME % op_name + doc = _make_flex_doc(op_name, "dataframe") @Appender(doc) def f(self, other, axis=default_axis, level=None, fill_value=None): @@ -561,8 +556,6 @@ def f(self, other, axis=default_axis, level=None, fill_value=None): axis = self._get_axis_number(axis) if axis is not None else 1 - # TODO: why are we passing flex=True instead of flex=not special? - # 15 tests fail if we pass flex=not special instead self, other = align_method_FRAME(self, other, axis, flex=True, level=level) if isinstance(other, ABCDataFrame): @@ -585,6 +578,29 @@ def f(self, other, axis=default_axis, level=None, fill_value=None): return f +def arith_method_FRAME(cls: Type["DataFrame"], op, special: bool): + assert special + op_name = _get_op_name(op, special) + doc = _arith_doc_FRAME % op_name + + @Appender(doc) + def f(self, other): + + if _should_reindex_frame_op(self, other, op, 1, 1, None, None): + return _frame_arith_method_with_reindex(self, other, op) + + axis = 1 # only relevant for Series other case + + self, other = align_method_FRAME(self, other, axis, flex=True, level=None) + + new_data = dispatch_to_series(self, other, op, axis=axis) + return self._construct_result(new_data) + + f.__name__ = op_name + + return f + + def flex_comp_method_FRAME(cls: Type["DataFrame"], op, special: bool): assert not special # "special" also means "not flex" op_name = _get_op_name(op, special) @@ -616,7 +632,7 @@ def comp_method_FRAME(cls: Type["DataFrame"], op, special: bool): def f(self, other): axis = 1 # only relevant for Series other case - self, other = align_method_FRAME(self, other, axis, level=None, flex=False) + self, other = align_method_FRAME(self, other, axis, flex=False, level=None) # See GH#4537 for discussion of scalar op behavior new_data = dispatch_to_series(self, other, op, axis=axis) diff --git a/pandas/core/ops/methods.py b/pandas/core/ops/methods.py index 05da378f8964d..86981f007a678 100644 --- a/pandas/core/ops/methods.py +++ b/pandas/core/ops/methods.py @@ -46,6 +46,7 @@ def _get_method_wrappers(cls): from pandas.core.ops import ( arith_method_FRAME, comp_method_FRAME, + flex_arith_method_FRAME, flex_comp_method_FRAME, flex_method_SERIES, ) @@ -58,7 +59,7 @@ def _get_method_wrappers(cls): comp_special = None bool_special = None elif issubclass(cls, ABCDataFrame): - arith_flex = arith_method_FRAME + arith_flex = flex_arith_method_FRAME comp_flex = flex_comp_method_FRAME arith_special = arith_method_FRAME comp_special = comp_method_FRAME diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index d9ef19e174700..94f813fd08128 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -1484,6 +1484,13 @@ def test_no_warning(self, all_arithmetic_operators): df = pd.DataFrame({"A": [0.0, 0.0], "B": [0.0, None]}) b = df["B"] with tm.assert_produces_warning(None): + getattr(df, all_arithmetic_operators)(b) + + def test_dunder_methods_binary(self, all_arithmetic_operators): + # GH#??? frame.__foo__ should only accept one argument + df = pd.DataFrame({"A": [0.0, 0.0], "B": [0.0, None]}) + b = df["B"] + with pytest.raises(TypeError, match="takes 2 positional arguments"): getattr(df, all_arithmetic_operators)(b, 0) diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index a796023c75b78..df6b8187964e8 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -276,25 +276,12 @@ def test_scalar_na_logical_ops_corners_aligns(self): expected = DataFrame(False, index=range(9), columns=["A"] + list(range(9))) - result = d.__and__(s, axis="columns") - tm.assert_frame_equal(result, expected) - - result = d.__and__(s, axis=1) - tm.assert_frame_equal(result, expected) - result = s & d tm.assert_frame_equal(result, expected) result = d & s tm.assert_frame_equal(result, expected) - expected = (s & s).to_frame("A") - result = d.__and__(s, axis="index") - tm.assert_frame_equal(result, expected) - - result = d.__and__(s, axis=0) - tm.assert_frame_equal(result, expected) - @pytest.mark.parametrize("op", [operator.and_, operator.or_, operator.xor]) def test_logical_ops_with_index(self, op): # GH#22092, GH#19792