From b1c52cc1e2dfdecc449edc83141824b8726926a6 Mon Sep 17 00:00:00 2001 From: AsgerJon Date: Wed, 28 May 2025 14:39:29 +0200 Subject: [PATCH 1/3] Improved error message for metaclass incompatibility --- Lib/test/test_types.py | 14 +++++++ .../2025-05-28-metaclass-error.rst | 2 + Objects/typeobject.c | 42 ++++++++++++++++--- 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-05-28-metaclass-error.rst diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 9011e0e1962820..042a37519b2a5d 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -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() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-28-metaclass-error.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-28-metaclass-error.rst new file mode 100644 index 00000000000000..9665446606e16c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-28-metaclass-error.rst @@ -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. \ No newline at end of file diff --git a/Objects/typeobject.c b/Objects/typeobject.c index db923c164774b7..c3a7bdf8d92936 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4129,11 +4129,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"); + /* Construct improved error message */ + 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__"); + 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( + "Metaclass conflict while defining class:\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.", + 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; From 4edcee1da99dbf1821085ae276311c03f521eb6d Mon Sep 17 00:00:00 2001 From: AsgerJon Date: Wed, 28 May 2025 21:33:50 +0200 Subject: [PATCH 2/3] Improved error message for metaclass incompatibility --- Include/internal/pycore_object.h | 2 +- Objects/typeobject.c | 22 ++++++++++++++++------ Python/bltinmodule.c | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index 50225623fe52db..c44414f3ccb1a6 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -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); diff --git a/Objects/typeobject.c b/Objects/typeobject.c index c3a7bdf8d92936..441b6cedb1c7b4 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -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, @@ -4129,7 +4130,6 @@ _PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases) continue; } /* else: */ - /* Construct improved error message */ PyObject *declared_meta_name = NULL; PyObject *base_class_name = NULL; PyObject *base_meta_name = NULL; @@ -4141,12 +4141,13 @@ _PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases) if (declared_meta_name && base_class_name && base_meta_name) { format_result = PyUnicode_FromFormat( - "Metaclass conflict while defining class:\n" + "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 @@ -4949,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; } @@ -5365,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); + } + + 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'.", diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index e08c63924ca16d..94ccfbc08ae784 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -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); if (winner == NULL) { goto error; } From 9b4eb9c2c8b066dd401da4828aa65cf76737b042 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 20:47:07 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2025-05-28-20-47-06.gh-issue-skip.CO_d25.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-05-28-20-47-06.gh-issue-skip.CO_d25.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-28-20-47-06.gh-issue-skip.CO_d25.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-28-20-47-06.gh-issue-skip.CO_d25.rst new file mode 100644 index 00000000000000..b4db42fc84c534 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-28-20-47-06.gh-issue-skip.CO_d25.rst @@ -0,0 +1 @@ +Metaclass incompatibility error message now more clearly explains the issue including the name of the offending baseclass and the conflicting metaclasses.