From e85b99bd1d998c588145a8235a2b424ad2c0d869 Mon Sep 17 00:00:00 2001 From: Charles-Meldhine Madi Mnemoi Date: Wed, 15 Oct 2025 14:51:36 +0200 Subject: [PATCH 01/10] fix(testing): `pytest.approx` returns a clearer error mesage when comparing mappings with different keys --- testing/python/approx.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/testing/python/approx.py b/testing/python/approx.py index f870b9bd4d8..5cc7dc48727 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -1,21 +1,18 @@ # mypy: allow-untyped-defs from __future__ import annotations -from contextlib import contextmanager import decimal -from decimal import Decimal -from fractions import Fraction -from math import inf -from math import nan -from math import sqrt import operator -from operator import eq -from operator import ne import re +from contextlib import contextmanager +from decimal import Decimal +from fractions import Fraction +from math import inf, nan, sqrt +from operator import eq, ne +import pytest from _pytest.pytester import Pytester from _pytest.python_api import _recursive_sequence_map -import pytest from pytest import approx From 610e1d34d0e54256e95c53d3c7948575c95e9522 Mon Sep 17 00:00:00 2001 From: Charles-Meldhine Madi Mnemoi Date: Wed, 15 Oct 2025 14:54:55 +0200 Subject: [PATCH 02/10] docs: add changelog entry --- changelog/13816.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/13816.bugfix.rst diff --git a/changelog/13816.bugfix.rst b/changelog/13816.bugfix.rst new file mode 100644 index 00000000000..432fc519ed5 --- /dev/null +++ b/changelog/13816.bugfix.rst @@ -0,0 +1 @@ +Fixed :func:`pytest.approx` which now returns a clearer error message when comparing mappings with different keys. From a56357d4e4a1a1d6e51640969306f3c31634f67e Mon Sep 17 00:00:00 2001 From: Charles-Meldhine Madi Mnemoi Date: Wed, 15 Oct 2025 13:02:39 +0200 Subject: [PATCH 03/10] fix(testing): `pytest.approx` correctly take account Mapping keys order to compare them --- src/_pytest/python_api.py | 5 ++--- testing/python/approx.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 1e389eb0663..e9dfdc857e2 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -256,9 +256,8 @@ def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]: max_abs_diff = -math.inf max_rel_diff = -math.inf different_ids = [] - for (approx_key, approx_value), other_value in zip( - approx_side_as_map.items(), other_side.values(), strict=True - ): + for approx_key, approx_value in approx_side_as_map.items(): + other_value = other_side.get(approx_key, None) if approx_value != other_value: if approx_value.expected is not None and other_value is not None: try: diff --git a/testing/python/approx.py b/testing/python/approx.py index 5cc7dc48727..e02c4704dad 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -1059,6 +1059,17 @@ def test_approx_dicts_with_mismatch_on_keys(self) -> None: ): assert actual == approx(expected) + def test_approx_on_unordered_mapping_with_mismatch(self) -> None: + """https://github.com/pytest-dev/pytest/pull/12445""" + expected = {"a": 1, "c": 3} + actual = {"c": 5, "a": 1} + + with pytest.raises( + AssertionError, + match="Mismatched elements: 1 / 2:\n Max absolute difference: 2\n", + ): + assert expected == approx(actual) + class MyVec3: # incomplete """sequence like""" From fbda803d757e7d72dd3927d3b5ddc4331d669f90 Mon Sep 17 00:00:00 2001 From: Charles-Meldhine Madi Mnemoi Date: Wed, 15 Oct 2025 13:06:44 +0200 Subject: [PATCH 04/10] doc(changelog): add entry for fixing `pytest.approx` to account for Mapping keys order --- changelog/12444.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/12444.bugfix.rst diff --git a/changelog/12444.bugfix.rst b/changelog/12444.bugfix.rst new file mode 100644 index 00000000000..146cfc7ab24 --- /dev/null +++ b/changelog/12444.bugfix.rst @@ -0,0 +1 @@ +Fixed :func:`pytest.approx` which now correctly takes account Mapping keys order to compare them. From 48f1ed3986e650b3733f55d8e5c38004c141191c Mon Sep 17 00:00:00 2001 From: Charles-Meldhine Madi Mnemoi Date: Tue, 2 Dec 2025 15:39:40 +0100 Subject: [PATCH 05/10] remove deprecated changelog entry --- changelog/13816.bugfix.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog/13816.bugfix.rst diff --git a/changelog/13816.bugfix.rst b/changelog/13816.bugfix.rst deleted file mode 100644 index 432fc519ed5..00000000000 --- a/changelog/13816.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed :func:`pytest.approx` which now returns a clearer error message when comparing mappings with different keys. From 6fc2603bad4d195bd90741993249dae21e7074db Mon Sep 17 00:00:00 2001 From: Charles-Meldhine Madi Mnemoi Date: Tue, 2 Dec 2025 15:48:40 +0100 Subject: [PATCH 06/10] apply ruff fixes --- testing/python/approx.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/testing/python/approx.py b/testing/python/approx.py index e02c4704dad..1e59e61c43a 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -1,18 +1,21 @@ # mypy: allow-untyped-defs from __future__ import annotations -import decimal -import operator -import re from contextlib import contextmanager +import decimal from decimal import Decimal from fractions import Fraction -from math import inf, nan, sqrt -from operator import eq, ne +from math import inf +from math import nan +from math import sqrt +import operator +from operator import eq +from operator import ne +import re -import pytest from _pytest.pytester import Pytester from _pytest.python_api import _recursive_sequence_map +import pytest from pytest import approx From da1addb005b6ed3ba8102e3817ee2bb50d912df6 Mon Sep 17 00:00:00 2001 From: Charles-Meldhine Madi Mnemoi Date: Tue, 13 Jan 2026 21:04:08 +0100 Subject: [PATCH 07/10] refactor: Use `pytester` to verify output thoroughly --- testing/python/approx.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/testing/python/approx.py b/testing/python/approx.py index 1e59e61c43a..651262f831c 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -1062,16 +1062,28 @@ def test_approx_dicts_with_mismatch_on_keys(self) -> None: ): assert actual == approx(expected) - def test_approx_on_unordered_mapping_with_mismatch(self) -> None: - """https://github.com/pytest-dev/pytest/pull/12445""" - expected = {"a": 1, "c": 3} - actual = {"c": 5, "a": 1} + def test_approx_on_unordered_mapping_with_mismatch( + self, pytester: Pytester + ) -> None: + """https://github.com/pytest-dev/pytest/issues/12444""" + pytester.makepyfile( + """ + import pytest - with pytest.raises( - AssertionError, - match="Mismatched elements: 1 / 2:\n Max absolute difference: 2\n", - ): - assert expected == approx(actual) + def test_approx_on_unordered_mapping_with_mismatch(): + expected = {"a": 1, "b": 2, "c": 3, "d": 4} + actual = {"d": 4, "c": 5, "a": 8, "b": 2} + assert actual == pytest.approx(expected) + """ + ) + result = pytester.runpytest() + result.assert_outcomes(failed=1) + result.stdout.fnmatch_lines( + [ + "*comparison failed.**Mismatched elements: 2 / 4:*", + "*Max absolute difference: 7*", + ] + ) class MyVec3: # incomplete From d78a7d64574f4f88cc0fe56ba054714cdde0562b Mon Sep 17 00:00:00 2001 From: Charles-Meldhine Madi Mnemoi Date: Tue, 13 Jan 2026 21:05:33 +0100 Subject: [PATCH 08/10] refactor: remove useless `set` usage to compare unique mapping keys --- src/_pytest/python_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index e9dfdc857e2..3a5a6bb1b0a 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -242,7 +242,7 @@ def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]: f"Lengths: {len(self.expected)} and {len(other_side)}", ] - if set(self.expected.keys()) != set(other_side.keys()): + if self.expected.keys() != other_side.keys(): return [ "comparison failed.", f"Mappings has different keys: expected {self.expected.keys()} but got {other_side.keys()}", From 2989ec5bf6601164aa67ce3e7b644409847c6c66 Mon Sep 17 00:00:00 2001 From: Charles-Meldhine Madi Mnemoi Date: Tue, 13 Jan 2026 21:06:29 +0100 Subject: [PATCH 09/10] refactor: change dictionary get method to direct access for ApproxMapping comparison --- src/_pytest/python_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 3a5a6bb1b0a..bab70aa4a8c 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -257,7 +257,7 @@ def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]: max_rel_diff = -math.inf different_ids = [] for approx_key, approx_value in approx_side_as_map.items(): - other_value = other_side.get(approx_key, None) + other_value = other_side[approx_key] if approx_value != other_value: if approx_value.expected is not None and other_value is not None: try: From ab3bd2f4b86afe2fd8916f2bef2493997f1d2de0 Mon Sep 17 00:00:00 2001 From: Charles-Meldhine Madi Mnemoi Date: Fri, 16 Jan 2026 22:23:17 +0100 Subject: [PATCH 10/10] test: more thourough testing --- testing/python/approx.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/testing/python/approx.py b/testing/python/approx.py index 651262f831c..481df80565c 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -1082,9 +1082,26 @@ def test_approx_on_unordered_mapping_with_mismatch(): [ "*comparison failed.**Mismatched elements: 2 / 4:*", "*Max absolute difference: 7*", + "*Index | Obtained | Expected *", + "* a * | 8 * | 1 *", + "* c * | 5 * | 3 *", ] ) + def test_approx_on_unordered_mapping_matching(self, pytester: Pytester) -> None: + """https://github.com/pytest-dev/pytest/issues/12444""" + pytester.makepyfile( + """ + import pytest + def test_approx_on_unordered_mapping_matching(): + expected = {"a": 1, "b": 2, "c": 3, "d": 4} + actual = {"d": 4, "c": 3, "a": 1, "b": 2} + assert actual == pytest.approx(expected) + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + class MyVec3: # incomplete """sequence like"""