From 98e7b7644bfa3ecc7919b1a139da299da081f34e Mon Sep 17 00:00:00 2001 From: Michael Foord Date: Sat, 20 Mar 2010 03:00:34 +0000 Subject: [PATCH] Issue 7832: renaming unittest.TestCase.assertSameElements to assertItemsEqual and changing behaviour --- Doc/library/unittest.rst | 30 +++++++++-------- Doc/whatsnew/2.7.rst | 2 +- Lib/test/test_cgi.py | 18 +++++----- Lib/test/test_unittest.py | 71 ++++++++++++++++++++++++++++----------- Lib/unittest/case.py | 54 ++++++++++++++++------------- Lib/unittest/util.py | 37 ++++++++++++++++++++ 6 files changed, 144 insertions(+), 68 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 5a17d465c3..8513d7e584 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -786,7 +786,7 @@ Test cases will be included in the error message. This method is used by default when comparing Unicode strings with :meth:`assertEqual`. - If specified *msg* will be used as the error message on failure. + If specified, *msg* will be used as the error message on failure. .. versionadded:: 2.7 @@ -807,22 +807,24 @@ Test cases Tests that *first* is or is not in *second* with an explanatory error message as appropriate. - If specified *msg* will be used as the error message on failure. + If specified, *msg* will be used as the error message on failure. .. versionadded:: 2.7 - .. method:: assertSameElements(actual, expected, msg=None) + .. method:: assertItemsEqual(actual, expected, msg=None) Test that sequence *expected* contains the same elements as *actual*, - regardless of their order. When they don't, an error message listing - the differences between the sequences will be generated. + regardless of their order. When they don't, an error message listing the + differences between the sequences will be generated. - Duplicate elements are ignored when comparing *actual* and *expected*. - It is the equivalent of ``assertEqual(set(expected), set(actual))`` - but it works with sequences of unhashable objects as well. + Duplicate elements are *not* ignored when comparing *actual* and + *expected*. It verifies if each element has the same count in both + sequences. It is the equivalent of ``assertEqual(sorted(expected), + sorted(actual))`` but it works with sequences of unhashable objects as + well. - If specified *msg* will be used as the error message on failure. + If specified, *msg* will be used as the error message on failure. .. versionadded:: 2.7 @@ -836,7 +838,7 @@ Test cases Fails if either of *set1* or *set2* does not have a :meth:`set.difference` method. - If specified *msg* will be used as the error message on failure. + If specified, *msg* will be used as the error message on failure. .. versionadded:: 2.7 @@ -848,7 +850,7 @@ Test cases method will be used by default to compare dictionaries in calls to :meth:`assertEqual`. - If specified *msg* will be used as the error message on failure. + If specified, *msg* will be used as the error message on failure. .. versionadded:: 2.7 @@ -859,7 +861,7 @@ Test cases superset of those in *expected*. If not, an error message listing the missing keys and mismatched values is generated. - If specified *msg* will be used as the error message on failure. + If specified, *msg* will be used as the error message on failure. .. versionadded:: 2.7 @@ -873,7 +875,7 @@ Test cases These methods are used by default when comparing lists or tuples with :meth:`assertEqual`. - If specified *msg* will be used as the error message on failure. + If specified, *msg* will be used as the error message on failure. .. versionadded:: 2.7 @@ -885,7 +887,7 @@ Test cases be raised. If the sequences are different an error message is constructed that shows the difference between the two. - If specified *msg* will be used as the error message on failure. + If specified, *msg* will be used as the error message on failure. This method is used to implement :meth:`assertListEqual` and :meth:`assertTupleEqual`. diff --git a/Doc/whatsnew/2.7.rst b/Doc/whatsnew/2.7.rst index d9ade513a6..20a4b56fc0 100644 --- a/Doc/whatsnew/2.7.rst +++ b/Doc/whatsnew/2.7.rst @@ -1086,7 +1086,7 @@ GvR worked on merging them into Python's version of :mod:`unittest`. * :meth:`assertIn` and :meth:`assertNotIn` tests whether *first* is or is not in *second*. -* :meth:`assertSameElements` tests whether two provided sequences +* :meth:`assertItemsEqual` tests whether two provided sequences contain the same elements. * :meth:`assertSetEqual` compares whether two sets are equal, and diff --git a/Lib/test/test_cgi.py b/Lib/test/test_cgi.py index 791553b526..554bf06ee9 100644 --- a/Lib/test/test_cgi.py +++ b/Lib/test/test_cgi.py @@ -135,18 +135,18 @@ class CgiTests(unittest.TestCase): if isinstance(expect, dict): # test dict interface self.assertEqual(len(expect), len(fcd)) - self.assertSameElements(expect.keys(), fcd.keys()) - self.assertSameElements(expect.values(), fcd.values()) - self.assertSameElements(expect.items(), fcd.items()) + self.assertItemsEqual(expect.keys(), fcd.keys()) + self.assertItemsEqual(expect.values(), fcd.values()) + self.assertItemsEqual(expect.items(), fcd.items()) self.assertEqual(fcd.get("nonexistent field", "default"), "default") self.assertEqual(len(sd), len(fs)) - self.assertSameElements(sd.keys(), fs.keys()) + self.assertItemsEqual(sd.keys(), fs.keys()) self.assertEqual(fs.getvalue("nonexistent field", "default"), "default") # test individual fields for key in expect.keys(): expect_val = expect[key] self.assertTrue(fcd.has_key(key)) - self.assertSameElements(fcd[key], expect[key]) + self.assertItemsEqual(fcd[key], expect[key]) self.assertEqual(fcd.get(key, "default"), fcd[key]) self.assertTrue(fs.has_key(key)) if len(expect_val) > 1: @@ -162,11 +162,11 @@ class CgiTests(unittest.TestCase): self.assertTrue(single_value) self.assertEqual(val, expect_val[0]) self.assertEqual(fs.getvalue(key), expect_val[0]) - self.assertSameElements(sd.getlist(key), expect_val) + self.assertItemsEqual(sd.getlist(key), expect_val) if single_value: - self.assertSameElements(sd.values(), + self.assertItemsEqual(sd.values(), first_elts(expect.values())) - self.assertSameElements(sd.items(), + self.assertItemsEqual(sd.items(), first_second_elts(expect.items())) def test_weird_formcontentdict(self): @@ -178,7 +178,7 @@ class CgiTests(unittest.TestCase): self.assertEqual(d[k], v) for k, v in d.items(): self.assertEqual(expect[k], v) - self.assertSameElements(expect.values(), d.values()) + self.assertItemsEqual(expect.values(), d.values()) def test_log(self): cgi.log("Testing") diff --git a/Lib/test/test_unittest.py b/Lib/test/test_unittest.py index 1b486282e7..5228e764fc 100644 --- a/Lib/test/test_unittest.py +++ b/Lib/test/test_unittest.py @@ -2575,9 +2575,9 @@ class Test_TestCase(TestCase, TestEquality, TestHashing): class SadSnake(object): """Dummy class for test_addTypeEqualityFunc.""" s1, s2 = SadSnake(), SadSnake() - self.assertFalse(s1 == s2) + self.assertNotEqual(s1, s2) def AllSnakesCreatedEqual(a, b, msg=None): - return type(a) == type(b) == SadSnake + return type(a) is type(b) is SadSnake self.addTypeEqualityFunc(SadSnake, AllSnakesCreatedEqual) self.assertEqual(s1, s2) # No this doesn't clean up and remove the SadSnake equality func @@ -2745,21 +2745,51 @@ class Test_TestCase(TestCase, TestEquality, TestHashing): self.assertRaises(self.failureException, self.assertDictEqual, [], d) self.assertRaises(self.failureException, self.assertDictEqual, 1, 1) - self.assertSameElements([1, 2, 3], [3, 2, 1]) - self.assertSameElements([1, 2] + [3] * 100, [1] * 100 + [2, 3]) - self.assertSameElements(['foo', 'bar', 'baz'], ['bar', 'baz', 'foo']) - self.assertRaises(self.failureException, self.assertSameElements, + def testAssertItemsEqual(self): + a = object() + self.assertItemsEqual([1, 2, 3], [3, 2, 1]) + self.assertItemsEqual(['foo', 'bar', 'baz'], ['bar', 'baz', 'foo']) + self.assertItemsEqual([a, a, 2, 2, 3], (a, 2, 3, a, 2)) + self.assertItemsEqual([1, "2", "a", "a"], ["a", "2", True, "a"]) + self.assertRaises(self.failureException, self.assertItemsEqual, + [1, 2] + [3] * 100, [1] * 100 + [2, 3]) + self.assertRaises(self.failureException, self.assertItemsEqual, + [1, "2", "a", "a"], ["a", "2", True, 1]) + self.assertRaises(self.failureException, self.assertItemsEqual, [10], [10, 11]) - self.assertRaises(self.failureException, self.assertSameElements, + self.assertRaises(self.failureException, self.assertItemsEqual, [10, 11], [10]) + self.assertRaises(self.failureException, self.assertItemsEqual, + [10, 11, 10], [10, 11]) # Test that sequences of unhashable objects can be tested for sameness: - self.assertSameElements([[1, 2], [3, 4]], [[3, 4], [1, 2]]) - - self.assertSameElements([{'a': 1}, {'b': 2}], [{'b': 2}, {'a': 1}]) - self.assertRaises(self.failureException, self.assertSameElements, + self.assertItemsEqual([[1, 2], [3, 4], 0], [False, [3, 4], [1, 2]]) + with test_support.check_warnings(quiet=True) as w: + # hashable types, but not orderable + self.assertRaises(self.failureException, self.assertItemsEqual, + [], [divmod, 'x', 1, 5j, 2j, frozenset()]) + # comparing dicts raises a py3k warning + self.assertItemsEqual([{'a': 1}, {'b': 2}], [{'b': 2}, {'a': 1}]) + # comparing heterogenous non-hashable sequences raises a py3k warning + self.assertItemsEqual([1, 'x', divmod, []], [divmod, [], 'x', 1]) + self.assertRaises(self.failureException, self.assertItemsEqual, + [], [divmod, [], 'x', 1, 5j, 2j, set()]) + # fail the test if warnings are not silenced + if w.warnings: + self.fail('assertItemsEqual raised a warning: ' + + str(w.warnings[0])) + self.assertRaises(self.failureException, self.assertItemsEqual, [[1]], [[2]]) + # Same elements, but not same sequence length + self.assertRaises(self.failureException, self.assertItemsEqual, + [1, 1, 2], [2, 1]) + self.assertRaises(self.failureException, self.assertItemsEqual, + [1, 1, "2", "a", "a"], ["2", "2", True, "a"]) + self.assertRaises(self.failureException, self.assertItemsEqual, + [1, {'b': 2}, None, True], [{'b': 2}, True, None]) + + def testAssertSetEqual(self): set1 = set() set2 = set() @@ -3009,13 +3039,14 @@ test case Do not use these methods. They will go away in 3.3. """ - self.failIfEqual(3, 5) - self.failUnlessEqual(3, 3) - self.failUnlessAlmostEqual(2.0, 2.0) - self.failIfAlmostEqual(3.0, 5.0) - self.failUnless(True) - self.failUnlessRaises(TypeError, lambda _: 3.14 + u'spam') - self.failIf(False) + with test_support.check_warnings(): + self.failIfEqual(3, 5) + self.failUnlessEqual(3, 3) + self.failUnlessAlmostEqual(2.0, 2.0) + self.failIfAlmostEqual(3.0, 5.0) + self.failUnless(True) + self.failUnlessRaises(TypeError, lambda _: 3.14 + u'spam') + self.failIf(False) def testDeepcopy(self): # Issue: 5660 @@ -3355,8 +3386,8 @@ class TestLongMessage(TestCase): "^Missing: 'key'$", "^Missing: 'key' : oops$"]) - def testAssertSameElements(self): - self.assertMessages('assertSameElements', ([], [None]), + def testAssertItemsEqual(self): + self.assertMessages('assertItemsEqual', ([], [None]), [r"\[None\]$", "^oops$", r"\[None\]$", r"\[None\] : oops$"]) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index ea8fefa833..2af48e1fb4 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -8,8 +8,9 @@ import re import warnings from . import result -from .util import strclass, safe_repr, sorted_list_difference - +from .util import ( + strclass, safe_repr, sorted_list_difference, unorderable_list_difference +) class SkipTest(Exception): """ @@ -686,10 +687,9 @@ class TestCase(object): msg: Optional message to use on failure instead of a list of differences. - For more general containership equality, assertSameElements will work - with things other than sets. This uses ducktyping to support - different types of sets, and is optimized for sets specifically - (parameters must support a difference method). + assertSetEqual uses ducktyping to support different types of sets, and + is optimized for sets specifically (parameters must support a + difference method). """ try: difference1 = set1.difference(set2) @@ -784,42 +784,48 @@ class TestCase(object): self.fail(self._formatMessage(msg, standardMsg)) - def assertSameElements(self, expected_seq, actual_seq, msg=None): - """An unordered sequence specific comparison. + def assertItemsEqual(self, expected_seq, actual_seq, msg=None): + """An unordered sequence / set specific comparison. It asserts that + expected_seq and actual_seq contain the same elements. It is + the equivalent of:: + + self.assertEqual(sorted(expected_seq), sorted(actual_seq)) Raises with an error message listing which elements of expected_seq are missing from actual_seq and vice versa if any. - Duplicate elements are ignored when comparing *expected_seq* and - *actual_seq*. It is the equivalent of ``assertEqual(set(expected), - set(actual))`` but it works with sequences of unhashable objects as - well. + Asserts that each element has the same count in both sequences. + Example: + - [0, 1, 1] and [1, 0, 1] compare equal. + - [0, 0, 1] and [0, 1] compare unequal. """ with warnings.catch_warnings(): if sys.py3kwarning: # Silence Py3k warning raised during the sorting for _msg in ["dict inequality comparisons", - "builtin_function_or_method order comparisons", - "comparing unequal types"]: + "builtin_function_or_method order comparisons", + "comparing unequal types"]: warnings.filterwarnings("ignore", _msg, DeprecationWarning) try: - expected = set(expected_seq) - actual = set(actual_seq) - missing = sorted(expected.difference(actual)) - unexpected = sorted(actual.difference(expected)) - except TypeError: - # Fall back to slower list-compare if any of the objects are - # not hashable. expected = sorted(expected_seq) actual = sorted(actual_seq) - missing, unexpected = sorted_list_difference(expected, actual) + except TypeError: + # Unsortable items (example: set(), complex(), ...) + expected = list(expected_seq) + actual = list(actual_seq) + missing, unexpected = unorderable_list_difference( + expected, actual, ignore_duplicate=False + ) + else: + return self.assertSequenceEqual(expected, actual, msg=msg) + errors = [] if missing: errors.append('Expected, but missing:\n %s' % - safe_repr(missing)) + safe_repr(missing)) if unexpected: errors.append('Unexpected, but present:\n %s' % - safe_repr(unexpected)) + safe_repr(unexpected)) if errors: standardMsg = '\n'.join(errors) self.fail(self._formatMessage(msg, standardMsg)) diff --git a/Lib/unittest/util.py b/Lib/unittest/util.py index 9fce625b1c..9ddf879387 100644 --- a/Lib/unittest/util.py +++ b/Lib/unittest/util.py @@ -48,3 +48,40 @@ def sorted_list_difference(expected, actual): unexpected.extend(actual[j:]) break return missing, unexpected + + +def unorderable_list_difference(expected, actual, ignore_duplicate=False): + """Same behavior as sorted_list_difference but + for lists of unorderable items (like dicts). + + As it does a linear search per item (remove) it + has O(n*n) performance. + """ + missing = [] + unexpected = [] + while expected: + item = expected.pop() + try: + actual.remove(item) + except ValueError: + missing.append(item) + if ignore_duplicate: + for lst in expected, actual: + try: + while True: + lst.remove(item) + except ValueError: + pass + if ignore_duplicate: + while actual: + item = actual.pop() + unexpected.append(item) + try: + while True: + actual.remove(item) + except ValueError: + pass + return missing, unexpected + + # anything left in actual is unexpected + return missing, actual -- 2.50.1