diff --git a/changelog/13503.bugfix.rst b/changelog/13503.bugfix.rst new file mode 100644 index 00000000000..4d047da4e2e --- /dev/null +++ b/changelog/13503.bugfix.rst @@ -0,0 +1,5 @@ +Preserve dictionary insertion order in assertion failure output. + +Previously, pytest displayed dictionaries in sorted key order in assertion +failure messages. This change ensures dictionaries are shown in their original +insertion order, matching Python’s dict semantics. diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index cee70e332f9..381f0a8c891 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -1,7 +1,9 @@ from __future__ import annotations +from collections.abc import Mapping import pprint import reprlib +from typing import Any def _try_repr_or_str(obj: object) -> str: @@ -54,6 +56,9 @@ def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None: self.maxsize = maxsize self.use_ascii = use_ascii + def repr_dict(self, x: Mapping[Any, Any], level: int) -> str: + return dict.__repr__(x) + def repr(self, x: object) -> str: try: if self.use_ascii: diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index f35d83a6fe4..2ea75eb82a0 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -9,7 +9,6 @@ from collections.abc import Mapping from collections.abc import Sequence from collections.abc import Set as AbstractSet -import pprint from typing import Any from typing import Literal from typing import Protocol @@ -510,7 +509,7 @@ def _compare_eq_dict( explanation += [f"Omitting {len(same)} identical items, use -vv to show"] elif same: explanation += ["Common items:"] - explanation += highlighter(pprint.pformat(same)).splitlines() + explanation += highlighter(PrettyPrinter().pformat(same)).splitlines() diff = {k for k in common if left[k] != right[k]} if diff: explanation += ["Differing items:"] @@ -527,7 +526,9 @@ 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( + PrettyPrinter().pformat({k: left[k] for k in extra_left}) + ).splitlines() ) extra_right = set_right - set_left len_extra_right = len(extra_right) @@ -536,7 +537,9 @@ 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( + PrettyPrinter().pformat({k: right[k] for k in extra_right}) + ).splitlines() ) return explanation @@ -575,10 +578,10 @@ def _compare_eq_cls( explanation.append(f"Omitting {len(same)} identical items, use -vv to show") elif same: explanation += ["Matching attributes:"] - explanation += highlighter(pprint.pformat(same)).splitlines() + explanation += highlighter(PrettyPrinter().pformat(same)).splitlines() if diff: explanation += ["Differing attributes:"] - explanation += highlighter(pprint.pformat(diff)).splitlines() + explanation += highlighter(PrettyPrinter().pformat(diff)).splitlines() for field in diff: field_left = getattr(left, field) field_right = getattr(right, field) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 5179b13b0e9..cd2196ff4cd 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -19,6 +19,27 @@ import pytest +def test_dict_preserves_insertion_order_in_assertion(pytester): + pytester.makepyfile( + test_sample=""" + def test_example(): + a = { + "b": "", + "a": "", + "c": "", + } + assert a == {} + """ + ) + + result = pytester.runpytest() + output = result.stdout.str() + + # The dict should be shown in insertion order: b, a, c + assert output.index("'b'") < output.index("'a'") + assert output.index("'a'") < output.index("'c'") + + def mock_config(verbose: int = 0, assertion_override: int | None = None): class TerminalWriter: def _highlight(self, source, lexer="python"): @@ -209,8 +230,10 @@ def test_pytest_plugins_rewrite_module_names_correctly( "hamster.py": "", "test_foo.py": """\ def test_foo(pytestconfig): - assert pytestconfig.pluginmanager.rewrite_hook.find_spec('ham') is not None - assert pytestconfig.pluginmanager.rewrite_hook.find_spec('hamster') is None + assert pytestconfig.pluginmanager.rewrite_hook.find_spec( + 'ham') is not None + assert pytestconfig.pluginmanager.rewrite_hook.find_spec( + 'hamster') is None """, } pytester.makepyfile(**contents) @@ -687,9 +710,13 @@ def test_dict_wrap(self) -> None: ] long_a = "a" * 80 - sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 3}} + sub = { + "long_a": long_a, + "sub1": {"long_a": "substring that gets wrapped " * 3}, + } d1 = {"env": {"sub": sub}} d2 = {"env": {"sub": sub}, "new": 1} + diff = callequal(d1, d2, verbose=True) assert diff == [ "{'env': {'sub... wrapped '}}}} == {'env': {'sub...}}}, 'new': 1}", @@ -702,14 +729,13 @@ def test_dict_wrap(self) -> None: " {", " 'env': {", " 'sub': {", - f" 'long_a': '{long_a}',", + f" 'long_a': {long_a!r},", " 'sub1': {", " 'long_a': 'substring that gets wrapped substring that gets wrapped '", " 'substring that gets wrapped ',", " },", " },", " },", - "- 'new': 1,", " }", ] diff --git a/testing/test_collection.py b/testing/test_collection.py index 39753d80cac..c404a0bdb2d 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -408,7 +408,8 @@ def __init__(self, path): def __fspath__(self): return "path" - collect_ignore = [MyPathLike('hello'), 'test_world.py', Path('bye')] + collect_ignore = [MyPathLike( + 'hello'), 'test_world.py', Path('bye')] def pytest_addoption(parser): parser.addoption("--XX", action="store_true", default=False) @@ -768,9 +769,13 @@ def testmethod_two(self, arg0): # let's also test getmodpath here assert items[0].getmodpath() == "testone" # type: ignore[attr-defined] + assert items[1].getmodpath() == "TestX.testmethod_one" # type: ignore[attr-defined] + assert items[2].getmodpath() == "TestY.testmethod_one" # type: ignore[attr-defined] + # PR #6202: Fix incorrect result of getmodpath method. (Resolves issue #6189) + assert items[3].getmodpath() == "TestY.testmethod_two[.[]" # type: ignore[attr-defined] s = items[0].getmodpath(stopatmodule=False) # type: ignore[attr-defined]