From 4c574eae3ecd14c578a6a83997f2b570a186f721 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 19 Aug 2022 11:10:28 +0200 Subject: [PATCH 1/2] REGR: fix calling numpy bitwise ufunc with Index objects --- doc/source/whatsnew/v1.4.4.rst | 1 + pandas/_libs/ops_dispatch.pyx | 23 ++++++++++++++++++----- pandas/core/indexes/base.py | 2 +- pandas/tests/indexes/test_numpy_compat.py | 13 +++++++++++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/doc/source/whatsnew/v1.4.4.rst b/doc/source/whatsnew/v1.4.4.rst index 57b8fdee5888a..4af71de227b30 100644 --- a/doc/source/whatsnew/v1.4.4.rst +++ b/doc/source/whatsnew/v1.4.4.rst @@ -16,6 +16,7 @@ Fixed regressions ~~~~~~~~~~~~~~~~~ - Fixed regression in taking NULL :class:`objects` from a :class:`DataFrame` causing a segmentation violation. These NULL values are created by :meth:`numpy.empty_like` (:issue:`46848`) - Fixed regression in :func:`concat` materializing :class:`Index` during sorting even if :class:`Index` was already sorted (:issue:`47501`) +- Fixed regression in calling bitwise numpy ufuncs (for example, ``np.bitwise_and``) on Index objects (:issue:`46769`) - Fixed regression in :meth:`DataFrame.loc` not updating the cache correctly after values were set (:issue:`47867`) - Fixed regression in :meth:`DataFrame.loc` not aligning index in some cases when setting a :class:`DataFrame` (:issue:`47578`) - Fixed regression in setting ``None`` or non-string value into a ``string``-dtype Series using a mask (:issue:`47628`) diff --git a/pandas/_libs/ops_dispatch.pyx b/pandas/_libs/ops_dispatch.pyx index 2b2a411e6635f..f67b55a69d0d7 100644 --- a/pandas/_libs/ops_dispatch.pyx +++ b/pandas/_libs/ops_dispatch.pyx @@ -15,13 +15,22 @@ DISPATCHED_UFUNCS = { "ge", "remainder", "matmul", - "or", - "xor", - "and", + # those three are currently set operations, not logical operations, for the + # Index dunder method (but this is deprecated) + # TODO(2.0) this can be uncommented (and DISPATCHED_UFUNCS_NO_INDEX removed) + # once deprecation is enforced in 2.0 + # "or", + # "xor", + # "and", "neg", "pos", "abs", } +DISPATCHED_UFUNCS_NO_INDEX = { + "or", + "xor", + "and", +} UNARY_UFUNCS = { "neg", "pos", @@ -61,7 +70,7 @@ REVERSED_NAMES = { def maybe_dispatch_ufunc_to_dunder_op( - object self, object ufunc, str method, *inputs, **kwargs + object self, object ufunc, str method, *inputs, bint _no_index=True, **kwargs ): """ Dispatch a ufunc to the equivalent dunder method. @@ -94,7 +103,11 @@ def maybe_dispatch_ufunc_to_dunder_op( if kwargs or ufunc.nin > 2: return NotImplemented - if method == "__call__" and op_name in DISPATCHED_UFUNCS: + if method == "__call__" and ( + op_name in DISPATCHED_UFUNCS or ( + _no_index and op_name in DISPATCHED_UFUNCS_NO_INDEX + ) + ): if inputs[0] is self: name = f"__{op_name}__" diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 816260c8a6d2d..5828f9e30809b 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -940,7 +940,7 @@ def __array_ufunc__(self, ufunc: np.ufunc, method: str_t, *inputs, **kwargs): return NotImplemented result = arraylike.maybe_dispatch_ufunc_to_dunder_op( - self, ufunc, method, *inputs, **kwargs + self, ufunc, method, *inputs, _no_index=False, **kwargs ) if result is not NotImplemented: return result diff --git a/pandas/tests/indexes/test_numpy_compat.py b/pandas/tests/indexes/test_numpy_compat.py index 2704127c7b511..453ece35a68e7 100644 --- a/pandas/tests/indexes/test_numpy_compat.py +++ b/pandas/tests/indexes/test_numpy_compat.py @@ -178,3 +178,16 @@ def test_numpy_ufuncs_reductions(index, func, request): assert isna(expected) else: assert result == expected + + +@pytest.mark.parametrize("func", [np.bitwise_and, np.bitwise_or, np.bitwise_xor]) +def test_numpy_ufuncs_bitwise(func): + # https://github.com/pandas-dev/pandas/issues/46769 + idx1 = Index([1, 2, 3, 4], dtype="int64") + idx2 = Index([3, 4, 5, 6], dtype="int64") + + with tm.assert_produces_warning(None): + result = func(idx1, idx2) + + expected = Index(func(idx1.values, idx2.values)) + tm.assert_index_equal(result, expected) From b65bb366b3c741baa77e2b32a74159e9f49e52aa Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sat, 20 Aug 2022 09:15:45 +0200 Subject: [PATCH 2/2] simplify --- pandas/_libs/ops_dispatch.pyx | 23 +++++------------------ pandas/core/indexes/base.py | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/pandas/_libs/ops_dispatch.pyx b/pandas/_libs/ops_dispatch.pyx index f67b55a69d0d7..2b2a411e6635f 100644 --- a/pandas/_libs/ops_dispatch.pyx +++ b/pandas/_libs/ops_dispatch.pyx @@ -15,21 +15,12 @@ DISPATCHED_UFUNCS = { "ge", "remainder", "matmul", - # those three are currently set operations, not logical operations, for the - # Index dunder method (but this is deprecated) - # TODO(2.0) this can be uncommented (and DISPATCHED_UFUNCS_NO_INDEX removed) - # once deprecation is enforced in 2.0 - # "or", - # "xor", - # "and", - "neg", - "pos", - "abs", -} -DISPATCHED_UFUNCS_NO_INDEX = { "or", "xor", "and", + "neg", + "pos", + "abs", } UNARY_UFUNCS = { "neg", @@ -70,7 +61,7 @@ REVERSED_NAMES = { def maybe_dispatch_ufunc_to_dunder_op( - object self, object ufunc, str method, *inputs, bint _no_index=True, **kwargs + object self, object ufunc, str method, *inputs, **kwargs ): """ Dispatch a ufunc to the equivalent dunder method. @@ -103,11 +94,7 @@ def maybe_dispatch_ufunc_to_dunder_op( if kwargs or ufunc.nin > 2: return NotImplemented - if method == "__call__" and ( - op_name in DISPATCHED_UFUNCS or ( - _no_index and op_name in DISPATCHED_UFUNCS_NO_INDEX - ) - ): + if method == "__call__" and op_name in DISPATCHED_UFUNCS: if inputs[0] is self: name = f"__{op_name}__" diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 5828f9e30809b..29194a1718e55 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -939,11 +939,19 @@ def __array_ufunc__(self, ufunc: np.ufunc, method: str_t, *inputs, **kwargs): if any(isinstance(other, (ABCSeries, ABCDataFrame)) for other in inputs): return NotImplemented - result = arraylike.maybe_dispatch_ufunc_to_dunder_op( - self, ufunc, method, *inputs, _no_index=False, **kwargs - ) - if result is not NotImplemented: - return result + # TODO(2.0) the 'and', 'or' and 'xor' dunder methods are currently set + # operations and not logical operations, so don't dispatch + # This is deprecated, so this full 'if' clause can be removed once + # deprecation is enforced in 2.0 + if not ( + method == "__call__" + and ufunc in (np.bitwise_and, np.bitwise_or, np.bitwise_xor) + ): + result = arraylike.maybe_dispatch_ufunc_to_dunder_op( + self, ufunc, method, *inputs, **kwargs + ) + if result is not NotImplemented: + return result if "out" in kwargs: # e.g. test_dti_isub_tdi