diff --git a/AUTHORS b/AUTHORS index 95e6b13f11e..9004008bfa5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -58,6 +58,7 @@ Ashley Whetter Aviral Verma Aviv Palivoda Babak Keyvani +Bahram Farahmand Barney Gale Ben Brown Ben Gartner diff --git a/changelog/13047.bugfix.rst b/changelog/13047.bugfix.rst new file mode 100644 index 00000000000..399e860505c --- /dev/null +++ b/changelog/13047.bugfix.rst @@ -0,0 +1,17 @@ +Restore :func:`pytest.approx` handling of equality checks between `bool` and `numpy.bool_` types. + +Comparing `bool` and `numpy.bool_` using :func:`pytest.approx` accidentally changed in version `8.3.4` and `8.3.5` to no longer match: + +.. code-block:: pycon + + >>> import numpy as np + >>> from pytest import approx + >>> [np.True_, np.True_] == pytest.approx([True, True]) + False + +This has now been fixed: + +.. code-block:: pycon + + >>> [np.True_, np.True_] == pytest.approx([True, True]) + True diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index af078e25256..3f899a24ea8 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -421,14 +421,26 @@ def __repr__(self) -> str: def __eq__(self, actual) -> bool: """Return whether the given value is equal to the expected value within the pre-specified tolerance.""" + + def is_bool(val: Any) -> bool: + # Check if `val` is a native bool or numpy bool. + if isinstance(val, bool): + return True + try: + import numpy as np + + return isinstance(val, np.bool_) + except ImportError: + return False + asarray = _as_numpy_array(actual) if asarray is not None: # Call ``__eq__()`` manually to prevent infinite-recursion with # numpy<1.13. See #3748. return all(self.__eq__(a) for a in asarray.flat) - # Short-circuit exact equality, except for bool - if isinstance(self.expected, bool) and not isinstance(actual, bool): + # Short-circuit exact equality, except for bool and np.bool_ + if is_bool(self.expected) and not is_bool(actual): return False elif actual == self.expected: return True @@ -436,8 +448,8 @@ def __eq__(self, actual) -> bool: # If either type is non-numeric, fall back to strict equality. # NB: we need Complex, rather than just Number, to ensure that __abs__, # __sub__, and __float__ are defined. Also, consider bool to be - # nonnumeric, even though it has the required arithmetic. - if isinstance(self.expected, bool) or not ( + # non-numeric, even though it has the required arithmetic. + if is_bool(self.expected) or not ( isinstance(self.expected, (Complex, Decimal)) and isinstance(actual, (Complex, Decimal)) ): diff --git a/testing/python/approx.py b/testing/python/approx.py index 7eba4755c01..01b58782cdb 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -647,6 +647,15 @@ def test_expecting_bool(self) -> None: assert True != approx(False, abs=2) # noqa: E712 assert 1 != approx(True) + def test_expecting_bool_numpy(self) -> None: + """Check approx comparing with numpy.bool (#13047).""" + np = pytest.importorskip("numpy") + assert np.False_ != approx(True) + assert np.True_ != approx(False) + assert np.True_ == approx(True) + assert np.False_ == approx(False) + assert np.True_ != approx(False, abs=2) + def test_list(self): actual = [1 + 1e-7, 2 + 1e-8] expected = [1, 2]