Skip to content

Commit b8945dc

Browse files
committed
Reject sequences nested in a sequence of a different type in approx
ApproxSequenceLike guarded against nesting with isinstance(x, type(expected)), which only caught an element of the same type as the container. A tuple inside a list, or a list inside a tuple, passed the guard and was handed to ApproxScalar, where it was compared exactly instead of approximately. That returned a wrong bool rather than raising, so [(1.20000000000001,)] == approx([(1.2,)]) was False while the flat (1.20000000000001,) == approx((1.2,)) was True. Use _is_sequence_like, the same predicate approx() uses to dispatch to ApproxSequenceLike, so nesting is rejected consistently. Mappings are excluded so that a dict nested in a sequence keeps its current behaviour. Closes #11945
1 parent 0d6fbde commit b8945dc

3 files changed

Lines changed: 31 additions & 1 deletion

File tree

changelog/11945.bugfix.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`pytest.approx` now rejects a sequence nested inside a sequence of a different type,
2+
such as a tuple inside a list. Previously only same-type nesting was detected, so these
3+
values reached the scalar comparison and were compared exactly rather than approximately,
4+
which silently returned the wrong result instead of raising ``TypeError``.

src/_pytest/approx.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,14 @@ def __init__(
358358
__tracebackhide__ = True
359359

360360
for index, x in enumerate(expected):
361-
if isinstance(x, type(expected)):
361+
# Use the same predicate the dispatcher uses to recognise a sequence,
362+
# so that nesting is rejected consistently. Comparing against
363+
# `type(expected)` only caught an element of the *same* type, which let
364+
# a tuple inside a list (and a list inside a tuple) through to
365+
# ApproxScalar, where it was compared exactly instead of approximately.
366+
# Mappings are left alone: they are not sequences, and rejecting them
367+
# here would change behaviour beyond this fix.
368+
if _is_sequence_like(x) and not isinstance(x, Mapping):
362369
msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}"
363370
raise TypeError(msg.format(x, index, pprint.pformat(expected)))
364371

testing/python/approx.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,11 @@ def test_foo():
924924
[
925925
pytest.param([[1]], "data structures", id="nested-list"),
926926
pytest.param({"key": {"key": 1}}, "dictionaries", id="nested-dict"),
927+
# GH#11945: the guard compared each element against type(expected),
928+
# so a sequence nested inside a sequence of a *different* type was
929+
# not rejected and ended up being compared exactly.
930+
pytest.param([(1,)], "data structures", id="tuple-in-list"),
931+
pytest.param(([1],), "data structures", id="list-in-tuple"),
927932
],
928933
)
929934
def test_expected_value_type_error(self, x, name):
@@ -933,6 +938,20 @@ def test_expected_value_type_error(self, x, name):
933938
):
934939
approx(x)
935940

941+
@pytest.mark.parametrize(
942+
"actual, expected",
943+
[
944+
pytest.param([(1.2,)], [(1.2,)], id="tuple-in-list"),
945+
pytest.param(([1.2],), ([1.2],), id="list-in-tuple"),
946+
],
947+
)
948+
def test_nested_mixed_sequence_not_silently_compared(self, actual, expected):
949+
# GH#11945: these used to return a bool from an exact comparison, so
950+
# `[(1.20000000000001,)] == approx([(1.2,)])` was False while the flat
951+
# `(1.20000000000001,) == approx((1.2,))` was True.
952+
with pytest.raises(TypeError):
953+
assert actual == approx(expected)
954+
936955
@pytest.mark.parametrize(
937956
"x",
938957
[

0 commit comments

Comments
 (0)