diff --git a/changelog/14050.bugfix.rst b/changelog/14050.bugfix.rst new file mode 100644 index 00000000000..9642a0941fa --- /dev/null +++ b/changelog/14050.bugfix.rst @@ -0,0 +1 @@ +Assertion comparison output now preserves dictionary insertion order instead of sorting keys. diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index cee70e332f9..3f5c956d9cf 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -1,5 +1,6 @@ from __future__ import annotations +from itertools import islice import pprint import reprlib @@ -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. diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 075d40cdf44..5812b78f22a 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -192,3 +192,31 @@ def __repr__(self): assert saferepr_unlimited(A()).startswith( "<[ValueError(42) raised in repr()] A object at 0x" ) + + +def test_saferepr_dict_preserves_insertion_order(): + d = {"b": 1, "a": 2} + assert saferepr(d, maxsize=None) == "{'b': 1, 'a': 2}" + + +def test_saferepr_dict_truncation_preserves_insertion_order(): + from _pytest._io.saferepr import SafeRepr + + d = {"b": 1, "a": 2} + s = SafeRepr(maxsize=None) + s.maxdict = 1 + assert s.repr(d) == "{'b': 1, ...}" + + +def test_saferepr_dict_fillvalue_when_level_is_zero(): + from _pytest._io.saferepr import SafeRepr + + s = SafeRepr(maxsize=None) + assert s.repr_dict({"a": 1}, level=0) == "{...}" + + +def test_saferepr_dict_empty(): + from _pytest._io.saferepr import SafeRepr + + s = SafeRepr(maxsize=None) + assert s.repr_dict({}, level=1) == "{}" diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 5179b13b0e9..49b8cbd6f06 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -129,7 +129,7 @@ def test_dummy_failure(pytester): # how meta! [ "> r.assertoutcome(passed=1)", "E AssertionError: ([[][]], [[][]], [[][]])*", - "E assert {'failed': 1,... 'skipped': 0} == {'failed': 0,... 'skipped': 0}", + "E assert {'passed': 0,...*'failed': 1} == {'passed': 1,...*'failed': 0}", "E Omitting 1 identical items, use -vv to show", "E Differing items:", "E Use -v to get more diff",