From 04673d455cda89b7c08460bb1b1e1aaf31f8b268 Mon Sep 17 00:00:00 2001 From: Ritzzer764 Date: Sat, 5 Jul 2025 14:58:06 +0530 Subject: [PATCH 01/11] fix: display output failure message in insertion order of dictionary --- src/_pytest/_io/pprint.py | 16 +++++++-- src/_pytest/assertion/util.py | 27 ++++++++------- testing/test_error_diffs.py | 64 +++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 14 deletions(-) diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index 28f06909206..5935d108a65 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -62,6 +62,7 @@ def __init__( indent: int = 4, width: int = 80, depth: int | None = None, + sort_dicts: bool = True, ) -> None: """Handle pretty printing operations onto a stream using a set of configured parameters. @@ -74,6 +75,9 @@ def __init__( depth The maximum depth to print out nested structures. + + sort_dicts + If true, dict keys are sorted. """ if indent < 0: @@ -85,6 +89,7 @@ def __init__( self._depth = depth self._indent_per_level = indent self._width = width + self._sort_dicts = sort_dicts def pformat(self, object: Any) -> str: sio = _StringIO() @@ -162,7 +167,10 @@ def _pprint_dict( ) -> None: write = stream.write write("{") - items = sorted(object.items(), key=_safe_tuple) + if self._sort_dicts: + items = sorted(object.items(), key=_safe_tuple) + else: + items = object.items() self._format_dict_items(items, stream, indent, allowance, context, level) write("}") @@ -608,7 +616,11 @@ def _safe_repr( components: list[str] = [] append = components.append level += 1 - for k, v in sorted(object.items(), key=_safe_tuple): + if self._sort_dicts: + items = sorted(object.items(), key=_safe_tuple) + else: + items = object.items() + for k, v in items: krepr = self._safe_repr(k, context, maxlevels, level) vrepr = self._safe_repr(v, context, maxlevels, level) append(f"{krepr}: {vrepr}") diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index c545e6cd20c..9262a374c28 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -348,8 +348,8 @@ def _compare_eq_iterable( # dynamic import to speedup pytest import difflib - left_formatting = PrettyPrinter().pformat(left).splitlines() - right_formatting = PrettyPrinter().pformat(right).splitlines() + left_formatting = PrettyPrinter(sort_dicts=False).pformat(left).splitlines() + right_formatting = PrettyPrinter(sort_dicts=False).pformat(right).splitlines() explanation = ["", "Full diff:"] # "right" is the expected base against which we compare "left", @@ -505,21 +505,24 @@ def _compare_eq_dict( set_left = set(left) set_right = set(right) common = set_left.intersection(set_right) - same = {k: left[k] for k in common if left[k] == right[k]} + same = {k: left[k] for k in left if k in right and left[k] == right[k]} if same and verbose < 2: explanation += [f"Omitting {len(same)} identical items, use -vv to show"] elif same: explanation += ["Common items:"] - explanation += highlighter(pprint.pformat(same)).splitlines() + # Common items are displayed in the order of the left dict + explanation += highlighter(pprint.pformat(same, sort_dicts=False)).splitlines() diff = {k for k in common if left[k] != right[k]} if diff: explanation += ["Differing items:"] - for k in diff: - explanation += [ - highlighter(saferepr({k: left[k]})) - + " != " - + highlighter(saferepr({k: right[k]})) - ] + # Differing items are displayed in the order of the left dict + for k in left: + if k in diff: + explanation += [ + highlighter(saferepr({k: left[k]})) + + " != " + + highlighter(saferepr({k: right[k]})) + ] extra_left = set_left - set_right len_extra_left = len(extra_left) if len_extra_left: @@ -527,7 +530,7 @@ def _compare_eq_dict( f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:" ) explanation.extend( - highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines() + highlighter(pprint.pformat({k: left[k] for k in left if k in extra_left}, sort_dicts=False)).splitlines() ) extra_right = set_right - set_left len_extra_right = len(extra_right) @@ -536,7 +539,7 @@ def _compare_eq_dict( f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:" ) explanation.extend( - highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines() + highlighter(pprint.pformat({k: right[k] for k in right if k in extra_right}, sort_dicts=False)).splitlines() ) return explanation diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index 741a6ca82d0..61534657723 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -198,6 +198,70 @@ def test_this(): """, id="Compare dicts with differing items", ), + pytest.param( + """ + def test_this(): + result = {'d': 4, 'c': 3, 'b': 2, 'a': 1} + expected = {'d': 4, 'c': 3, 'e': 5} + assert result == expected + """, + """ + > assert result == expected + E AssertionError: assert {'d': 4, 'c': 3, 'b': 2, 'a': 1} == {'d': 4, 'c': 3, 'e': 5} + E + E Common items: + E {'d': 4, 'c': 3} + E Left contains 2 more items: + E {'b': 2, 'a': 1} + E Right contains 1 more item: + E {'e': 5} + E + E Full diff: + E { + E 'd': 4, + E 'c': 3, + E - 'e': 5, + E ? ^ ^ + E + 'b': 2, + E ? ^ ^ + E + 'a': 1, + E } + """, + id="Compare dicts and check order of diff", + ), + pytest.param( + """ + def test_this(): + result = {'c': 3, 'd': 4, 'b': 2, 'a': 1} + expected = {'d': 5, 'c': 3, 'b': 1} + assert result == expected + """, + """ + > assert result == expected + E AssertionError: assert {'c': 3, 'd': 4, 'b': 2, 'a': 1} == {'d': 5, 'c': 3, 'b': 1} + E + E Common items: + E {'c': 3} + E Differing items: + E {'d': 4} != {'d': 5} + E {'b': 2} != {'b': 1} + E Left contains 1 more item: + E {'a': 1} + E + E Full diff: + E { + E - 'd': 5, + E 'c': 3, + E + 'd': 4, + E - 'b': 1, + E ? ^ + E + 'b': 2, + E ? ^ + E + 'a': 1, + E } + """, + id="Compare dicts with different order and values", + ), pytest.param( """ def test_this(): From d25b6995cefefff2a338fd1131e396092f24aa8e Mon Sep 17 00:00:00 2001 From: Ritzzer764 Date: Sat, 5 Jul 2025 15:10:14 +0530 Subject: [PATCH 02/11] chore: add changelog message --- changelog/13503.improvement.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog/13503.improvement.rst diff --git a/changelog/13503.improvement.rst b/changelog/13503.improvement.rst new file mode 100644 index 00000000000..d4a3bd9ef5e --- /dev/null +++ b/changelog/13503.improvement.rst @@ -0,0 +1,4 @@ +Fixed the order of dictionary keys in assertion failure messages. +Previously, dictionary diffs were shown in alphabetical order, regardless of how the keys appeared in the original dicts. +Now, common keys are shown in the insertion order of the left-hand dictionary, +and differing keys are shown in the insertion order of their respective dictionaries \ No newline at end of file From d4a58da815ecce8f4182a6db9de1c7f4d4464a77 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 09:39:51 +0000 Subject: [PATCH 03/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/_io/pprint.py | 2 +- src/_pytest/assertion/util.py | 12 ++++++++++-- testing/test_error_diffs.py | 8 ++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index 5935d108a65..bf14fd77ac7 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -75,7 +75,7 @@ def __init__( depth The maximum depth to print out nested structures. - + sort_dicts If true, dict keys are sorted. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 9262a374c28..dd5572d3e8a 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -530,7 +530,11 @@ def _compare_eq_dict( f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:" ) explanation.extend( - highlighter(pprint.pformat({k: left[k] for k in left if k in extra_left}, sort_dicts=False)).splitlines() + highlighter( + pprint.pformat( + {k: left[k] for k in left if k in extra_left}, sort_dicts=False + ) + ).splitlines() ) extra_right = set_right - set_left len_extra_right = len(extra_right) @@ -539,7 +543,11 @@ def _compare_eq_dict( f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:" ) explanation.extend( - highlighter(pprint.pformat({k: right[k] for k in right if k in extra_right}, sort_dicts=False)).splitlines() + highlighter( + pprint.pformat( + {k: right[k] for k in right if k in extra_right}, sort_dicts=False + ) + ).splitlines() ) return explanation diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index 61534657723..edb9624c559 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -208,14 +208,14 @@ def test_this(): """ > assert result == expected E AssertionError: assert {'d': 4, 'c': 3, 'b': 2, 'a': 1} == {'d': 4, 'c': 3, 'e': 5} - E + E E Common items: E {'d': 4, 'c': 3} E Left contains 2 more items: E {'b': 2, 'a': 1} E Right contains 1 more item: E {'e': 5} - E + E E Full diff: E { E 'd': 4, @@ -239,7 +239,7 @@ def test_this(): """ > assert result == expected E AssertionError: assert {'c': 3, 'd': 4, 'b': 2, 'a': 1} == {'d': 5, 'c': 3, 'b': 1} - E + E E Common items: E {'c': 3} E Differing items: @@ -247,7 +247,7 @@ def test_this(): E {'b': 2} != {'b': 1} E Left contains 1 more item: E {'a': 1} - E + E E Full diff: E { E - 'd': 5, From bb9549146958fd32f0cd6860c6518a377d4ac5d8 Mon Sep 17 00:00:00 2001 From: Ritzzer764 Date: Sat, 5 Jul 2025 15:24:35 +0530 Subject: [PATCH 04/11] fix: test case --- testing/test_error_diffs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index edb9624c559..61534657723 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -208,14 +208,14 @@ def test_this(): """ > assert result == expected E AssertionError: assert {'d': 4, 'c': 3, 'b': 2, 'a': 1} == {'d': 4, 'c': 3, 'e': 5} - E + E E Common items: E {'d': 4, 'c': 3} E Left contains 2 more items: E {'b': 2, 'a': 1} E Right contains 1 more item: E {'e': 5} - E + E E Full diff: E { E 'd': 4, @@ -239,7 +239,7 @@ def test_this(): """ > assert result == expected E AssertionError: assert {'c': 3, 'd': 4, 'b': 2, 'a': 1} == {'d': 5, 'c': 3, 'b': 1} - E + E E Common items: E {'c': 3} E Differing items: @@ -247,7 +247,7 @@ def test_this(): E {'b': 2} != {'b': 1} E Left contains 1 more item: E {'a': 1} - E + E E Full diff: E { E - 'd': 5, From ce769dbe2451086dd8c8b2a87da7b0f06109b2da Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 09:42:03 +0000 Subject: [PATCH 05/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog/13503.improvement.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/changelog/13503.improvement.rst b/changelog/13503.improvement.rst index d4a3bd9ef5e..82e598ed4a3 100644 --- a/changelog/13503.improvement.rst +++ b/changelog/13503.improvement.rst @@ -1,4 +1,4 @@ -Fixed the order of dictionary keys in assertion failure messages. -Previously, dictionary diffs were shown in alphabetical order, regardless of how the keys appeared in the original dicts. -Now, common keys are shown in the insertion order of the left-hand dictionary, -and differing keys are shown in the insertion order of their respective dictionaries \ No newline at end of file +Fixed the order of dictionary keys in assertion failure messages. +Previously, dictionary diffs were shown in alphabetical order, regardless of how the keys appeared in the original dicts. +Now, common keys are shown in the insertion order of the left-hand dictionary, +and differing keys are shown in the insertion order of their respective dictionaries From 969407c4fbb7a515a0a8c3fe86a2924613a5ddf9 Mon Sep 17 00:00:00 2001 From: Ritzzer764 Date: Sat, 5 Jul 2025 15:29:41 +0530 Subject: [PATCH 06/11] test: fix test cases --- testing/test_error_diffs.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index 61534657723..f538d7f83cf 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -208,14 +208,12 @@ def test_this(): """ > assert result == expected E AssertionError: assert {'d': 4, 'c': 3, 'b': 2, 'a': 1} == {'d': 4, 'c': 3, 'e': 5} - E E Common items: E {'d': 4, 'c': 3} E Left contains 2 more items: E {'b': 2, 'a': 1} E Right contains 1 more item: E {'e': 5} - E E Full diff: E { E 'd': 4, @@ -239,7 +237,6 @@ def test_this(): """ > assert result == expected E AssertionError: assert {'c': 3, 'd': 4, 'b': 2, 'a': 1} == {'d': 5, 'c': 3, 'b': 1} - E E Common items: E {'c': 3} E Differing items: @@ -247,7 +244,6 @@ def test_this(): E {'b': 2} != {'b': 1} E Left contains 1 more item: E {'a': 1} - E E Full diff: E { E - 'd': 5, From 36733f059c6fd8887dea709108ae2eaa41621bbb Mon Sep 17 00:00:00 2001 From: Ritzzer764 Date: Sat, 5 Jul 2025 16:32:16 +0530 Subject: [PATCH 07/11] test: add test case nested dicts --- testing/test_error_diffs.py | 56 ++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index f538d7f83cf..59b97b92ee7 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -230,33 +230,51 @@ def test_this(): pytest.param( """ def test_this(): - result = {'c': 3, 'd': 4, 'b': 2, 'a': 1} - expected = {'d': 5, 'c': 3, 'b': 1} + result = { + "b": {"m": 3, "n": 4}, + "a": {"x": 1, "y": 2}, + "c": {"k": 5, "l": 6} + } + expected = { + "c": {"l": 6, "k": 5}, + "e": {"v": 8}, + "d": {"u": 7} + } assert result == expected """, """ > assert result == expected - E AssertionError: assert {'c': 3, 'd': 4, 'b': 2, 'a': 1} == {'d': 5, 'c': 3, 'b': 1} + E AssertionError: assert {'b': {'m': 3, 'n': 4}, 'a': {'x': 1, 'y': 2}, 'c': {'k': 5, 'l': 6}} == {'c': {'l': 6, 'k': 5}, 'e': {'v': 8}, 'd': {'u': 7}} E Common items: - E {'c': 3} - E Differing items: - E {'d': 4} != {'d': 5} - E {'b': 2} != {'b': 1} - E Left contains 1 more item: - E {'a': 1} + E {'c': {'k': 5, 'l': 6}} + E Left contains 2 more items: + E {'b': {'m': 3, 'n': 4}, 'a': {'x': 1, 'y': 2}} + E Right contains 2 more items: + E {'e': {'v': 8}, 'd': {'u': 7}} E Full diff: E { - E - 'd': 5, - E 'c': 3, - E + 'd': 4, - E - 'b': 1, - E ? ^ - E + 'b': 2, - E ? ^ - E + 'a': 1, + E + 'b': { + E + 'm': 3, + E + 'n': 4, + E + }, + E + 'a': { + E + 'x': 1, + E + 'y': 2, + E + }, + E 'c': { + E + 'k': 5, + E 'l': 6, + E - 'k': 5, + E - }, + E - 'e': { + E - 'v': 8, + E - }, + E - 'd': { + E - 'u': 7, + E }, E } - """, - id="Compare dicts with different order and values", + """, + id="Compare nested dicts and check order of diff", ), pytest.param( """ From 816e755707932695f191fbe1e8e9dcb437b99f5a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 13:07:43 +0000 Subject: [PATCH 08/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_error_diffs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index 59b97b92ee7..bb63b30f4d9 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -274,7 +274,7 @@ def test_this(): E }, E } """, - id="Compare nested dicts and check order of diff", + id="Compare nested dicts and check order of diff", ), pytest.param( """ From 66196904f3f3837056f46ceb322b7499848d7a08 Mon Sep 17 00:00:00 2001 From: Ritzzer764 Date: Sun, 20 Jul 2025 18:45:32 +0530 Subject: [PATCH 09/11] style: fix fmt --- testing/test_error_diffs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index bb63b30f4d9..240a0006dcf 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -244,7 +244,8 @@ def test_this(): """, """ > assert result == expected - E AssertionError: assert {'b': {'m': 3, 'n': 4}, 'a': {'x': 1, 'y': 2}, 'c': {'k': 5, 'l': 6}} == {'c': {'l': 6, 'k': 5}, 'e': {'v': 8}, 'd': {'u': 7}} + E AssertionError: assert {'b': {'m': 3, 'n': 4}, 'a': {'x': 1, 'y': 2}, 'c': {'k': 5, 'l': 6}} \ +== {'c': {'l': 6, 'k': 5}, 'e': {'v': 8}, 'd': {'u': 7}} E Common items: E {'c': {'k': 5, 'l': 6}} E Left contains 2 more items: From ffb5e46167b5d51466f04292611de77f320abe8f Mon Sep 17 00:00:00 2001 From: Ritzzer764 Date: Sun, 20 Jul 2025 19:20:38 +0530 Subject: [PATCH 10/11] test: add test case for pprint --- testing/io/test_pprint.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/testing/io/test_pprint.py b/testing/io/test_pprint.py index 1326ef34b2e..afa2574b116 100644 --- a/testing/io/test_pprint.py +++ b/testing/io/test_pprint.py @@ -406,3 +406,37 @@ class DataclassWithTwoItems: ) def test_consistent_pretty_printer(data: Any, expected: str) -> None: assert PrettyPrinter().pformat(data) == textwrap.dedent(expected).strip() + +@pytest.mark.parametrize( + ("sort_dicts"), + ( + pytest.param(True, id="sort_dicts-True"), + pytest.param(False, id="sort_dicts-False"), + ), +) +def test_pretty_printer_sort_dicts(sort_dicts: bool) -> None: + data = { + "b": 2, + "a": 1, + "c": 3, + } + + if sort_dicts: + expected = textwrap.dedent(""" + { + 'a': 1, + 'b': 2, + 'c': 3, + } + """).strip() + else: + expected = textwrap.dedent(""" + { + 'b': 2, + 'a': 1, + 'c': 3, + } + """).strip() + + actual = PrettyPrinter(sort_dicts=sort_dicts).pformat(data) + assert actual == expected From 004825824a612277d452488b01af76ce33bb85d9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 13:51:03 +0000 Subject: [PATCH 11/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/io/test_pprint.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/io/test_pprint.py b/testing/io/test_pprint.py index afa2574b116..6a553cedc09 100644 --- a/testing/io/test_pprint.py +++ b/testing/io/test_pprint.py @@ -407,6 +407,7 @@ class DataclassWithTwoItems: def test_consistent_pretty_printer(data: Any, expected: str) -> None: assert PrettyPrinter().pformat(data) == textwrap.dedent(expected).strip() + @pytest.mark.parametrize( ("sort_dicts"), (