Skip to content

Commit 01281a1

Browse files
deepak7lalclaude
andcommitted
Fix pytest.approx mapping details for non-numeric values
ApproxMapping._repr_compare guards its diff arithmetic with `except ZeroDivisionError`, so an unequal pair of non-numeric values under the same key raises TypeError into the assertion-repr hook and the mismatch table is replaced by "representation of details failed". The sequence path already handles this. Catching TypeError alone is not enough here. decimal.FloatOperation subclasses it, and #15006 made the sequence path re-raise that ahead of the non-number handler so a mapping mixing Decimals with floats cannot report the smaller of two differences as the maximum. Mirror both clauses rather than only the second. Adds the mapping counterpart of test_mixed_decimal_and_float_sequence_does_not_hide_float_operation, which fails without the re-raise, so the wrong maximum is now caught by the suite. Closes #15009 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent efc3832 commit 01281a1

4 files changed

Lines changed: 70 additions & 1 deletion

File tree

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ David Szotten
140140
David Vierra
141141
Daw-Ran Liou
142142
Debi Mishra
143+
deepak7lal
143144
Denis Cherednichenko
144145
Denis Kirisov
145146
Denivy Braiam Rück

changelog/15009.bugfix.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
:func:`pytest.approx` now reports mismatch details for mappings whose values are
2+
not numbers.
3+
4+
Previously an unequal pair of non-numeric values under the same key -- for
5+
example ``{"item": "a"} == approx({"item": "b"})`` -- made the diff calculation
6+
raise ``TypeError``, so the assertion detail table was replaced by a
7+
"representation of details failed" message. The equivalent sequence comparison
8+
already handled this.
9+
10+
``decimal.FloatOperation`` is re-raised ahead of that handler, matching the
11+
sequence path, so a mapping that mixes Decimals with floats still surfaces the
12+
trap instead of reporting the smaller of two differences as the maximum.

src/_pytest/approx.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,12 @@ def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]:
352352
/ approx_value.expected
353353
),
354354
)
355-
except ZeroDivisionError:
355+
# decimal.FloatOperation subclasses TypeError, so it would be
356+
# caught below and reported as a missing difference.
357+
except FloatOperation:
358+
raise
359+
# Ignore non-numbers for the diff calculations (#15009).
360+
except (ZeroDivisionError, TypeError):
356361
pass
357362
different_ids.append(approx_key)
358363

testing/python/approx.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,57 @@ def test_mixed_decimal_and_float_sequence_does_not_hide_float_operation(
12831283
with pytest.raises(decimal.FloatOperation):
12841284
approx(expected)._repr_compare(other)
12851285

1286+
def test_mixed_decimal_and_float_mapping_does_not_hide_float_operation(
1287+
self, monkeypatch
1288+
) -> None:
1289+
"""The mapping path must re-raise FloatOperation like the sequence one.
1290+
1291+
Catching TypeError to skip non-numbers also catches
1292+
decimal.FloatOperation, and the reported maximum then silently becomes
1293+
the smaller of the two differences. Insertion order decides which,
1294+
so the wrong number is not even stable.
1295+
"""
1296+
expected = {"a": Decimal(9), "b": 9.0}
1297+
other = {"a": Decimal(1), "b": 2.0}
1298+
assert approx(expected)._repr_compare(other)[1] == "Max absolute difference: 8"
1299+
1300+
monkeypatch.setitem(decimal.getcontext().traps, decimal.FloatOperation, True)
1301+
with pytest.raises(decimal.FloatOperation):
1302+
approx(expected)._repr_compare(other)
1303+
1304+
def test_mixed_mapping(self, assert_approx_raises_regex) -> None:
1305+
"""Approx should work on mappings that also contain non-numbers (#15009)."""
1306+
assert_approx_raises_regex(
1307+
{"a": 1.1, "b": 2, "c": "word"},
1308+
{"a": 1.0, "b": 2, "c": "different"},
1309+
[
1310+
r"",
1311+
r" comparison failed. Mismatched elements: 2 / 3:",
1312+
rf" Max absolute difference: {SOME_FLOAT}",
1313+
rf" Max relative difference: {SOME_FLOAT}",
1314+
r" Index \| Obtained\s+\| Expected\s*",
1315+
r"\s*a\s*\|\s*1\.1\s*\|\s*1\.0\s*±\s*1\.0e\-06\s*",
1316+
r"\s*c\s*\|\s*word\s*\|\s*different\s*",
1317+
],
1318+
verbosity_level=2,
1319+
)
1320+
1321+
def test_mapping_of_only_strings(self, assert_approx_raises_regex) -> None:
1322+
"""A mapping whose values are all non-numeric still reports mismatches (#15009)."""
1323+
assert_approx_raises_regex(
1324+
{"item": "a"},
1325+
{"item": "b"},
1326+
[
1327+
r"",
1328+
r" comparison failed. Mismatched elements: 1 / 1:",
1329+
r" Max absolute difference: -inf",
1330+
r" Max relative difference: -inf",
1331+
r" Index \| Obtained\s*\| Expected\s*",
1332+
r"\s*item\s*\|\s*a\s*\|\s*b\s*",
1333+
],
1334+
verbosity_level=2,
1335+
)
1336+
12861337
def test_decimal_nan_tolerance_raises_value_error(self) -> None:
12871338
"""A Decimal NaN tolerance must not escape as decimal.InvalidOperation."""
12881339
nan_abs = approx(Decimal(1), abs=Decimal("NaN"))

0 commit comments

Comments
 (0)