diff --git a/Lib/test/test_unittest/test_assertions.py b/Lib/test/test_unittest/test_assertions.py index 1dec947ea76d23..6b071ef0ed0452 100644 --- a/Lib/test/test_unittest/test_assertions.py +++ b/Lib/test/test_unittest/test_assertions.py @@ -267,9 +267,47 @@ def testAssertNotIn(self): def testAssertDictEqual(self): self.assertMessages('assertDictEqual', ({}, {'key': 'value'}), - [r"\+ \{'key': 'value'\}$", "^oops$", - r"\+ \{'key': 'value'\}$", - r"\+ \{'key': 'value'\} : oops$"]) + [r"^\{\} != \{'key': 'value'\}\n\{\nKeys in the second " + r"dict but not the first:\n \+ 'key': 'value',\n\}$", + r"^oops$", + r"^\{\} != \{'key': 'value'\}\n\{\nKeys in the second " + r"dict but not the first:\n \+ 'key': 'value',\n\}$", + r"^\{\} != \{'key': 'value'\}\n\{\nKeys in the second " + r"dict but not the first:\n \+ 'key': 'value',\n\} : oops$"]) + self.assertDictEqual({}, {}) + self.assertDictEqual({'key': 'value'}, {'key': 'value'}) + self.assertRaisesRegex( + AssertionError, + r"^\{\} != \{'key': 'value'\}\n{\nKeys in the second " + r"dict but not the first:\n \+ 'key': 'value',\n}$", + lambda: self.assertDictEqual({}, {'key': 'value'})) + self.assertRaisesRegex( + AssertionError, + r"^\{'key': 'value'\} != \{\}\n{\nKeys in the first " + r"dict but not the second:\n - 'key': 'value',\n}$", + lambda: self.assertDictEqual({'key': 'value'}, {})) + self.assertRaisesRegex( + AssertionError, + r"^\{'key': 'value'\} != \{'key': 'othervalue'\}\n{\nKeys in both dicts " + r"with differing values:\n - 'key': 'value',\n \+ 'key': 'othervalue',\n}$", + lambda: self.assertDictEqual({'key': 'value'}, {'key': 'othervalue'})) + self.assertRaisesRegex( + AssertionError, + r"^\{'same': 'same', 'samekey': 'onevalue', 'otherkey': 'othervalue'\} " + r"!= \{'same': 'same', 'samekey': 'twovalue', 'somekey': 'somevalue'\}\n{\n" + r" 'same': 'same',\n" + r"Keys in both dicts with differing values:\n" + r" - 'samekey': 'onevalue',\n" + r" \+ 'samekey': 'twovalue',\n" + r"Keys in the first dict but not the second:\n" + r" - 'otherkey': 'othervalue',\n" + r"Keys in the second dict but not the first:\n" + r" \+ 'somekey': 'somevalue',\n" + r"\}$", + lambda: self.assertDictEqual( + {'same': 'same', 'samekey': 'onevalue', 'otherkey': 'othervalue'}, + {'same': 'same', 'samekey': 'twovalue', 'somekey': 'somevalue'})) + def testAssertMultiLineEqual(self): self.assertMessages('assertMultiLineEqual', ("", "foo"), diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 55c79d353539ca..6b708090d1166f 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -14,7 +14,8 @@ from . import result from .util import (strclass, safe_repr, _count_diff_all_purpose, - _count_diff_hashable, _common_shorten_repr) + _count_diff_hashable, _common_shorten_repr, + _shorten, _MIN_END_LEN, _MAX_LENGTH) __unittest = True @@ -1203,14 +1204,48 @@ def assertIsNot(self, expr1, expr2, msg=None): self.fail(self._formatMessage(msg, standardMsg)) def assertDictEqual(self, d1, d2, msg=None): - self.assertIsInstance(d1, dict, 'First argument is not a dictionary') - self.assertIsInstance(d2, dict, 'Second argument is not a dictionary') + self.assertIsInstance(d1, dict, "First argument is not a dictionary") + self.assertIsInstance(d2, dict, "Second argument is not a dictionary") if d1 != d2: - standardMsg = '%s != %s' % _common_shorten_repr(d1, d2) - diff = ('\n' + '\n'.join(difflib.ndiff( - pprint.pformat(d1).splitlines(), - pprint.pformat(d2).splitlines()))) + standardMsg = "%s != %s" % _common_shorten_repr(d1, d2) + + d1keys = set(d1.keys()) + d2keys = set(d2.keys()) + d1extrakeys = d1keys - d2keys + d2extrakeys = d2keys - d1keys + commonkeys = d1keys & d2keys + lines = [] + def _value_repr(value): + return _shorten(safe_repr(value), _MAX_LENGTH//2-_MIN_END_LEN, _MIN_END_LEN) + def _justified_values(d, keys, prefix): + items = [(_value_repr(key), _value_repr(d[key])) for key in sorted(keys)] + justify_width = max(len(key) for key, value in items) + justify_width = max(min(justify_width, _MAX_LENGTH - _MIN_END_LEN - 2), 4) + return (" %s %s: %s," % (prefix, key.ljust(justify_width), value) for key, value in items) + if commonkeys: + commonvalues = [] + for key in sorted(commonkeys): + if d1[key] == d2[key]: + commonvalues.append(key) + commonkeys.remove(key) + if commonvalues: + lines.extend(_justified_values(d1, commonvalues, " ")) + if commonkeys: + lines.append("Keys in both dicts with differing values:") + for key in sorted(commonkeys): + key_repr = _value_repr(key) + lines.append(" - %s: %s," % (key_repr, _value_repr(d1[key]))) + lines.append(" + %s: %s," % (key_repr, _value_repr(d2[key]))) + if d1extrakeys: + lines.append("Keys in the first dict but not the second:") + lines.extend(_justified_values(d1, d1extrakeys, "-")) + if d2extrakeys: + lines.append("Keys in the second dict but not the first:") + lines.extend(_justified_values(d2, d2extrakeys, "+")) + + diff = "\n{\n%s\n}" % '\n'.join(lines) + standardMsg = self._truncateMessage(standardMsg, diff) self.fail(self._formatMessage(msg, standardMsg)) diff --git a/Misc/NEWS.d/next/Library/2024-11-16-21-04-16.gh-issue-99151.74rlUp.rst b/Misc/NEWS.d/next/Library/2024-11-16-21-04-16.gh-issue-99151.74rlUp.rst new file mode 100644 index 00000000000000..a46986497cf7a5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-11-16-21-04-16.gh-issue-99151.74rlUp.rst @@ -0,0 +1,2 @@ +Improve performance and error readability of +:meth:`~unittest.TestCase.assertDictEqual`.