Skip to content
Draft
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: 4 additions & 2 deletions src/_pytest/_io/pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ def _pprint_dict(
) -> None:
write = stream.write
write("{")
items = sorted(object.items(), key=_safe_tuple)
# Preserve insertion order (guaranteed since Python 3.7)
items = object.items()
self._format_dict_items(items, stream, indent, allowance, context, level)
write("}")

Expand Down Expand Up @@ -608,7 +609,8 @@ def _safe_repr(
components: list[str] = []
append = components.append
level += 1
for k, v in sorted(object.items(), key=_safe_tuple):
# Preserve insertion order (guaranteed since Python 3.7)
for k, v in object.items():
krepr = self._safe_repr(k, context, maxlevels, level)
vrepr = self._safe_repr(v, context, maxlevels, level)
append(f"{krepr}: {vrepr}")
Expand Down
15 changes: 12 additions & 3 deletions src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,8 @@ 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()
# Use custom PrettyPrinter to preserve insertion order
explanation += highlighter(PrettyPrinter().pformat(same)).splitlines()
diff = {k for k in common if left[k] != right[k]}
if diff:
explanation += ["Differing items:"]
Expand All @@ -526,17 +527,25 @@ def _compare_eq_dict(
explanation.append(
f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:"
)
# Preserve insertion order from the original dict - use custom PrettyPrinter
explanation.extend(
highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines()
highlighter(
PrettyPrinter().pformat({k: left[k] for k in left if k in extra_left})
).splitlines()
)
extra_right = set_right - set_left
len_extra_right = len(extra_right)
if len_extra_right:
explanation.append(
f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:"
)
# Preserve insertion order from the original dict - use custom PrettyPrinter
explanation.extend(
highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines()
highlighter(
PrettyPrinter().pformat(
{k: right[k] for k in right if k in extra_right}
)
).splitlines()
)
return explanation

Expand Down
92 changes: 83 additions & 9 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,9 @@ def test_dict_wrap(self) -> None:
"",
"Omitting 1 identical items, use -vv to show",
"Right contains 1 more item:",
"{'new': 1}",
"{",
" 'new': 1,",
"}",
"",
"Full diff:",
" {",
Expand Down Expand Up @@ -741,17 +743,25 @@ def test_dict_omitting_with_verbosity_2(self) -> None:
assert lines is not None
assert lines[2].startswith("Common items:")
assert "Omitting" not in lines[2]
assert lines[3] == "{'b': 1}"
# Common items now formatted with proper indentation across multiple lines
assert lines[3] == "{"
assert lines[4] == " 'b': 1,"
assert lines[5] == "}"

def test_dict_different_items(self) -> None:
lines = callequal({"a": 0}, {"b": 1, "c": 2}, verbose=2)
assert lines == [
"{'a': 0} == {'b': 1, 'c': 2}",
"",
"Left contains 1 more item:",
"{'a': 0}",
"{",
" 'a': 0,",
"}",
"Right contains 2 more items:",
"{'b': 1, 'c': 2}",
"{",
" 'b': 1,",
" 'c': 2,",
"}",
"",
"Full diff:",
" {",
Expand All @@ -767,9 +777,14 @@ def test_dict_different_items(self) -> None:
"{'b': 1, 'c': 2} == {'a': 0}",
"",
"Left contains 2 more items:",
"{'b': 1, 'c': 2}",
"{",
" 'b': 1,",
" 'c': 2,",
"}",
"Right contains 1 more item:",
"{'a': 0}",
"{",
" 'a': 0,",
"}",
"",
"Full diff:",
" {",
Expand All @@ -781,6 +796,61 @@ def test_dict_different_items(self) -> None:
" }",
]

def test_dict_insertion_order_preserved(self) -> None:
"""Test that dictionary insertion order is preserved in output (issue #13503)."""
# Create dicts with keys that would differ in alphabetical vs insertion order
left_dict = {
"zebra": 1,
"apple": 2,
"mango": 3,
}
right_dict: dict[str, int] = {}

lines = callequal(left_dict, right_dict, verbose=2)
assert lines is not None

# The "Left contains" section should preserve insertion order (zebra, apple, mango)
# NOT alphabetical order (apple, mango, zebra)
left_section = "\n".join(lines)

# Find the position of each key in the output
zebra_pos = left_section.find("'zebra'")
apple_pos = left_section.find("'apple'")
mango_pos = left_section.find("'mango'")

# All keys should appear
assert zebra_pos != -1
assert apple_pos != -1
assert mango_pos != -1

# Insertion order: zebra should come before apple, apple before mango
assert zebra_pos < apple_pos, "Expected zebra before apple (insertion order)"
assert apple_pos < mango_pos, "Expected apple before mango (insertion order)"

# Test with right dict having extra items
left_dict2: dict[str, str] = {}
right_dict2 = {
"zulu": "a",
"alpha": "b",
"mike": "c",
}

lines2 = callequal(left_dict2, right_dict2, verbose=2)
assert lines2 is not None

right_section = "\n".join(lines2)
zulu_pos = right_section.find("'zulu'")
alpha_pos = right_section.find("'alpha'")
mike_pos = right_section.find("'mike'")

assert zulu_pos != -1
assert alpha_pos != -1
assert mike_pos != -1

# Insertion order: zulu, alpha, mike
assert zulu_pos < alpha_pos, "Expected zulu before alpha (insertion order)"
assert alpha_pos < mike_pos, "Expected alpha before mike (insertion order)"

def test_sequence_different_items(self) -> None:
lines = callequal((1, 2), (3, 4, 5), verbose=2)
assert lines == [
Expand Down Expand Up @@ -2070,12 +2140,16 @@ def test():
}
""",
[
# Common items are now formatted with multi-line indentation
"{bold}{red}E Common items:{reset}",
"{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-1{hl-reset}{str}'{hl-reset}: {number}1*",
"{bold}{red}E {reset}{{{endline}{reset}",
"*{str}'{hl-reset}{str}number-is-1{hl-reset}{str}'{hl-reset}: {number}1*",
"{bold}{red}E Left contains 1 more item:{reset}",
"{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-5{hl-reset}{str}'{hl-reset}: {number}5*",
"{bold}{red}E {reset}{{{endline}{reset}",
"*{str}'{hl-reset}{str}number-is-5{hl-reset}{str}'{hl-reset}: {number}5*",
"{bold}{red}E Right contains 1 more item:{reset}",
"{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-0{hl-reset}{str}'{hl-reset}: {number}0*",
"{bold}{red}E {reset}{{{endline}{reset}",
"*{str}'{hl-reset}{str}number-is-0{hl-reset}{str}'{hl-reset}: {number}0*",
"{bold}{red}E {reset}{light-gray} {hl-reset} {{{endline}{reset}",
"{bold}{red}E {light-gray} {hl-reset} 'number-is-1': 1,{endline}{reset}",
"{bold}{red}E {light-green}+ 'number-is-5': 5,{hl-reset}{endline}{reset}",
Expand Down
Loading