E57E Issue 1294232: Fix errors in metaclass calculation affecting some cas… · python/cpython@de31b19 · GitHub
[go: up one dir, main page]

Skip to content

Commit de31b19

Browse files
committed
Issue 1294232: Fix errors in metaclass calculation affecting some cases of metaclass inheritance. Patch by Daniel Urban.
1 parent 711f87c commit de31b19

File tree

5 files changed

+242
-22
lines changed

5 files changed

+242
-22
lines changed

Include/object.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ PyAPI_FUNC(PyObject *) PyType_GenericNew(PyTypeObject *,
449449
#ifndef Py_LIMITED_API
450450
PyAPI_FUNC(PyObject *) _PyType_Lookup(PyTypeObject *, PyObject *);
451451
PyAPI_FUNC(PyObject *) _PyObject_LookupSpecial(PyObject *, char *, PyObject **);
452+
PyAPI_FUNC(PyTypeObject *) _PyType_CalculateMetaclass(PyTypeObject *, PyObject *);
452453
#endif
453454
PyAPI_FUNC(unsigned int) PyType_ClearCache(void);
454455
PyAPI_FUNC(void) PyType_Modified(PyTypeObject *);

Lib/test/test_descr.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,174 @@ class C(object, metaclass=A):
625625
# The most derived metaclass of D is A rather than type.
626626
class D(B, C):
627627
pass
628+
self.assertIs(A, type(D))
629+
630+
# issue1294232: correct metaclass calculation
631+
new_calls = [] # to check the order of __new__ calls
632+
class AMeta(type):
633+
@staticmethod
634+
def __new__(mcls, name, bases, ns):
635+
new_calls.append('AMeta')
636+
return super().__new__(mcls, name, bases, ns)
637+
@classmethod
638+
def __prepare__(mcls, name, bases):
639+
return {}
640+
641+
class BMeta(AMeta):
642+
@staticmethod
643+
def __new__(mcls, name, bases, ns):
644+
new_calls.append('BMeta')
645+
return super().__new__(mcls, name, bases, ns)
646+
@classmethod
647+
def __prepare__(mcls, name, bases):
648+
ns = super().__prepare__(name, bases)
649+
ns['BMeta_was_here'] = True
650+
return ns
651+
652+
class A(metaclass=AMeta):
653+
pass
654+
self.assertEqual(['AMeta'], new_calls)
655+
new_calls[:] = []
656+
657+
class B(metaclass=BMeta):
658+
pass
659+
# BMeta.__new__ calls AMeta.__new__ with super:
660+
self.assertEqual(['BMeta', 'AMeta'], new_calls)
661+
new_calls[:] = []
662+
663+
class C(A, B):
664+
pass
665+
# The most derived metaclass is BMeta:
666+
self.assertEqual(['BMeta', 'AMeta'], new_calls)
667+
new_calls[:] = []
668+
# BMeta.__prepare__ should've been called:
669+
self.assertIn('BMeta_was_here', C.__dict__)
670+
671+
# The order of the bases shouldn't matter:
672+
class C2(B, A):
673+
pass
674+
self.assertEqual(['BMeta', 'AMeta'], new_calls)
675+
new_calls[:] = []
676+
self.assertIn('BMeta_was_here', C2.__dict__)
677+
678+
# Check correct metaclass calculation when a metaclass is declared:
679+
class D(C, metaclass=type):
680+
pass
681+
self.assertEqual(['BMeta', 'AMeta'], new_calls)
682+
new_calls[:] = []
683+
self.assertIn('BMeta_was_here', D.__dict__)
684+
685+
class E(C, metaclass=AMeta):
686+
pass
687+
self.assertEqual(['BMeta', 'AMeta'], new_calls)
688+
new_calls[:] = []
689+
self.assertIn('BMeta_was_here', E.__dict__)
690+
691+
# Special case: the given metaclass isn't a class,
692+
# so there is no metaclass calculation.
693+
marker = object()
694+
def func(*args, **kwargs):
695+
return marker
696+
class X(metaclass=func):
697+
pass
698+
class Y(object, metaclass=func):
699+
pass
700+
class Z(D, metaclass=func):
701+
pass
702+
self.assertIs(marker, X)
703+
self.assertIs(marker, Y)
704+
self.assertIs(marker, Z)
705+
706+
# The given metaclass is a class,
707+
# but not a descendant of type.
708+
prepare_calls = [] # to track __prepare__ calls
709+
class ANotMeta:
710+
def __new__(mcls, *args, **kwargs):
711+
new_calls.append('ANotMeta')
712+
return super().__new__(mcls)
713+
@classmethod
714+
def __prepare__(mcls, name, bases):
715+
prepare_calls.append('ANotMeta')
716+
return {}
717+
class BNotMeta(ANotMeta):
718+
def __new__(mcls, *args, **kwargs):
719+
new_calls.append('BNotMeta')
720+
return super().__new__(mcls)
721+
@classmethod
722+
def __prepare__(mcls, name, bases):
723+
prepare_calls.append('BNotMeta')
724+
return super().__prepare__(name, bases)
725+
726+
class A(metaclass=ANotMeta):
727+
pass
728+
self.assertIs(ANotMeta, type(A))
729+
self.assertEqual(['ANotMeta'], prepare_calls)
730+
prepare_calls[:] = []
731+
self.assertEqual(['ANotMeta'], new_calls)
732+
new_calls[:] = []
733+
734+
class B(metaclass=BNotMeta):
735+
pass
736+
self.assertIs(BNotMeta, type(B))
737+
self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls)
738+
prepare_calls[:] = []
739+
self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls)
740+
new_calls[:] = []
741+
742+
class C(A, B):
743+
pass
744+
self.assertIs(BNotMeta, type(C))
745+
self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls)
746+
new_calls[:] = []
747+
self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls)
748+
prepare_calls[:] = []
749+
750+
class C2(B, A):
751+
pass
752+
self.assertIs(BNotMeta, type(C2))
753+
self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls)
754+
new_calls[:] = []
755+
self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls)
756+
prepare_calls[:] = []
757+
758+
# This is a TypeError, because of a metaclass conflict:
759+
# BNotMeta is neither a subclass, nor a superclass of type
760+
with self.assertRaises(TypeError):
761+
class D(C, metaclass=type):
762+
pass
763+
764+
class E(C, metaclass=ANotMeta):
765+
pass
766+
self.assertIs(BNotMeta, type(E))
767+
self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls)
768+
new_calls[:] = []
769+
self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls)
770+
prepare_calls[:] = []
771+
772+
class F(object(), C):
773+
pass
774+
self.assertIs(BNotMeta, type(F))
775+
self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls)
776+
new_calls[:] = []
777+
self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls)
778+
prepare_calls[:] = []
779+
780+
class F2(C, object()):
781+
pass
782+
self.assertIs(BNotMeta, type(F2))
783+
self.assertEqual(['BNotMeta', 'ANotMeta'], new_calls)
784+
new_calls[:] = []
785+
self.assertEqual(['BNotMeta', 'ANotMeta'], prepare_calls)
786+
prepare_calls[:] = []
787+
788+
# TypeError: BNotMeta is neither a
789+
# subclass, nor a superclass of int
790+
with self.assertRaises(TypeError):
791+
class X(C, int()):
792+
pass
793+
with self.assertRaises(TypeError):
794+
class X(int(), C):
795+
pass
628796

629797
def test_module_subclasses(self):
630798
# Testing Python subclass of module...

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ What's New in Python 3.2.3?
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #1294232: In a few cases involving metaclass inheritance, the
14+
interpreter would sometimes invoke the wrong metaclass when building a new
15+
class object. These cases now behave correctly. Patch by Daniel Urban.
16+
1317
- Issue #12604: VTRACE macro expanded to no-op in _sre.c to avoid compiler
1418
warnings. Patch by Josh Triplett and Petri Lehtinen.
1519

Objects/typeobject.c

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,42 @@ PyType_GetFlags(PyTypeObject *type)
19121912
return type->tp_flags;
19131913
}
19141914

1915+
/* Determine the most derived metatype. */
1916+
PyTypeObject *
1917+
_PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases)
1918+
{
1919+
Py_ssize_t i, nbases;
1920+
PyTypeObject *winner;
1921+
PyObject *tmp;
1922+
PyTypeObject *tmptype;
1923+
1924+
/* Determine the proper metatype to deal with this,
1925+
and check for metatype conflicts while we're at it.
1926+
Note that if some other metatype wins to contract,
1927+
it's possible that its instances are not types. */
1928+
1929+
nbases = PyTuple_GET_SIZE(bases);
1930+
winner = metatype;
1931+
for (i = 0; i < nbases; i++) {
1932+
tmp = PyTuple_GET_ITEM(bases, i);
1933+
tmptype = Py_TYPE(tmp);
1934+
if (PyType_IsSubtype(winner, tmptype))
1935+
continue;
1936+
if (PyType_IsSubtype(tmptype, winner)) {
1937+
winner = tmptype;
1938+
continue;
1939+
}
1940+
/* else: */
1941+
PyErr_SetString(PyExc_TypeError,
1942+
"metaclass conflict: "
1943+
"the metaclass of a derived class "
1944+
"must be a (non-strict) subclass "
1945+
"of the metaclasses of all its bases");
1946+
return NULL;
1947+
}
1948+
return winner;
1949+
}
1950+
19151951
static PyObject *
19161952
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
19171953
{
@@ -1955,35 +1991,20 @@ type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
19551991
&PyDict_Type, &dict))
19561992
return NULL;
19571993

1958-
/* Determine the proper metatype to deal with this,
1959-
and check for metatype conflicts while we're at it.
1960-
Note that if some other metatype wins to contract,
1961-
it's possible that its instances are not types. */
1962-
nbases = PyTuple_GET_SIZE(bases);
1963-
winner = metatype;
1964-
for (i = 0; i < nbases; i++) {
1965-
tmp = PyTuple_GET_ITEM(bases, i);
1966-
tmptype = Py_TYPE(tmp);
1967-
if (PyType_IsSubtype(winner, tmptype))
1968-
continue;
1969-
if (PyType_IsSubtype(tmptype, winner)) {
1970-
winner = tmptype;
1971-
continue;
1972-
}
1973-
PyErr_SetString(PyExc_TypeError,
1974-
"metaclass conflict: "
1975-
"the metaclass of a derived class "
1976-
"must be a (non-strict) subclass "
1977-
"of the metaclasses of all its bases");
1994+
/* Determine the proper metatype to deal with this: */
1995+
winner = _PyType_CalculateMetaclass(metatype, bases);
1996+
if (winner == NULL) {
19781997
return NULL;
19791998
}
1999+
19802000
if (winner != metatype) {
19812001
if (winner->tp_new != type_new) /* Pass it to the winner */
19822002
return winner->tp_new(winner, args, kwds);
19832003
metatype = winner;
19842004
}
19852005

19862006
/* Adjust for empty tuple bases */
2007+
nbases = PyTuple_GET_SIZE(bases);
19872008
if (nbases == 0) {
19882009
bases = PyTuple_Pack(1, &PyBaseObject_Type);
19892010
if (bases == NULL)

Python/bltinmodule.c

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ int Py_HasFileSystemDefaultEncoding = 1;
3535
static PyObject *
3636
builtin___build_class__(PyObject *self, PyObject *args, PyObject *kwds)
3737
{
38-
PyObject *func, *name, *bases, *mkw, *meta, *prep, *ns, *cell;
38+
PyObject *func, *name, *bases, *mkw, *meta, *winner, *prep, *ns, *cell;
3939
PyObject *cls = NULL;
4040
Py_ssize_t nargs, nbases;
41+
int isclass;
4142

4243
assert(args != NULL);
4344
if (!PyTuple_Check(args)) {
@@ -82,17 +83,42 @@ builtin___build_class__(PyObject *self, PyObject *args, PyObject *kwds)
8283
Py_DECREF(bases);
8384
return NULL;
8485
}
86+
/* metaclass is explicitly given, check if it's indeed a class */
87+
isclass = PyType_Check(meta);
8588
}
8689
}
8790
if (meta == NULL) {
88-
if (PyTuple_GET_SIZE(bases) == 0)
91+
/* if there are no bases, use type: */
92+
if (PyTuple_GET_SIZE(bases) == 0) {
8993
meta = (PyObject *) (&PyType_Type);
94+
}
95+
/* else get the type of the first base */
9096
else {
9197
PyObject *base0 = PyTuple_GET_ITEM(bases, 0);
9298
meta = (PyObject *) (base0->ob_type);
9399
}
94100
Py_INCREF(meta);
101+
isclass = 1; /* meta is really a class */
102+
}
103+
if (isclass) {
104+
/* meta is really a class, so check for a more derived
105+
metaclass, or possible metaclass conflicts: */
106+
winner = (PyObject *)_PyType_CalculateMetaclass((PyTypeObject *)meta,
107+
bases);
108+
if (winner == NULL) {
109+
Py_DECREF(meta);
110+
Py_XDECREF(mkw);
111+
Py_DECREF(bases);
112+
return NULL;
113+
}
114+
if (winner != meta) {
115+
Py_DECREF(meta);
116+
meta = winner;
117+
Py_INCREF(meta);
118+
}
95119
}
120+
/* else: meta is not a class, so we cannot do the metaclass
121+
calculation, so we will use the explicitly given object as it is */
96122
prep = PyObject_GetAttrString(meta, "__prepare__");
97123
if (prep == NULL) {
98124
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {

0 commit comments

Comments
 (0)
0