]> granicus.if.org Git - python/commitdiff
Issue 1294232: Fix errors in metaclass calculation affecting some cases of metaclass...
authorNick Coghlan <ncoghlan@gmail.com>
Sun, 23 Oct 2011 12:04:16 +0000 (22:04 +1000)
committerNick Coghlan <ncoghlan@gmail.com>
Sun, 23 Oct 2011 12:04:16 +0000 (22:04 +1000)
Include/object.h
Lib/test/test_descr.py
Misc/NEWS
Objects/typeobject.c
Python/bltinmodule.c

index 37537859668d6c67c04187667aeba556000c99d8..2528841d520e6575c19567f24b0900d5791be5f0 100644 (file)
@@ -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 *);
index 587d792f512c15f46fee2a8a74fab5ffd6dd4255..b214996aa9f0816a57860440d5c2d22fdd86eda1 100644 (file)
@@ -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...
index fbf3b8d619982fc3dd73a16e555ce13ae7863348..c078623d3b4ca0a39d3fef4867af92a5b9593bc0 100644 (file)
--- 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.
 
index 856a4a5f386dd2368a2f78bc347fa8ba041b10b3..9f6240168e99caa8fa849cb916a7d02025ffa7ac 100644 (file)
@@ -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)
index 6258167b6089f1c4eda687c2534f615881cfcc5d..9c50b88da8dfb0db98b6248d020fbd1c418dd6a3 100644 (file)
@@ -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)) {