Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6096720
Preserve dict insertion order in assertion repr
anuranjansb Dec 20, 2025
692b178
Preserve dict insertion order in assertion repr output
anuranjansb Dec 20, 2025
dd4ca0a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 20, 2025
b5e4ea9
Add changelog entry for dict assertion order fix
anuranjansb Dec 20, 2025
f0ff77b
Fix mypy typing for dict repr override
anuranjansb Dec 20, 2025
5cb161b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 20, 2025
ee03ecf
Fix mypy ignores for getmodpath tests
anuranjansb Dec 20, 2025
7dec4cc
Fix mypy typing for SafeRepr.repr_dict
anuranjansb Dec 20, 2025
1a7ecce
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 20, 2025
7874c8e
Update test_dict_wrap expected output after dict assertion change
anuranjansb Dec 21, 2025
8330433
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 21, 2025
9bfa369
Fix ruff errors for typing imports and test formatting
anuranjansb Dec 21, 2025
25d6e91
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 21, 2025
b461212
Fi mypy attr-defined ignores for getmodpath test
anuranjansb Dec 21, 2025
c8e5061
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 21, 2025
13d6a9a
Fix remaining mypy attr-defined ignores in test_collection
anuranjansb Dec 21, 2025
074c772
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 21, 2025
e9634c6
Remove remaining standalone type ignores
anuranjansb Dec 21, 2025
8dab164
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 21, 2025
45d8862
Fix expected assertion output for extra dict item on Windows
anuranjansb Dec 21, 2025
46f3dbe
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 21, 2025
986b687
Add missing assertion for first dict wrap scenario
anuranjansb Dec 22, 2025
a53d990
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/13503.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions src/_pytest/_io/saferepr.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 9 additions & 6 deletions src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:"]
Expand All @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down
36 changes: 31 additions & 5 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}",
Expand All @@ -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,",
" }",
]

Expand Down
7 changes: 6 additions & 1 deletion testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down
Loading