From 44f14b039949005ecc93fd8294933c84fab6f374 Mon Sep 17 00:00:00 2001 From: Tim Peters Date: Sun, 2 Mar 2003 00:19:49 +0000 Subject: SF bug 693121: Set == non-Set is a TypeError. Allow mixed-type __eq__ and __ne__ for Set objects. This is messier than I'd like because Set *also* implements __cmp__. I know of one glitch now: cmp(s, t) returns 0 now when s and t are both Sets and s == t, despite that Set.__cmp__ unconditionally raises TypeError (and by intent). The rub is that __eq__ gets tried first, and the x.__eq__(y) True result convinces Python that cmp(x, y) is 0 without even calling Set.__cmp__. --- Lib/sets.py | 32 ++++++++++++++++++++++++++------ Lib/test/test_sets.py | 35 +++++++++++++++++++++++------------ Misc/NEWS | 17 ++++++++++++----- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/Lib/sets.py b/Lib/sets.py index 0824fb1..e6a509f 100644 --- a/Lib/sets.py +++ b/Lib/sets.py @@ -102,20 +102,40 @@ class BaseSet(object): """ return self._data.iterkeys() - # Three-way comparison is not supported + # Three-way comparison is not supported. However, because __eq__ is + # tried before __cmp__, if Set x == Set y, x.__eq__(y) returns True and + # then cmp(x, y) returns 0 (Python doesn't actually call __cmp__ in this + # case). def __cmp__(self, other): raise TypeError, "can't compare sets using cmp()" - # Equality comparisons using the underlying dicts + # Equality comparisons using the underlying dicts. Mixed-type comparisons + # are allowed here, where Set == z for non-Set z always returns False, + # and Set != z always True. This allows expressions like "x in y" to + # give the expected result when y is a sequence of mixed types, not + # raising a pointless TypeError just because y contains a Set, or x is + # a Set and y contain's a non-set ("in" invokes only __eq__). + # Subtle: it would be nicer if __eq__ and __ne__ could return + # NotImplemented instead of True or False. Then the other comparand + # would get a chance to determine the result, and if the other comparand + # also returned NotImplemented then it would fall back to object address + # comparison (which would always return False for __eq__ and always + # True for __ne__). However, that doesn't work, because this type + # *also* implements __cmp__: if, e.g., __eq__ returns NotImplemented, + # Python tries __cmp__ next, and the __cmp__ here then raises TypeError. def __eq__(self, other): - self._binary_sanity_check(other) - return self._data == other._data + if isinstance(other, BaseSet): + return self._data == other._data + else: + return False def __ne__(self, other): - self._binary_sanity_check(other) - return self._data != other._data + if isinstance(other, BaseSet): + return self._data != other._data + else: + return True # Copying operations diff --git a/Lib/test/test_sets.py b/Lib/test/test_sets.py index 9223596..d8b7f3f 100644 --- a/Lib/test/test_sets.py +++ b/Lib/test/test_sets.py @@ -232,7 +232,16 @@ class TestBinaryOps(unittest.TestCase): def test_cmp(self): a, b = Set('a'), Set('b') - self.assertRaises(TypeError, cmp, (a,b)) + self.assertRaises(TypeError, cmp, a, b) + + # You can view this as a buglet: cmp(a, a) does not raise TypeError, + # because __eq__ is tried before __cmp__, and a.__eq__(a) returns, + # which Python thinks is good enough to synthesize a cmp() result + # without calling __cmp__. + self.assertEqual(cmp(a, a), 0) + + self.assertRaises(TypeError, cmp, a, 12) + self.assertRaises(TypeError, cmp, "abc", a) #============================================================================== @@ -476,17 +485,19 @@ class TestSubsetNonOverlap(TestSubsets): class TestOnlySetsInBinaryOps(unittest.TestCase): - def test_cmp(self): - try: - self.other == self.set - self.fail("expected TypeError") - except TypeError: - pass - try: - self.set != self.other - self.fail("expected TypeError") - except TypeError: - pass + def test_eq_ne(self): + # Unlike the others, this is testing that == and != *are* allowed. + self.assertEqual(self.other == self.set, False) + self.assertEqual(self.set == self.other, False) + self.assertEqual(self.other != self.set, True) + self.assertEqual(self.set != self.other, True) + + def test_ge_gt_lt_le(self): + # Unlike the others, this is testing that == and != *are* allowed. + self.assertRaises(TypeError, lambda: self.set < self.other) + self.assertRaises(TypeError, lambda: self.set <= self.other) + self.assertRaises(TypeError, lambda: self.set > self.other) + self.assertRaises(TypeError, lambda: self.set >= self.other) def test_union_update(self): try: diff --git a/Misc/NEWS b/Misc/NEWS index 5405ce5..a6dc7c6 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -31,6 +31,13 @@ Extension modules Library ------- +- sets.Set objects now support mixed-type __eq__ and __ne__, instead + of raising TypeError. If x is a Set object and y is a non-Set object, + x == y is False, and x != y is True. This is akin to the change made + for mixed-type comparisons of datetime objects in 2.3a2; more info + about the rationale is in the NEWS entry for that. See also SF bug + report . + - os.listdir() now returns Unicode strings on platforms that set Py_FileSystemDefaultEncoding, for file names that are not representable in ASCII. (This currently only affects MacOS X; on Windows versions @@ -83,7 +90,7 @@ Mac - A new method MacOS.WMAvailable() returns true if it is safe to access the window manager, false otherwise. - + - EasyDialogs dialogs are now movable-modal. @@ -343,8 +350,8 @@ Library - the platform dependent path related variables sep, altsep, extsep, pathsep, curdir, pardir and defpath are now defined in the platform dependent path modules (e.g. ntpath.py) rather than os.py, so these - variables are now available via os.path. They continue to be - available from the os module. + variables are now available via os.path. They continue to be + available from the os module. (see ). - array.array was added to the types repr.py knows about (see @@ -499,12 +506,12 @@ Mac - Type Carbon.File.FSCatalogInfo and supporting methods have been implemented. This also makes macfs.FSSpec.SetDates() work again. - + - There is a new module pimp, the package install manager for Python, and accompanying applet PackageManager. These allow you to easily download and install pretested extension packages either in source or binary form. Only in MacPython-OSX. - + - Applets are now built with bundlebuilder in MacPython-OSX, which should make them more robust and also provides a path towards BuildApplication. The downside of this change is that applets can no longer be run from the -- cgit v0.12