255 lines
9.1 KiB
Python
255 lines
9.1 KiB
Python
|
import operator
|
||
|
|
||
|
import numpy as np
|
||
|
import pytest
|
||
|
|
||
|
import pandas as pd
|
||
|
import pandas._testing as tm
|
||
|
from pandas.arrays import BooleanArray
|
||
|
from pandas.core.ops.mask_ops import (
|
||
|
kleene_and,
|
||
|
kleene_or,
|
||
|
kleene_xor,
|
||
|
)
|
||
|
from pandas.tests.extension.base import BaseOpsUtil
|
||
|
|
||
|
|
||
|
class TestLogicalOps(BaseOpsUtil):
|
||
|
def test_numpy_scalars_ok(self, all_logical_operators):
|
||
|
a = pd.array([True, False, None], dtype="boolean")
|
||
|
op = getattr(a, all_logical_operators)
|
||
|
|
||
|
tm.assert_extension_array_equal(op(True), op(np.bool_(True)))
|
||
|
tm.assert_extension_array_equal(op(False), op(np.bool_(False)))
|
||
|
|
||
|
def get_op_from_name(self, op_name):
|
||
|
short_opname = op_name.strip("_")
|
||
|
short_opname = short_opname if "xor" in short_opname else short_opname + "_"
|
||
|
try:
|
||
|
op = getattr(operator, short_opname)
|
||
|
except AttributeError:
|
||
|
# Assume it is the reverse operator
|
||
|
rop = getattr(operator, short_opname[1:])
|
||
|
op = lambda x, y: rop(y, x)
|
||
|
|
||
|
return op
|
||
|
|
||
|
def test_empty_ok(self, all_logical_operators):
|
||
|
a = pd.array([], dtype="boolean")
|
||
|
op_name = all_logical_operators
|
||
|
result = getattr(a, op_name)(True)
|
||
|
tm.assert_extension_array_equal(a, result)
|
||
|
|
||
|
result = getattr(a, op_name)(False)
|
||
|
tm.assert_extension_array_equal(a, result)
|
||
|
|
||
|
result = getattr(a, op_name)(pd.NA)
|
||
|
tm.assert_extension_array_equal(a, result)
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"other", ["a", pd.Timestamp(2017, 1, 1, 12), np.timedelta64(4)]
|
||
|
)
|
||
|
def test_eq_mismatched_type(self, other):
|
||
|
# GH-44499
|
||
|
arr = pd.array([True, False])
|
||
|
result = arr == other
|
||
|
expected = pd.array([False, False])
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
result = arr != other
|
||
|
expected = pd.array([True, True])
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
def test_logical_length_mismatch_raises(self, all_logical_operators):
|
||
|
op_name = all_logical_operators
|
||
|
a = pd.array([True, False, None], dtype="boolean")
|
||
|
msg = "Lengths must match"
|
||
|
|
||
|
with pytest.raises(ValueError, match=msg):
|
||
|
getattr(a, op_name)([True, False])
|
||
|
|
||
|
with pytest.raises(ValueError, match=msg):
|
||
|
getattr(a, op_name)(np.array([True, False]))
|
||
|
|
||
|
with pytest.raises(ValueError, match=msg):
|
||
|
getattr(a, op_name)(pd.array([True, False], dtype="boolean"))
|
||
|
|
||
|
def test_logical_nan_raises(self, all_logical_operators):
|
||
|
op_name = all_logical_operators
|
||
|
a = pd.array([True, False, None], dtype="boolean")
|
||
|
msg = "Got float instead"
|
||
|
|
||
|
with pytest.raises(TypeError, match=msg):
|
||
|
getattr(a, op_name)(np.nan)
|
||
|
|
||
|
@pytest.mark.parametrize("other", ["a", 1])
|
||
|
def test_non_bool_or_na_other_raises(self, other, all_logical_operators):
|
||
|
a = pd.array([True, False], dtype="boolean")
|
||
|
with pytest.raises(TypeError, match=str(type(other).__name__)):
|
||
|
getattr(a, all_logical_operators)(other)
|
||
|
|
||
|
def test_kleene_or(self):
|
||
|
# A clear test of behavior.
|
||
|
a = pd.array([True] * 3 + [False] * 3 + [None] * 3, dtype="boolean")
|
||
|
b = pd.array([True, False, None] * 3, dtype="boolean")
|
||
|
result = a | b
|
||
|
expected = pd.array(
|
||
|
[True, True, True, True, False, None, True, None, None], dtype="boolean"
|
||
|
)
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
result = b | a
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
# ensure we haven't mutated anything inplace
|
||
|
tm.assert_extension_array_equal(
|
||
|
a, pd.array([True] * 3 + [False] * 3 + [None] * 3, dtype="boolean")
|
||
|
)
|
||
|
tm.assert_extension_array_equal(
|
||
|
b, pd.array([True, False, None] * 3, dtype="boolean")
|
||
|
)
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"other, expected",
|
||
|
[
|
||
|
(pd.NA, [True, None, None]),
|
||
|
(True, [True, True, True]),
|
||
|
(np.bool_(True), [True, True, True]),
|
||
|
(False, [True, False, None]),
|
||
|
(np.bool_(False), [True, False, None]),
|
||
|
],
|
||
|
)
|
||
|
def test_kleene_or_scalar(self, other, expected):
|
||
|
# TODO: test True & False
|
||
|
a = pd.array([True, False, None], dtype="boolean")
|
||
|
result = a | other
|
||
|
expected = pd.array(expected, dtype="boolean")
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
result = other | a
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
# ensure we haven't mutated anything inplace
|
||
|
tm.assert_extension_array_equal(
|
||
|
a, pd.array([True, False, None], dtype="boolean")
|
||
|
)
|
||
|
|
||
|
def test_kleene_and(self):
|
||
|
# A clear test of behavior.
|
||
|
a = pd.array([True] * 3 + [False] * 3 + [None] * 3, dtype="boolean")
|
||
|
b = pd.array([True, False, None] * 3, dtype="boolean")
|
||
|
result = a & b
|
||
|
expected = pd.array(
|
||
|
[True, False, None, False, False, False, None, False, None], dtype="boolean"
|
||
|
)
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
result = b & a
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
# ensure we haven't mutated anything inplace
|
||
|
tm.assert_extension_array_equal(
|
||
|
a, pd.array([True] * 3 + [False] * 3 + [None] * 3, dtype="boolean")
|
||
|
)
|
||
|
tm.assert_extension_array_equal(
|
||
|
b, pd.array([True, False, None] * 3, dtype="boolean")
|
||
|
)
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"other, expected",
|
||
|
[
|
||
|
(pd.NA, [None, False, None]),
|
||
|
(True, [True, False, None]),
|
||
|
(False, [False, False, False]),
|
||
|
(np.bool_(True), [True, False, None]),
|
||
|
(np.bool_(False), [False, False, False]),
|
||
|
],
|
||
|
)
|
||
|
def test_kleene_and_scalar(self, other, expected):
|
||
|
a = pd.array([True, False, None], dtype="boolean")
|
||
|
result = a & other
|
||
|
expected = pd.array(expected, dtype="boolean")
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
result = other & a
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
# ensure we haven't mutated anything inplace
|
||
|
tm.assert_extension_array_equal(
|
||
|
a, pd.array([True, False, None], dtype="boolean")
|
||
|
)
|
||
|
|
||
|
def test_kleene_xor(self):
|
||
|
a = pd.array([True] * 3 + [False] * 3 + [None] * 3, dtype="boolean")
|
||
|
b = pd.array([True, False, None] * 3, dtype="boolean")
|
||
|
result = a ^ b
|
||
|
expected = pd.array(
|
||
|
[False, True, None, True, False, None, None, None, None], dtype="boolean"
|
||
|
)
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
result = b ^ a
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
# ensure we haven't mutated anything inplace
|
||
|
tm.assert_extension_array_equal(
|
||
|
a, pd.array([True] * 3 + [False] * 3 + [None] * 3, dtype="boolean")
|
||
|
)
|
||
|
tm.assert_extension_array_equal(
|
||
|
b, pd.array([True, False, None] * 3, dtype="boolean")
|
||
|
)
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"other, expected",
|
||
|
[
|
||
|
(pd.NA, [None, None, None]),
|
||
|
(True, [False, True, None]),
|
||
|
(np.bool_(True), [False, True, None]),
|
||
|
(np.bool_(False), [True, False, None]),
|
||
|
],
|
||
|
)
|
||
|
def test_kleene_xor_scalar(self, other, expected):
|
||
|
a = pd.array([True, False, None], dtype="boolean")
|
||
|
result = a ^ other
|
||
|
expected = pd.array(expected, dtype="boolean")
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
result = other ^ a
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
# ensure we haven't mutated anything inplace
|
||
|
tm.assert_extension_array_equal(
|
||
|
a, pd.array([True, False, None], dtype="boolean")
|
||
|
)
|
||
|
|
||
|
@pytest.mark.parametrize("other", [True, False, pd.NA, [True, False, None] * 3])
|
||
|
def test_no_masked_assumptions(self, other, all_logical_operators):
|
||
|
# The logical operations should not assume that masked values are False!
|
||
|
a = pd.arrays.BooleanArray(
|
||
|
np.array([True, True, True, False, False, False, True, False, True]),
|
||
|
np.array([False] * 6 + [True, True, True]),
|
||
|
)
|
||
|
b = pd.array([True] * 3 + [False] * 3 + [None] * 3, dtype="boolean")
|
||
|
if isinstance(other, list):
|
||
|
other = pd.array(other, dtype="boolean")
|
||
|
|
||
|
result = getattr(a, all_logical_operators)(other)
|
||
|
expected = getattr(b, all_logical_operators)(other)
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
if isinstance(other, BooleanArray):
|
||
|
other._data[other._mask] = True
|
||
|
a._data[a._mask] = False
|
||
|
|
||
|
result = getattr(a, all_logical_operators)(other)
|
||
|
expected = getattr(b, all_logical_operators)(other)
|
||
|
tm.assert_extension_array_equal(result, expected)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("operation", [kleene_or, kleene_xor, kleene_and])
|
||
|
def test_error_both_scalar(operation):
|
||
|
msg = r"Either `left` or `right` need to be a np\.ndarray."
|
||
|
with pytest.raises(TypeError, match=msg):
|
||
|
# masks need to be non-None, otherwise it ends up in an infinite recursion
|
||
|
operation(True, True, np.zeros(1), np.zeros(1))
|