From 48361f5cbf419cce361fd1aa0389d6304ad167db Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Mon, 11 Aug 2008 15:45:58 +0000 Subject: [PATCH] Issue 2235: Py3k warnings are now emitted for classes that will no longer inherit a__hash__ implementation from a parent class in Python 3.x. The standard library and test suite have been updated to not emit these warnings. --- Lib/UserList.py | 1 + Lib/_abcoll.py | 6 ++ Lib/ctypes/test/test_simplesubclasses.py | 2 + Lib/numbers.py | 3 + Lib/test/test_builtin.py | 1 + Lib/test/test_coercion.py | 1 + Lib/test/test_collections.py | 1 + Lib/test/test_copy.py | 4 ++ Lib/test/test_datetime.py | 1 + Lib/test/test_descr.py | 5 ++ Lib/test/test_hash.py | 24 +++---- Lib/test/test_operator.py | 1 + Lib/test/test_py3kwarn.py | 92 ++++++++++++++++++++++-- Lib/test/test_slice.py | 1 + Lib/test/test_sort.py | 1 + Lib/unittest.py | 3 + Lib/xml/dom/minidom.py | 1 + Objects/typeobject.c | 35 +++++++++ 18 files changed, 165 insertions(+), 18 deletions(-) diff --git a/Lib/UserList.py b/Lib/UserList.py index 556a3273a8a..b4459857079 100644 --- a/Lib/UserList.py +++ b/Lib/UserList.py @@ -25,6 +25,7 @@ class UserList(collections.MutableSequence): else: return other def __cmp__(self, other): return cmp(self.data, self.__cast(other)) + __hash__ = None # Mutable sequence, so not hashable def __contains__(self, item): return item in self.data def __len__(self): return len(self.data) def __getitem__(self, i): return self.data[i] diff --git a/Lib/_abcoll.py b/Lib/_abcoll.py index 85d733f39c5..a5fee081df5 100644 --- a/Lib/_abcoll.py +++ b/Lib/_abcoll.py @@ -207,6 +207,9 @@ class Set(Sized, Iterable, Container): other = self._from_iterable(other) return (self - other) | (other - self) + # Sets are not hashable by default, but subclasses can change this + __hash__ = None + def _hash(self): """Compute the hash value of a set. @@ -350,6 +353,9 @@ class Mapping(Sized, Iterable, Container): def values(self): return [self[key] for key in self] + # Mappings are not hashable by default, but subclasses can change this + __hash__ = None + def __eq__(self, other): return isinstance(other, Mapping) and \ dict(self.items()) == dict(other.items()) diff --git a/Lib/ctypes/test/test_simplesubclasses.py b/Lib/ctypes/test/test_simplesubclasses.py index 71551707421..5671cce3320 100644 --- a/Lib/ctypes/test/test_simplesubclasses.py +++ b/Lib/ctypes/test/test_simplesubclasses.py @@ -6,6 +6,8 @@ class MyInt(c_int): if type(other) != MyInt: return -1 return cmp(self.value, other.value) + def __hash__(self): # Silence Py3k warning + return hash(self.value) class Test(unittest.TestCase): diff --git a/Lib/numbers.py b/Lib/numbers.py index 38240d62502..fa59fd8e7de 100644 --- a/Lib/numbers.py +++ b/Lib/numbers.py @@ -18,6 +18,9 @@ class Number(object): """ __metaclass__ = ABCMeta + # Concrete numeric types must provide their own hash implementation + __hash__ = None + ## Notes on Decimal ## ---------------- diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 70980f8924f..6671f2c02eb 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -1064,6 +1064,7 @@ class BuiltinTest(unittest.TestCase): class badzero(int): def __cmp__(self, other): raise RuntimeError + __hash__ = None # Invalid cmp makes this unhashable self.assertRaises(RuntimeError, range, a, a + 1, badzero(1)) # Reject floats when it would require PyLongs to represent. diff --git a/Lib/test/test_coercion.py b/Lib/test/test_coercion.py index e3a7e43737b..a70f82d84c4 100644 --- a/Lib/test/test_coercion.py +++ b/Lib/test/test_coercion.py @@ -309,6 +309,7 @@ class CoercionTest(unittest.TestCase): def __cmp__(slf, other): self.assert_(other == 42, 'expected evil_coercer, got %r' % other) return 0 + __hash__ = None # Invalid cmp makes this unhashable self.assertEquals(cmp(WackyComparer(), evil_coercer), 0) # ...and classic classes too, since that code path is a little different class ClassicWackyComparer: diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index 99eb8cf9add..d689add41d2 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -172,6 +172,7 @@ class TestOneTrickPonyABCs(unittest.TestCase): class H(Hashable): def __hash__(self): return super(H, self).__hash__() + __eq__ = Hashable.__eq__ # Silence Py3k warning self.assertEqual(hash(H()), 0) self.failIf(issubclass(int, H)) diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index d2899bd4eab..be334ccf059 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -435,6 +435,7 @@ class TestCopy(unittest.TestCase): return (C, (), self.__dict__) def __cmp__(self, other): return cmp(self.__dict__, other.__dict__) + __hash__ = None # Silence Py3k warning x = C() x.foo = [42] y = copy.copy(x) @@ -451,6 +452,7 @@ class TestCopy(unittest.TestCase): self.__dict__.update(state) def __cmp__(self, other): return cmp(self.__dict__, other.__dict__) + __hash__ = None # Silence Py3k warning x = C() x.foo = [42] y = copy.copy(x) @@ -477,6 +479,7 @@ class TestCopy(unittest.TestCase): def __cmp__(self, other): return (cmp(list(self), list(other)) or cmp(self.__dict__, other.__dict__)) + __hash__ = None # Silence Py3k warning x = C([[1, 2], 3]) y = copy.copy(x) self.assertEqual(x, y) @@ -494,6 +497,7 @@ class TestCopy(unittest.TestCase): def __cmp__(self, other): return (cmp(dict(self), list(dict)) or cmp(self.__dict__, other.__dict__)) + __hash__ = None # Silence Py3k warning x = C([("foo", [1, 2]), ("bar", 3)]) y = copy.copy(x) self.assertEqual(x, y) diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index cdc9eed6b9c..16749610140 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -986,6 +986,7 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase): # compare-by-address (which never says "equal" for distinct # objects). return 0 + __hash__ = None # Silence Py3k warning # This still errors, because date and datetime comparison raise # TypeError instead of NotImplemented when they don't know what to diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 53b7611fb05..f170d591a60 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -1121,6 +1121,7 @@ order (MRO) for bases """ class G(object): def __cmp__(self, other): return 0 + __hash__ = None # Silence Py3k warning g = G() orig_objects = len(gc.get_objects()) for i in xrange(10): @@ -2727,6 +2728,7 @@ order (MRO) for bases """ if isinstance(other, int) or isinstance(other, long): return cmp(self.value, other) return NotImplemented + __hash__ = None # Silence Py3k warning c1 = C(1) c2 = C(2) @@ -2755,6 +2757,7 @@ order (MRO) for bases """ return abs(self - other) <= 1e-6 except: return NotImplemented + __hash__ = None # Silence Py3k warning zz = ZZ(1.0000003) self.assertEqual(zz, 1+0j) self.assertEqual(1+0j, zz) @@ -2767,6 +2770,7 @@ order (MRO) for bases """ self.value = int(value) def __cmp__(self_, other): self.fail("shouldn't call __cmp__") + __hash__ = None # Silence Py3k warning def __eq__(self, other): if isinstance(other, C): return self.value == other.value @@ -3262,6 +3266,7 @@ order (MRO) for bases """ class S(str): def __eq__(self, other): return self.lower() == other.lower() + __hash__ = None # Silence Py3k warning def test_subclass_propagation(self): # Testing propagation of slot functions to subclasses... diff --git a/Lib/test/test_hash.py b/Lib/test/test_hash.py index f3954c2280b..47c66d1d89d 100644 --- a/Lib/test/test_hash.py +++ b/Lib/test/test_hash.py @@ -52,6 +52,9 @@ class FixedHash(object): class OnlyEquality(object): def __eq__(self, other): return self is other + # Trick to suppress Py3k warning in 2.x + __hash__ = None +del OnlyEquality.__hash__ class OnlyInequality(object): def __ne__(self, other): @@ -60,6 +63,9 @@ class OnlyInequality(object): class OnlyCmp(object): def __cmp__(self, other): return cmp(id(self), id(other)) + # Trick to suppress Py3k warning in 2.x + __hash__ = None +del OnlyCmp.__hash__ class InheritedHashWithEquality(FixedHash, OnlyEquality): pass class InheritedHashWithInequality(FixedHash, OnlyInequality): pass @@ -71,18 +77,15 @@ class NoHash(object): class HashInheritanceTestCase(unittest.TestCase): default_expected = [object(), DefaultHash(), + OnlyEquality(), + OnlyInequality(), + OnlyCmp(), ] fixed_expected = [FixedHash(), InheritedHashWithEquality(), InheritedHashWithInequality(), InheritedHashWithCmp(), ] - # TODO: Change these to expecting an exception - # when forward porting to Py3k - warning_expected = [OnlyEquality(), - OnlyInequality(), - OnlyCmp(), - ] error_expected = [NoHash()] def test_default_hash(self): @@ -93,20 +96,13 @@ class HashInheritanceTestCase(unittest.TestCase): for obj in self.fixed_expected: self.assertEqual(hash(obj), _FIXED_HASH_VALUE) - def test_warning_hash(self): - for obj in self.warning_expected: - # TODO: Check for the expected Py3k warning here - obj_hash = hash(obj) - self.assertEqual(obj_hash, _default_hash(obj)) - def test_error_hash(self): for obj in self.error_expected: self.assertRaises(TypeError, hash, obj) def test_hashable(self): objects = (self.default_expected + - self.fixed_expected + - self.warning_expected) + self.fixed_expected) for obj in objects: self.assert_(isinstance(obj, Hashable), repr(obj)) diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index 1c3fda326fa..9bc0a4ef55e 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -57,6 +57,7 @@ class OperatorTestCase(unittest.TestCase): class C(object): def __eq__(self, other): raise SyntaxError + __hash__ = None # Silence Py3k warning self.failUnlessRaises(TypeError, operator.eq) self.failUnlessRaises(SyntaxError, operator.eq, C(), C()) self.failIf(operator.eq(1, 0)) diff --git a/Lib/test/test_py3kwarn.py b/Lib/test/test_py3kwarn.py index 67b2538f01a..340e86f42dc 100644 --- a/Lib/test/test_py3kwarn.py +++ b/Lib/test/test_py3kwarn.py @@ -12,6 +12,9 @@ if not sys.py3kwarning: class TestPy3KWarnings(unittest.TestCase): + def assertWarning(self, _, warning, expected_message): + self.assertEqual(str(warning.message), expected_message) + def test_backquote(self): expected = 'backquote not supported in 3.x; use repr()' with catch_warning() as w: @@ -28,30 +31,41 @@ class TestPy3KWarnings(unittest.TestCase): with catch_warning() as w: safe_exec("True = False") self.assertWarning(None, w, expected) + w.reset() safe_exec("False = True") self.assertWarning(None, w, expected) + w.reset() try: safe_exec("obj.False = True") except NameError: pass self.assertWarning(None, w, expected) + w.reset() try: safe_exec("obj.True = False") except NameError: pass self.assertWarning(None, w, expected) + w.reset() safe_exec("def False(): pass") self.assertWarning(None, w, expected) + w.reset() safe_exec("def True(): pass") self.assertWarning(None, w, expected) + w.reset() safe_exec("class False: pass") self.assertWarning(None, w, expected) + w.reset() safe_exec("class True: pass") self.assertWarning(None, w, expected) + w.reset() safe_exec("def f(True=43): pass") self.assertWarning(None, w, expected) + w.reset() safe_exec("def f(False=None): pass") self.assertWarning(None, w, expected) + w.reset() safe_exec("f(False=True)") self.assertWarning(None, w, expected) + w.reset() safe_exec("f(True=1)") self.assertWarning(None, w, expected) @@ -60,20 +74,25 @@ class TestPy3KWarnings(unittest.TestCase): expected = 'type inequality comparisons not supported in 3.x' with catch_warning() as w: self.assertWarning(int < str, w, expected) + w.reset() self.assertWarning(type < object, w, expected) def test_object_inequality_comparisons(self): expected = 'comparing unequal types not supported in 3.x' with catch_warning() as w: self.assertWarning(str < [], w, expected) + w.reset() self.assertWarning(object() < (1, 2), w, expected) def test_dict_inequality_comparisons(self): expected = 'dict inequality comparisons not supported in 3.x' with catch_warning() as w: self.assertWarning({} < {2:3}, w, expected) + w.reset() self.assertWarning({} <= {}, w, expected) + w.reset() self.assertWarning({} > {2:3}, w, expected) + w.reset() self.assertWarning({2:3} >= {}, w, expected) def test_cell_inequality_comparisons(self): @@ -86,6 +105,7 @@ class TestPy3KWarnings(unittest.TestCase): cell1, = f(1).func_closure with catch_warning() as w: self.assertWarning(cell0 == cell1, w, expected) + w.reset() self.assertWarning(cell0 < cell1, w, expected) def test_code_inequality_comparisons(self): @@ -96,8 +116,11 @@ class TestPy3KWarnings(unittest.TestCase): pass with catch_warning() as w: self.assertWarning(f.func_code < g.func_code, w, expected) + w.reset() self.assertWarning(f.func_code <= g.func_code, w, expected) + w.reset() self.assertWarning(f.func_code >= g.func_code, w, expected) + w.reset() self.assertWarning(f.func_code > g.func_code, w, expected) def test_builtin_function_or_method_comparisons(self): @@ -107,13 +130,13 @@ class TestPy3KWarnings(unittest.TestCase): meth = {}.get with catch_warning() as w: self.assertWarning(func < meth, w, expected) + w.reset() self.assertWarning(func > meth, w, expected) + w.reset() self.assertWarning(meth <= func, w, expected) + w.reset() self.assertWarning(meth >= func, w, expected) - def assertWarning(self, _, warning, expected_message): - self.assertEqual(str(warning.message), expected_message) - def test_sort_cmp_arg(self): expected = "the cmp argument is not supported in 3.x" lst = range(5) @@ -121,8 +144,11 @@ class TestPy3KWarnings(unittest.TestCase): with catch_warning() as w: self.assertWarning(lst.sort(cmp=cmp), w, expected) + w.reset() self.assertWarning(sorted(lst, cmp=cmp), w, expected) + w.reset() self.assertWarning(lst.sort(cmp), w, expected) + w.reset() self.assertWarning(sorted(lst, cmp), w, expected) def test_sys_exc_clear(self): @@ -156,7 +182,7 @@ class TestPy3KWarnings(unittest.TestCase): self.assertWarning(None, w, expected) def test_buffer(self): - expected = 'buffer() not supported in 3.x; use memoryview()' + expected = 'buffer() not supported in 3.x' with catch_warning() as w: self.assertWarning(buffer('a'), w, expected) @@ -167,6 +193,64 @@ class TestPy3KWarnings(unittest.TestCase): with catch_warning() as w: self.assertWarning(f.xreadlines(), w, expected) + def test_hash_inheritance(self): + with catch_warning() as w: + # With object as the base class + class WarnOnlyCmp(object): + def __cmp__(self, other): pass + self.assertEqual(len(w.warnings), 1) + self.assertWarning(None, w, + "Overriding __cmp__ blocks inheritance of __hash__ in 3.x") + w.reset() + class WarnOnlyEq(object): + def __eq__(self, other): pass + self.assertEqual(len(w.warnings), 1) + self.assertWarning(None, w, + "Overriding __eq__ blocks inheritance of __hash__ in 3.x") + w.reset() + class WarnCmpAndEq(object): + def __cmp__(self, other): pass + def __eq__(self, other): pass + self.assertEqual(len(w.warnings), 2) + self.assertWarning(None, w.warnings[-2], + "Overriding __cmp__ blocks inheritance of __hash__ in 3.x") + self.assertWarning(None, w, + "Overriding __eq__ blocks inheritance of __hash__ in 3.x") + w.reset() + class NoWarningOnlyHash(object): + def __hash__(self): pass + self.assertEqual(len(w.warnings), 0) + # With an intermediate class in the heirarchy + class DefinesAllThree(object): + def __cmp__(self, other): pass + def __eq__(self, other): pass + def __hash__(self): pass + class WarnOnlyCmp(DefinesAllThree): + def __cmp__(self, other): pass + self.assertEqual(len(w.warnings), 1) + self.assertWarning(None, w, + "Overriding __cmp__ blocks inheritance of __hash__ in 3.x") + w.reset() + class WarnOnlyEq(DefinesAllThree): + def __eq__(self, other): pass + self.assertEqual(len(w.warnings), 1) + self.assertWarning(None, w, + "Overriding __eq__ blocks inheritance of __hash__ in 3.x") + w.reset() + class WarnCmpAndEq(DefinesAllThree): + def __cmp__(self, other): pass + def __eq__(self, other): pass + self.assertEqual(len(w.warnings), 2) + self.assertWarning(None, w.warnings[-2], + "Overriding __cmp__ blocks inheritance of __hash__ in 3.x") + self.assertWarning(None, w, + "Overriding __eq__ blocks inheritance of __hash__ in 3.x") + w.reset() + class NoWarningOnlyHash(DefinesAllThree): + def __hash__(self): pass + self.assertEqual(len(w.warnings), 0) + + class TestStdlibRemovals(unittest.TestCase): diff --git a/Lib/test/test_slice.py b/Lib/test/test_slice.py index 8c90c10e846..854805b9155 100644 --- a/Lib/test/test_slice.py +++ b/Lib/test/test_slice.py @@ -33,6 +33,7 @@ class SliceTest(unittest.TestCase): class BadCmp(object): def __eq__(self, other): raise Exc + __hash__ = None # Silence Py3k warning s1 = slice(BadCmp()) s2 = slice(BadCmp()) diff --git a/Lib/test/test_sort.py b/Lib/test/test_sort.py index 84c92cc9704..a61fb96b794 100644 --- a/Lib/test/test_sort.py +++ b/Lib/test/test_sort.py @@ -70,6 +70,7 @@ class TestBase(unittest.TestCase): def __cmp__(self, other): return cmp(self.key, other.key) + __hash__ = None # Silence Py3k warning def __repr__(self): return "Stable(%d, %d)" % (self.key, self.index) diff --git a/Lib/unittest.py b/Lib/unittest.py index b5a1a4b8b50..09c6ca97c8d 100644 --- a/Lib/unittest.py +++ b/Lib/unittest.py @@ -425,6 +425,9 @@ class TestSuite: def __ne__(self, other): return not self == other + # Can't guarantee hash invariant, so flag as unhashable + __hash__ = None + def __iter__(self): return iter(self._tests) diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py index 9f7f62c7321..ad42947238d 100644 --- a/Lib/xml/dom/minidom.py +++ b/Lib/xml/dom/minidom.py @@ -516,6 +516,7 @@ class NamedNodeMap(object): __len__ = _get_length + __hash__ = None # Mutable type can't be correctly hashed def __cmp__(self, other): if self._attrs is getattr(other, "_attrs", None): return 0 diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 0af3f30de5e..42974f8fcca 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -3648,6 +3648,22 @@ inherit_special(PyTypeObject *type, PyTypeObject *base) type->tp_flags |= Py_TPFLAGS_DICT_SUBCLASS; } +static int +overrides_name(PyTypeObject *type, char *name) +{ + PyObject *dict = type->tp_dict; + + assert(dict != NULL); + if (PyDict_GetItemString(dict, name) != NULL) { + return 1; + } + return 0; +} + +#define OVERRIDES_HASH(x) overrides_name(x, "__hash__") +#define OVERRIDES_CMP(x) overrides_name(x, "__cmp__") +#define OVERRIDES_EQ(x) overrides_name(x, "__eq__") + static void inherit_slots(PyTypeObject *type, PyTypeObject *base) { @@ -3786,6 +3802,25 @@ inherit_slots(PyTypeObject *type, PyTypeObject *base) type->tp_compare = base->tp_compare; type->tp_richcompare = base->tp_richcompare; type->tp_hash = base->tp_hash; + /* Check for changes to inherited methods in Py3k*/ + if (Py_Py3kWarningFlag) { + if (base->tp_hash && + (base->tp_hash != PyObject_HashNotImplemented) && + !OVERRIDES_HASH(type)) { + if (OVERRIDES_CMP(type)) { + PyErr_WarnPy3k("Overriding " + "__cmp__ blocks inheritance " + "of __hash__ in 3.x", + 1); + } + if (OVERRIDES_EQ(type)) { + PyErr_WarnPy3k("Overriding " + "__eq__ blocks inheritance " + "of __hash__ in 3.x", + 1); + } + } + } } } else {