Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions changelog/14079.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix assertion diff output to preserve dictionary insertion order.

When comparing dictionaries with extra keys, pytest could incorrectly inject
those keys into the structured diff output, producing misleading results.
The assertion diff now correctly preserves insertion order and reports extra
keys separately.
18 changes: 10 additions & 8 deletions src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ def _compare_eq_dict(
elif same:
explanation += ["Common items:"]
explanation += highlighter(pprint.pformat(same)).splitlines()

diff = {k for k in common if left[k] != right[k]}
if diff:
explanation += ["Differing items:"]
Expand All @@ -520,24 +521,25 @@ def _compare_eq_dict(
+ " != "
+ highlighter(saferepr({k: right[k]}))
]

extra_left = set_left - set_right
len_extra_left = len(extra_left)
if len_extra_left:
if extra_left:
explanation.append(
f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:"
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(saferepr({k: left[k] for k in extra_left})).splitlines()
)

extra_right = set_right - set_left
len_extra_right = len(extra_right)
if len_extra_right:
if extra_right:
explanation.append(
f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:"
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(saferepr({k: right[k] for k in extra_right})).splitlines()
)

return explanation


Expand Down
45 changes: 43 additions & 2 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,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 @@ -417,6 +419,45 @@ class TestAssert_reprcompare:
def test_different_types(self) -> None:
assert callequal([0, 1], "foo") is None

def test_dict_preserves_insertion_order_regression(self) -> None:
# Regression test for issue #14079

long_a = "a" * 80
sub = {
"long_a": long_a,
"sub1": {
"long_a": "substring that gets wrapped " * 3,
},
}

left = {"env": {"sub": sub}}
right = {"env": {"sub": sub}, "new": 1}

diff = callequal(left, right, verbose=True)
assert diff is not None

# extra key is reported
assert "{'new': 1}" in diff

# inspect only structured diff
assert "Full diff:" in diff
start = diff.index("Full diff:") + 1
diff_block = diff[start:]

# 'new' must not appear as a structural key
assert all(
"'new': 1" not in line or line.lstrip().startswith("-")
for line in diff_block
)

# insertion order preserved inside left dict
env_index = next(i for i, line in enumerate(diff_block) if "'env': {" in line)
closing_index = next(
i for i, line in enumerate(diff_block) if line.strip() == "}"
)

assert env_index < closing_index

def test_summary(self) -> None:
lines = callequal([0, 1], [0, 2])
assert lines is not None
Expand Down