Skip to content

Commit d23acf8

Browse files
committed
Improve performance and error readability of unittest.TestCase.assertDictEqual
The function previously used a simple difflib.ndiff on top of a pprint.pformat of each dict, which resulted in very bad performance on large dicts and unclear assertion error outputs in many cases. This change formats the diffs in a more readable manner by inspecting the differences between the dicts, truncating long keys and values, and justifying values in the various groups of lines.
1 parent ed81971 commit d23acf8

File tree

1 file changed

+44
-8
lines changed

1 file changed

+44
-8
lines changed

Lib/unittest/case.py

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
from . import result
1616
from .util import (strclass, safe_repr, _count_diff_all_purpose,
17-
_count_diff_hashable, _common_shorten_repr)
17+
_count_diff_hashable, _common_shorten_repr,
18+
_shorten, _MIN_END_LEN, _MAX_LENGTH)
1819

1920
__unittest = True
2021

@@ -1202,15 +1203,50 @@ def assertIsNot(self, expr1, expr2, msg=None):
12021203
standardMsg = 'unexpectedly identical: %s' % (safe_repr(expr1),)
12031204
self.fail(self._formatMessage(msg, standardMsg))
12041205

1205-
def assertDictEqual(self, d1, d2, msg=None):
1206-
self.assertIsInstance(d1, dict, 'First argument is not a dictionary')
1207-
self.assertIsInstance(d2, dict, 'Second argument is not a dictionary')
1206+
def assertDictEqual(self, d1: dict, d2: dict, msg: str | None = None):
1207+
self.assertIsInstance(d1, dict, "First argument is not a dictionary")
1208+
self.assertIsInstance(d2, dict, "Second argument is not a dictionary")
12081209

12091210
if d1 != d2:
1210-
standardMsg = '%s != %s' % _common_shorten_repr(d1, d2)
1211-
diff = ('\n' + '\n'.join(difflib.ndiff(
1212-
pprint.pformat(d1).splitlines(),
1213-
pprint.pformat(d2).splitlines())))
1211+
standardMsg = "%s != %s" % _common_shorten_repr(d1, d2)
1212+
1213+
d1keys = set(d1.keys())
1214+
d2keys = set(d2.keys())
1215+
d1extrakeys = d1keys - d2keys
1216+
d2extrakeys = d2keys - d1keys
1217+
commonkeys = d1keys & d2keys
1218+
lines = []
1219+
def _value_repr(value):
1220+
return _shorten(safe_repr(value), _MAX_LENGTH//2-_MIN_END_LEN, _MIN_END_LEN)
1221+
def _justified_values(d, keys, prefix):
1222+
items = [(_value_repr(key), _value_repr(d[key])) for key in sorted(keys)]
1223+
justify_width = max(len(key) for key, value in items)
1224+
justify_width = max(min(justify_width, _MAX_LENGTH - _MIN_END_LEN - 2), 4)
1225+
return (" %s %s: %s," % (prefix, key.ljust(justify_width), value) for key, value in items)
1226+
if commonkeys:
1227+
commonvalues = []
1228+
for key in sorted(commonkeys):
1229+
if d1[key] == d2[key]:
1230+
commonvalues.append(key)
1231+
commonkeys.remove(key)
1232+
if commonvalues:
1233+
lines.append(" Keys in both dicts with identical values:")
1234+
lines.extend(_justified_values(d1, commonvalues, " "))
1235+
if commonkeys:
1236+
lines.append(" Keys in both dicts with differing values:")
1237+
for key in sorted(commonkeys):
1238+
key_repr = _value_repr(key)
1239+
lines.append(" - %s: %s," % (key_repr, _value_repr(d1[key])))
1240+
lines.append(" + %s: %s," % (key_repr, _value_repr(d2[key])))
1241+
if d1extrakeys:
1242+
lines.append(" Keys in the first dict but not the second:")
1243+
lines.extend(_justified_values(d1, d1extrakeys, "-"))
1244+
if d2extrakeys:
1245+
lines.append(" Keys in the second dict but not the first:")
1246+
lines.extend(_justified_values(d2, d2extrakeys, "+"))
1247+
1248+
diff = "\n{\n%s\n}" % '\n'.join(lines)
1249+
12141250
standardMsg = self._truncateMessage(standardMsg, diff)
12151251
self.fail(self._formatMessage(msg, standardMsg))
12161252

0 commit comments

Comments
 (0)