diff --git a/changelog/13503.improvement.rst b/changelog/13503.improvement.rst new file mode 100644 index 00000000000..82e598ed4a3 --- /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 diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index 28f06909206..bf14fd77ac7 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. @@ -75,6 +76,9 @@ def __init__( depth The maximum depth to print out nested structures. + sort_dicts + If true, dict keys are sorted. + """ if indent < 0: raise ValueError("indent must be >= 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..dd5572d3e8a 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,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 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 +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 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/io/test_pprint.py b/testing/io/test_pprint.py index 1326ef34b2e..6a553cedc09 100644 --- a/testing/io/test_pprint.py +++ b/testing/io/test_pprint.py @@ -406,3 +406,38 @@ 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 diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index 741a6ca82d0..240a0006dcf 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -198,6 +198,85 @@ 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 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 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 = { + "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 {'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: + 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 + '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 nested dicts and check order of diff", + ), pytest.param( """ def test_this():