Skip to content

Improved error message for metaclass incompatibility #134864

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,7 @@ static inline int _PyType_SUPPORTS_WEAKREFS(PyTypeObject *type) {
extern PyObject* _PyType_AllocNoTrack(PyTypeObject *type, Py_ssize_t nitems);
PyAPI_FUNC(PyObject *) _PyType_NewManagedObject(PyTypeObject *type);

extern PyTypeObject* _PyType_CalculateMetaclass(PyTypeObject *, PyObject *);
extern PyTypeObject* _PyType_CalculateMetaclass(PyTypeObject *, PyObject *, PyObject *);
extern PyObject* _PyType_GetDocFromInternalDoc(const char *, const char *);
extern PyObject* _PyType_GetTextSignatureFromInternalDoc(const char *, const char *, int);
extern int _PyObject_SetAttributeErrorContext(PyObject *v, PyObject* name);
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2562,5 +2562,19 @@ def collate_results(raw):
self.assertEqual(interp_results, {})


class TestMetaclassConflict(unittest.TestCase):
def test_metaclass_conflict_message(self):
class MetaFoo(type): pass
class MetaBar(type): pass
class Bar(metaclass=MetaBar): pass
with self.assertRaises(TypeError) as cm:
class Foo(Bar, metaclass=MetaFoo): pass
msg = str(cm.exception)
self.assertIn("Metaclass conflict", msg)
self.assertIn("MetaFoo", msg)
self.assertIn("MetaBar", msg)
self.assertIn("Bar", msg)


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Metaclass incompatibility error message now more clearly explains the issue including the name of the offending baseclass and the conflicting metaclasses.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this file, you can go ahead and delete it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Metaclass incompatibility error message now more clearly explains the issue
including the name of the offending baseclass and the conflicting metaclasses.
60 changes: 51 additions & 9 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -4105,13 +4105,14 @@ PyType_SUPPORTS_WEAKREFS(PyTypeObject *type)

/* Determine the most derived metatype. */
PyTypeObject *
_PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases)
_PyType_CalculateMetaclass(PyTypeObject *metatype,
PyObject *bases,
PyObject *name)
{
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,
Expand All @@ -4129,11 +4130,43 @@ _PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases)
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");
PyObject *declared_meta_name = NULL;
PyObject *base_class_name = NULL;
PyObject *base_meta_name = NULL;
PyObject *format_result = NULL;

declared_meta_name = PyObject_GetAttrString((PyObject *)winner, "__name__");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyObject_GetAttrString sets an exception when it fails, so we might try and invoke the eval loop in the subsequent calls, which isn't safe. It needs to be in a pattern like this:

PyObject *first_attr = PyObject_GetAttrString(whatever, "first_attr");
if (first_attr == NULL) {
    return NULL;
}
PyObject *second_attr = PyObject_GetAttrString(whatever, "second_attr");
if (second_attr == NULL) {
    Py_DECREF(first_attr);
    return NULL;
}

/* ... */

Py_DECREF(first_attr);
Py_DECREF(second_attr);

base_class_name = PyObject_GetAttrString(tmp, "__name__");
base_meta_name = PyObject_GetAttrString((PyObject *)tmptype, "__name__");

if (declared_meta_name && base_class_name && base_meta_name) {
format_result = PyUnicode_FromFormat(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use PyErr_Format instead of manually creating the string.

"Metaclass conflict while defining class: '%U'\n"
"- Declared metaclass: '%U'\n"
"- Incompatible base class: '%U'\n"
"- That base is derived from metaclass: '%U'\n"
"All base classes must be based on classes derived from the same "
"metaclass or a subclass thereof.",
name,
declared_meta_name,
base_class_name,
base_meta_name
);
if (format_result) {
PyErr_SetObject(PyExc_TypeError, format_result);
Py_DECREF(format_result);
}
} else {
/* fallback */
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");
}

Py_XDECREF(declared_meta_name);
Py_XDECREF(base_class_name);
Py_XDECREF(base_meta_name);
return NULL;
}
return winner;
Expand Down Expand Up @@ -4917,7 +4950,7 @@ type_new_get_bases(type_new_ctx *ctx, PyObject **type)

// Search the bases for the proper metatype to deal with this
PyTypeObject *winner;
winner = _PyType_CalculateMetaclass(ctx->metatype, ctx->bases);
winner = _PyType_CalculateMetaclass(ctx->metatype, ctx->bases, ctx->name);
if (winner == NULL) {
return -1;
}
Expand Down Expand Up @@ -5333,10 +5366,19 @@ PyType_FromMetaclass(
if (!metaclass) {
metaclass = &PyType_Type;
}
metaclass = _PyType_CalculateMetaclass(metaclass, bases);
/* Get resolve the name from 'spec' */
PyObject *name = NULL;
if (spec && spec->name) {
name = PyUnicode_FromString(spec->name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have to manually convert this to a string. PyUnicode_FromFormat (or PyErr_Format) accept C strings with the %s format specifier.

}

metaclass = _PyType_CalculateMetaclass(metaclass, bases, name);
if (metaclass == NULL) {
goto finally;
}
/* Remove name again */
Py_XDECREF(name);

if (!PyType_Check(metaclass)) {
PyErr_Format(PyExc_TypeError,
"Metaclass '%R' is not a subclass of 'type'.",
Expand Down
2 changes: 1 addition & 1 deletion Python/bltinmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
/* meta is really a class, so check for a more derived
metaclass, or possible metaclass conflicts: */
winner = (PyObject *)_PyType_CalculateMetaclass((PyTypeObject *)meta,
bases);
bases,name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bases,name);
bases, name);

if (winner == NULL) {
goto error;
}
Expand Down
Loading