diff --git a/changelog/6057.feature.rst b/changelog/6057.feature.rst new file mode 100644 index 000000000..b7334e7fe --- /dev/null +++ b/changelog/6057.feature.rst @@ -0,0 +1,3 @@ +Add tolerances to complex values when printing ``pytest.approx``. + +For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index f03d45ab7..52a91a905 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -223,26 +223,24 @@ class ApproxScalar(ApproxBase): def __repr__(self): """ Return a string communicating both the expected value and the tolerance - for the comparison being made, e.g. '1.0 +- 1e-6'. Use the unicode - plus/minus symbol if this is python3 (it's too hard to get right for - python2). + for the comparison being made, e.g. '1.0 ± 1e-6', '(3+4j) ± 5e-6 ∠ ±180°'. """ - if isinstance(self.expected, complex): - return str(self.expected) # Infinities aren't compared using tolerances, so don't show a - # tolerance. - if math.isinf(self.expected): + # tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j) + if math.isinf(abs(self.expected)): return str(self.expected) # If a sensible tolerance can't be calculated, self.tolerance will # raise a ValueError. In this case, display '???'. try: vetted_tolerance = "{:.1e}".format(self.tolerance) + if isinstance(self.expected, complex) and not math.isinf(self.tolerance): + vetted_tolerance += " ∠ ±180°" except ValueError: vetted_tolerance = "???" - return "{} \u00b1 {}".format(self.expected, vetted_tolerance) + return "{} ± {}".format(self.expected, vetted_tolerance) def __eq__(self, actual): """ diff --git a/testing/python/approx.py b/testing/python/approx.py index 0575557ae..60fde151a 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -24,55 +24,50 @@ class MyDocTestRunner(doctest.DocTestRunner): class TestApprox: - @pytest.fixture - def plus_minus(self): - return "\u00b1" - - def test_repr_string(self, plus_minus): - tol1, tol2, infr = "1.0e-06", "2.0e-06", "inf" - assert repr(approx(1.0)) == "1.0 {pm} {tol1}".format(pm=plus_minus, tol1=tol1) - assert repr( - approx([1.0, 2.0]) - ) == "approx([1.0 {pm} {tol1}, 2.0 {pm} {tol2}])".format( - pm=plus_minus, tol1=tol1, tol2=tol2 - ) - assert repr( - approx((1.0, 2.0)) - ) == "approx((1.0 {pm} {tol1}, 2.0 {pm} {tol2}))".format( - pm=plus_minus, tol1=tol1, tol2=tol2 - ) + def test_repr_string(self): + assert repr(approx(1.0)) == "1.0 ± 1.0e-06" + assert repr(approx([1.0, 2.0])) == "approx([1.0 ± 1.0e-06, 2.0 ± 2.0e-06])" + assert repr(approx((1.0, 2.0))) == "approx((1.0 ± 1.0e-06, 2.0 ± 2.0e-06))" assert repr(approx(inf)) == "inf" - assert repr(approx(1.0, rel=nan)) == "1.0 {pm} ???".format(pm=plus_minus) - assert repr(approx(1.0, rel=inf)) == "1.0 {pm} {infr}".format( - pm=plus_minus, infr=infr - ) - assert repr(approx(1.0j, rel=inf)) == "1j" + assert repr(approx(1.0, rel=nan)) == "1.0 ± ???" + assert repr(approx(1.0, rel=inf)) == "1.0 ± inf" # Dictionaries aren't ordered, so we need to check both orders. assert repr(approx({"a": 1.0, "b": 2.0})) in ( - "approx({{'a': 1.0 {pm} {tol1}, 'b': 2.0 {pm} {tol2}}})".format( - pm=plus_minus, tol1=tol1, tol2=tol2 - ), - "approx({{'b': 2.0 {pm} {tol2}, 'a': 1.0 {pm} {tol1}}})".format( - pm=plus_minus, tol1=tol1, tol2=tol2 - ), + "approx({'a': 1.0 ± 1.0e-06, 'b': 2.0 ± 2.0e-06})", + "approx({'b': 2.0 ± 2.0e-06, 'a': 1.0 ± 1.0e-06})", ) + def test_repr_complex_numbers(self): + assert repr(approx(inf + 1j)) == "(inf+1j)" + assert repr(approx(1.0j, rel=inf)) == "1j ± inf" + + # can't compute a sensible tolerance + assert repr(approx(nan + 1j)) == "(nan+1j) ± ???" + + assert repr(approx(1.0j)) == "1j ± 1.0e-06 ∠ ±180°" + + # relative tolerance is scaled to |3+4j| = 5 + assert repr(approx(3 + 4 * 1j)) == "(3+4j) ± 5.0e-06 ∠ ±180°" + + # absolute tolerance is not scaled + assert repr(approx(3.3 + 4.4 * 1j, abs=0.02)) == "(3.3+4.4j) ± 2.0e-02 ∠ ±180°" + @pytest.mark.parametrize( - "value, repr_string", + "value, expected_repr_string", [ - (5.0, "approx(5.0 {pm} 5.0e-06)"), - ([5.0], "approx([5.0 {pm} 5.0e-06])"), - ([[5.0]], "approx([[5.0 {pm} 5.0e-06]])"), - ([[5.0, 6.0]], "approx([[5.0 {pm} 5.0e-06, 6.0 {pm} 6.0e-06]])"), - ([[5.0], [6.0]], "approx([[5.0 {pm} 5.0e-06], [6.0 {pm} 6.0e-06]])"), + (5.0, "approx(5.0 ± 5.0e-06)"), + ([5.0], "approx([5.0 ± 5.0e-06])"), + ([[5.0]], "approx([[5.0 ± 5.0e-06]])"), + ([[5.0, 6.0]], "approx([[5.0 ± 5.0e-06, 6.0 ± 6.0e-06]])"), + ([[5.0], [6.0]], "approx([[5.0 ± 5.0e-06], [6.0 ± 6.0e-06]])"), ], ) - def test_repr_nd_array(self, plus_minus, value, repr_string): + def test_repr_nd_array(self, value, expected_repr_string): """Make sure that arrays of all different dimensions are repr'd correctly.""" np = pytest.importorskip("numpy") np_array = np.array(value) - assert repr(approx(np_array)) == repr_string.format(pm=plus_minus) + assert repr(approx(np_array)) == expected_repr_string def test_operator_overloading(self): assert 1 == approx(1, rel=1e-6, abs=1e-12)