diff --git a/Include/object.h b/Include/object.h index 37537859668..2528841d520 100644 --- a/Include/object.h +++ b/Include/object.h @@ -449,6 +449,7 @@ PyAPI_FUNC(PyObject *) PyType_GenericNew(PyTypeObject *, #ifndef Py_LIMITED_API PyAPI_FUNC(PyObject *) _PyType_Lookup(PyTypeObject *, PyObject *); PyAPI_FUNC(PyObject *) _PyObject_LookupSpecial(PyObject *, char *, PyObject **); +PyAPI_FUNC(PyTypeObject *) _PyType_CalculateMetaclass(PyTypeObject *, PyObject *); #endif PyAPI_FUNC(unsigned int) PyType_ClearCache(void); PyAPI_FUNC(void) PyType_Modified(PyTypeObject *); diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 587d792f512..b214996aa9f 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -625,6 +625,174 @@ class ClassPropertiesAndMethods(unittest.TestCase): # The most derived metaclass of D is A rather than type. class D(B, C): pass + self.assertIs(A, type(D)) + + # issue1294232: correct metaclass calculation + new_calls = [] # to check the order of __new__ calls + class AMeta(type): + @staticmethod + def __new__(mcls, name, bases, ns): + new_calls.append('AMeta') + return super().__new__(mcls, name, bases, ns) + @classmethod + def __prepare__(mcls, name, bases): + return {} + + class BMeta(AMeta): + @staticmethod + def __new__(mcls, name, bases, ns): + new_calls.append('BMeta') + return super().__new__(mcls, name, bases, ns) + @classmethod + def __prepare__(mcls, name, bases): + ns = super().__prepare__(name, bases) + ns['BMeta_was_here'] = True + return ns + + class A(metaclass=AMeta): + pass + self.assertEqual(['AMeta'], new_calls) + new_calls[:] = [] + + class B(metaclass=BMeta): + pass + # BMeta.__new__ calls AMeta.__new__ with super: + self.assertEqual(['BMeta', 'AMeta'], new_calls) + new_calls[:] = [] + + class C(A, B): + pass + # The most derived metaclass is BMeta: + self.assertEqual(['BMeta', 'AMeta'], new_calls) + new_calls[:] = [] + # BMeta.__prepare__ should've been called: + self.assertIn('BMeta_was_here', C.__dict__) + + # The order of the bases shouldn't matter: + class C2(B, A): + pass + self.assertEqual(['BMeta', 'AMeta'], new_calls) + new_calls[:] = [] + self.assertIn('BMeta_was_here', C2.__dict__) + + # Check correct metaclass calculation when a metaclass is declared: + class D(C, metaclass=type): + pass + self.assertEqual(['BMeta', 'AMeta'], new_calls) + new_calls[:] = [] + self.assertIn('BMeta_was_here', D.__dict__) + + class E(C, metaclass=AMeta): + pass + self.assertEqual(['BMeta', 'AMeta'], new_calls) + new_calls[:] = [] + self.assertIn('BMeta_was_here', E.__dict__) + + # Special case: the given metaclass isn't a class, + # so there is no metaclass calculation. + marker = object() + def func(*args, **kwargs): + return marker + class X(metaclass=func): + pass + class Y(object, metaclass=func): + pass + class Z(D, metaclass=func): + pass + self.assertIs(marker, X) + self.assertIs(marker, Y) + self.assertIs(marker, Z) + + # The given metaclass is a class, + # but not a descendant of type. + prepare_calls = [] # to track __prepare__ calls + class ANotMeta: + def __new__(mcls, *args, **kwargs): + new_calls.append('ANotMeta') + return super().__new__(mcls) + @classmethod + def __prepare__(mcls, name, bases): + prepare_calls.append('ANotMeta') + return {} + class BNotMeta(ANotMeta): + def __new__(mcls, *args, **kwargs): + new_calls.append('BNotMeta') + return super().__new__(mcls) + @classmethod + def __prepare__(mcls, name, bases): + prepare_calls.append('BNotMeta') + return super().__prepare__(name, bases) + + class A(metaclass=ANotMeta): + pass + self.assertIs(ANotMeta, type(A)) + self.assertEqual(['ANotMeta'], prepare_calls) + prepare_calls[:] = [] + self.assertEqual(['ANotMeta'], new_calls) + new_calls[:] = [] + + class B(metaclass=BNotMeta): + pass + self.assertIs(BNotMeta, type(B)) + self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls) + prepare_calls[:] = [] + self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls) + new_calls[:] = [] + + class C(A, B): + pass + self.assertIs(BNotMeta, type(C)) + self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls) + new_calls[:] = [] + self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls) + prepare_calls[:] = [] + + class C2(B, A): + pass + self.assertIs(BNotMeta, type(C2)) + self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls) + new_calls[:] = [] + self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls) + prepare_calls[:] = [] + + # This is a TypeError, because of a metaclass conflict: + # BNotMeta is neither a subclass, nor a superclass of type + with self.assertRaises(TypeError): + class D(C, metaclass=type): + pass + + class E(C, metaclass=ANotMeta): + pass + self.assertIs(BNotMeta, type(E)) + self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls) + new_calls[:] = [] + self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls) + prepare_calls[:] = [] + + class F(object(), C): + pass + self.assertIs(BNotMeta, type(F)) + self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls) + new_calls[:] = [] + self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls) + prepare_calls[:] = [] + + class F2(C, object()): + pass + self.assertIs(BNotMeta, type(F2)) + self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls) + new_calls[:] = [] + self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls) + prepare_calls[:] = [] + + # TypeError: BNotMeta is neither a + # subclass, nor a superclass of int + with self.assertRaises(TypeError): + class X(C, int()): + pass + with self.assertRaises(TypeError): + class X(int(), C): + pass def test_module_subclasses(self): # Testing Python subclass of module... diff --git a/Misc/NEWS b/Misc/NEWS index fbf3b8d6199..c078623d3b4 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,10 @@ What's New in Python 3.2.3? Core and Builtins ----------------- +- Issue #1294232: In a few cases involving metaclass inheritance, the + interpreter would sometimes invoke the wrong metaclass when building a new + class object. These cases now behave correctly. Patch by Daniel Urban. + - Issue #12604: VTRACE macro expanded to no-op in _sre.c to avoid compiler warnings. Patch by Josh Triplett and Petri Lehtinen. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 856a4a5f386..9f6240168e9 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1912,6 +1912,42 @@ PyType_GetFlags(PyTypeObject *type) return type->tp_flags; } +/* Determine the most derived metatype. */ +PyTypeObject * +_PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases) +{ + Py_ssize_t i, nbases; + PyTypeObject *winner; + PyObject *tmp; + PyTypeObject *tmptype; + + /* Determine the proper metatype to deal with this, + and check for metatype conflicts while we're at it. + Note that if some other metatype wins to contract, + it's possible that its instances are not types. */ + + nbases = PyTuple_GET_SIZE(bases); + winner = metatype; + for (i = 0; i < nbases; i++) { + tmp = PyTuple_GET_ITEM(bases, i); + tmptype = Py_TYPE(tmp); + if (PyType_IsSubtype(winner, tmptype)) + continue; + if (PyType_IsSubtype(tmptype, winner)) { + winner = tmptype; + continue; + } + /* else: */ + PyErr_SetString(PyExc_TypeError, + "metaclass conflict: " + "the metaclass of a derived class " + "must be a (non-strict) subclass " + "of the metaclasses of all its bases"); + return NULL; + } + return winner; +} + static PyObject * type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds) { @@ -1955,28 +1991,12 @@ type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds) &PyDict_Type, &dict)) return NULL; - /* Determine the proper metatype to deal with this, - and check for metatype conflicts while we're at it. - Note that if some other metatype wins to contract, - it's possible that its instances are not types. */ - nbases = PyTuple_GET_SIZE(bases); - winner = metatype; - for (i = 0; i < nbases; i++) { - tmp = PyTuple_GET_ITEM(bases, i); - tmptype = Py_TYPE(tmp); - if (PyType_IsSubtype(winner, tmptype)) - continue; - if (PyType_IsSubtype(tmptype, winner)) { - winner = tmptype; - continue; - } - PyErr_SetString(PyExc_TypeError, - "metaclass conflict: " - "the metaclass of a derived class " - "must be a (non-strict) subclass " - "of the metaclasses of all its bases"); + /* Determine the proper metatype to deal with this: */ + winner = _PyType_CalculateMetaclass(metatype, bases); + if (winner == NULL) { return NULL; } + if (winner != metatype) { if (winner->tp_new != type_new) /* Pass it to the winner */ return winner->tp_new(winner, args, kwds); @@ -1984,6 +2004,7 @@ type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds) } /* Adjust for empty tuple bases */ + nbases = PyTuple_GET_SIZE(bases); if (nbases == 0) { bases = PyTuple_Pack(1, &PyBaseObject_Type); if (bases == NULL) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 6258167b608..9c50b88da8d 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -35,9 +35,10 @@ int Py_HasFileSystemDefaultEncoding = 1; static PyObject * builtin___build_class__(PyObject *self, PyObject *args, PyObject *kwds) { - PyObject *func, *name, *bases, *mkw, *meta, *prep, *ns, *cell; + PyObject *func, *name, *bases, *mkw, *meta, *winner, *prep, *ns, *cell; PyObject *cls = NULL; Py_ssize_t nargs, nbases; + int isclass; assert(args != NULL); if (!PyTuple_Check(args)) { @@ -82,17 +83,42 @@ builtin___build_class__(PyObject *self, PyObject *args, PyObject *kwds) Py_DECREF(bases); return NULL; } + /* metaclass is explicitly given, check if it's indeed a class */ + isclass = PyType_Check(meta); } } if (meta == NULL) { - if (PyTuple_GET_SIZE(bases) == 0) + /* if there are no bases, use type: */ + if (PyTuple_GET_SIZE(bases) == 0) { meta = (PyObject *) (&PyType_Type); + } + /* else get the type of the first base */ else { PyObject *base0 = PyTuple_GET_ITEM(bases, 0); meta = (PyObject *) (base0->ob_type); } Py_INCREF(meta); + isclass = 1; /* meta is really a class */ } + if (isclass) { + /* meta is really a class, so check for a more derived + metaclass, or possible metaclass conflicts: */ + winner = (PyObject *)_PyType_CalculateMetaclass((PyTypeObject *)meta, + bases); + if (winner == NULL) { + Py_DECREF(meta); + Py_XDECREF(mkw); + Py_DECREF(bases); + return NULL; + } + if (winner != meta) { + Py_DECREF(meta); + meta = winner; + Py_INCREF(meta); + } + } + /* else: meta is not a class, so we cannot do the metaclass + calculation, so we will use the explicitly given object as it is */ prep = PyObject_GetAttrString(meta, "__prepare__"); if (prep == NULL) { if (PyErr_ExceptionMatches(PyExc_AttributeError)) {