diff --git a/src/capi/typeobject.cpp b/src/capi/typeobject.cpp index c88a9c1d59e7cfc2b44967a0d2c9e9b447cbe2ea..177bd05c3cff76d2b0f40bd40f2df3b508c0eafb 100644 --- a/src/capi/typeobject.cpp +++ b/src/capi/typeobject.cpp @@ -3317,8 +3317,24 @@ extern "C" int PyType_Ready(PyTypeObject* cls) noexcept { cls->gc_visit = &conservativeGCHandler; cls->is_user_defined = true; - // this should get automatically initialized to 0 on this path: - assert(cls->attrs_offset == 0); + + if (!cls->instancesHaveHCAttrs() && cls->tp_base) { + // These doesn't get copied in inherit_slots like other slots do. + if (cls->tp_base->instancesHaveHCAttrs()) { + cls->attrs_offset = cls->tp_base->attrs_offset; + } + + // Example of when this code path could be reached and needs to be: + // If this class is a metaclass defined in a C extension, chances are that some of its + // instances may be hardcoded in the C extension as well. Those instances will call + // PyType_Ready and expect their class (this metaclass) to have a place to put attributes. + // e.g. CTypes does this. + bool is_metaclass = PyType_IsSubtype(cls, type_cls); + assert(!is_metaclass || cls->instancesHaveHCAttrs() || cls->instancesHaveDictAttrs()); + } else { + // this should get automatically initialized to 0 on this path: + assert(cls->attrs_offset == 0); + } if (Py_TPFLAGS_BASE_EXC_SUBCLASS & cls->tp_flags) { exception_types.push_back(cls); diff --git a/src/runtime/types.h b/src/runtime/types.h index 619e4bc517ec5fd960cb9afe83b7b685ce0154e7..32012c8291adf274ffb561e7984966b70492ac2f 100644 --- a/src/runtime/types.h +++ b/src/runtime/types.h @@ -197,7 +197,7 @@ public: // Analogous to tp_dictoffset // A class should have at most of one attrs_offset and tp_dictoffset be nonzero. // (But having nonzero attrs_offset here would map to having nonzero tp_dictoffset in CPython) - const int attrs_offset; + int attrs_offset; bool instancesHaveHCAttrs() { return attrs_offset != 0; } bool instancesHaveDictAttrs() { return tp_dictoffset != 0; }