Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog/14050.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Display dictionary differences in assertion failures using the original key insertion order instead of sorted order.
4 changes: 2 additions & 2 deletions src/_pytest/_io/pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def _pprint_dict(
) -> None:
write = stream.write
write("{")
items = sorted(object.items(), key=_safe_tuple)
items = object.items()
self._format_dict_items(items, stream, indent, allowance, context, level)
write("}")

Expand Down Expand Up @@ -608,7 +608,7 @@ def _safe_repr(
components: list[str] = []
append = components.append
level += 1
for k, v in sorted(object.items(), key=_safe_tuple):
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
25 changes: 25 additions & 0 deletions src/_pytest/_io/saferepr.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from itertools import islice
import pprint
import reprlib

Expand Down Expand Up @@ -77,8 +78,32 @@ def repr_instance(self, x: object, level: int) -> str:
s = _format_repr_exception(exc, x)
if self.maxsize is not None:
s = _ellipsize(s, self.maxsize)

return s

def repr_dict(self, x: dict[object, object], level: int) -> str:
"""Represent a dict while preserving its insertion order.

Differs from ``reprlib.Repr.repr_dict`` by iterating directly over ``x``
rather than using the stdlib's sorting helper.
"""
fillvalue = "..."
n = len(x)
if n == 0:
return "{}"
if level <= 0:
return "{" + fillvalue + "}"
newlevel = level - 1
repr1 = self.repr1
pieces = []
for key in islice(x, self.maxdict):
keyrepr = repr1(key, newlevel)
valrepr = repr1(x[key], newlevel)
pieces.append(f"{keyrepr}: {valrepr}")
if n > self.maxdict:
pieces.append(fillvalue)
return "{" + ", ".join(pieces) + "}"


def safeformat(obj: object) -> str:
"""Return a pretty printed string for the given object.
Expand Down
4 changes: 2 additions & 2 deletions src/_pytest/pytester_assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ def assertoutcome(

realpassed, realskipped, realfailed = outcomes
obtained = {
"failed": len(realfailed),
"passed": len(realpassed),
"skipped": len(realskipped),
"failed": len(realfailed),
}
expected = {"passed": passed, "skipped": skipped, "failed": failed}
expected = {"failed": failed, "passed": passed, "skipped": skipped}
assert obtained == expected, outcomes


Expand Down
37 changes: 37 additions & 0 deletions testing/io/test_saferepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest._io.saferepr import SafeRepr
from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
import pytest
Expand Down Expand Up @@ -192,3 +193,39 @@ def __repr__(self):
assert saferepr_unlimited(A()).startswith(
"<[ValueError(42) raised in repr()] A object at 0x"
)


def test_saferepr_dict_preserves_insertion_order() -> None:
d = {
"b": 2,
"a": 1,
"d": 4,
"e": 5,
"c": 3,
}
s = SafeRepr(maxsize=None)
s.maxdict = 10
assert s.repr(d) == "{'b': 2, 'a': 1, 'd': 4, 'e': 5, 'c': 3}"


def test_saferepr_dict_truncation_preserves_insertion_order() -> None:
d = {
"b": 2,
"a": 1,
"d": 4,
"e": 5,
"c": 3,
}
s = SafeRepr(maxsize=None)
s.maxdict = 1
assert s.repr(d) == "{'b': 2, ...}"


def test_saferepr_dict_fillvalue_when_level_is_zero() -> None:
s = SafeRepr(maxsize=None)
assert s.repr_dict({"a": 1}, level=0) == "{...}"


def test_saferepr_dict_empty() -> None:
s = SafeRepr(maxsize=None)
assert s.repr_dict({}, level=1) == "{}"
31 changes: 31 additions & 0 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -2198,3 +2198,34 @@ def test_vvv():
]
)
result.stdout.no_fnmatch_line(expected_non_vvv_arg_line)


def test_dict_extra_items_preserve_insertion_order(pytester: Pytester) -> None:
"""Assertion output of dict diff shows keys in insertion order (#13503)."""
pytester.makepyfile(
test_order="""
def test_order():
a = {
"b": 2,
"a": 1,
"d": 4,
"e": 5,
"c": 3,
}
assert a == {}
"""
)

result = pytester.runpytest("-vv")
result.stdout.fnmatch_lines(
[
"*Left contains 5 more items:*",
"*Full diff:",
"* + *'b': 2,",
"* + *'a': 1,",
"* + *'d': 4,",
"* + *'e': 5,",
"* + *'c': 3,",
"test_order.py:*: AssertionError",
]
)