diff --git a/changelog/9353.bugfix.rst b/changelog/9353.bugfix.rst new file mode 100644 index 000000000..6824d11f5 --- /dev/null +++ b/changelog/9353.bugfix.rst @@ -0,0 +1 @@ +`approx()` now uses strict equality when `type(expected) == bool`. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index ea646811d..a6f8029ab 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -265,10 +265,16 @@ class ApproxMapping(ApproxBase): max_abs_diff = max( max_abs_diff, abs(approx_value.expected - other_value) ) - max_rel_diff = max( - max_rel_diff, - abs((approx_value.expected - other_value) / approx_value.expected), - ) + try: + max_rel_diff = max( + max_rel_diff, + abs( + (approx_value.expected - other_value) + / approx_value.expected + ), + ) + except ZeroDivisionError: + pass different_ids.append(approx_key) message_data = [ @@ -395,8 +401,12 @@ class ApproxScalar(ApproxBase): # Don't show a tolerance for values that aren't compared using # tolerances, i.e. non-numerics and infinities. Need to call abs to # handle complex numbers, e.g. (inf + 1j). - if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf( - abs(self.expected) # type: ignore[arg-type] + if ( + isinstance(self.expected, bool) + or (not isinstance(self.expected, (Complex, Decimal))) + or math.isinf( + abs(self.expected) or isinstance(self.expected, bool) # type: ignore[arg-type] + ) ): return str(self.expected) @@ -424,14 +434,17 @@ class ApproxScalar(ApproxBase): # numpy<1.13. See #3748. return all(self.__eq__(a) for a in asarray.flat) - # Short-circuit exact equality. - if actual == self.expected: + # Short-circuit exact equality, except for bool + if isinstance(self.expected, bool) and not isinstance(actual, bool): + return False + elif actual == self.expected: return True # 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. - if not ( + # __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 ( isinstance(self.expected, (Complex, Decimal)) and isinstance(actual, (Complex, Decimal)) ): diff --git a/testing/python/approx.py b/testing/python/approx.py index 2eec4e9f7..4daf4c974 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -87,7 +87,7 @@ def assert_approx_raises_regex(pytestconfig): return do_assert -SOME_FLOAT = r"[+-]?([0-9]*[.])?[0-9]+\s*" +SOME_FLOAT = r"[+-]?((?:([0-9]*[.])?[0-9]+(e-?[0-9]+)?)|inf|nan)\s*" SOME_INT = r"[0-9]+\s*" @@ -95,6 +95,19 @@ class TestApprox: def test_error_messages(self, assert_approx_raises_regex): np = pytest.importorskip("numpy") + # treat bool exactly + assert_approx_raises_regex( + {"a": 1.0, "b": True}, + {"a": 1.0, "b": False}, + [ + " comparison failed. Mismatched elements: 1 / 2:", + f" Max absolute difference: {SOME_FLOAT}", + f" Max relative difference: {SOME_FLOAT}", + r" Index\s+\| Obtained\s+\| Expected", + r".*(True|False)\s+", + ], + ) + assert_approx_raises_regex( 2.0, 1.0, @@ -545,6 +558,13 @@ class TestApprox: assert approx(x, rel=5e-6, abs=0) == a assert approx(x, rel=5e-7, abs=0) != a + def test_expecting_bool(self): + assert True == approx(True) # noqa: E712 + assert False == approx(False) # noqa: E712 + assert True != approx(False) # noqa: E712 + assert True != approx(False, abs=2) # noqa: E712 + assert 1 != approx(True) + def test_list(self): actual = [1 + 1e-7, 2 + 1e-8] expected = [1, 2] @@ -610,6 +630,7 @@ class TestApprox: def test_dict_nonnumeric(self): assert {"a": 1.0, "b": None} == pytest.approx({"a": 1.0, "b": None}) assert {"a": 1.0, "b": 1} != pytest.approx({"a": 1.0, "b": None}) + assert {"a": 1.0, "b": True} != pytest.approx({"a": 1.0, "b": False}, abs=2) def test_dict_vs_other(self): assert 1 != approx({"a": 0})