diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ec69ad746..fa74569a78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,34 @@ jobs: python-version: ${{ matrix.python-version }} cmake-args: -DPYBIND11_TEST_PRECOMPILE=ON + # The legacy PyHeapTypeObject type-creation path on CPython 3.12+ + # (PyType_FromMetaclass is the default there). + type-creation-legacy: + if: github.event.pull_request.draft == false + strategy: + fail-fast: false + matrix: + include: + - runs-on: ubuntu-latest + python-version: '3.12' + cmake-args: -DPYBIND11_TYPE_CREATION_VIA_SPEC=OFF + - runs-on: ubuntu-latest + python-version: '3.14' + cmake-args: -DPYBIND11_TYPE_CREATION_VIA_SPEC=OFF -DPYBIND11_TEST_PRECOMPILE=ON + - runs-on: macos-latest + python-version: '3.13' + cmake-args: -DPYBIND11_TYPE_CREATION_VIA_SPEC=OFF + - runs-on: windows-2022 + python-version: '3.13' + cmake-args: -DPYBIND11_TYPE_CREATION_VIA_SPEC=OFF + + name: 📐 + uses: ./.github/workflows/reusable-standard.yml + with: + runs-on: ${{ matrix.runs-on }} + python-version: ${{ matrix.python-version }} + cmake-args: ${{ matrix.cmake-args }} + standard-large: if: github.event.pull_request.draft == false strategy: diff --git a/CMakeLists.txt b/CMakeLists.txt index 46f0a1f11d..77a19add9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,6 +99,12 @@ option(PYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION "To enforce that a handle_type_name<> specialization exists" OFF) option(PYBIND11_SIMPLE_GIL_MANAGEMENT "Use simpler GIL management logic that does not support disassociation" OFF) +set(PYBIND11_TYPE_CREATION_VIA_SPEC + "" + CACHE + STRING + "Create type objects with PyType_FromMetaclass: ON, OFF, or empty for the default (ON on CPython 3.12+)" +) set(PYBIND11_INTERNALS_VERSION "" CACHE STRING "Override the ABI version, may be used to enable the unstable ABI.") @@ -323,6 +329,11 @@ if(NOT TARGET pybind11_headers) if(PYBIND11_SIMPLE_GIL_MANAGEMENT) target_compile_definitions(pybind11_headers INTERFACE PYBIND11_SIMPLE_GIL_MANAGEMENT) endif() + if(PYBIND11_TYPE_CREATION_VIA_SPEC) + target_compile_definitions(pybind11_headers INTERFACE PYBIND11_TYPE_CREATION_VIA_SPEC=1) + elseif(NOT "${PYBIND11_TYPE_CREATION_VIA_SPEC}" STREQUAL "") + target_compile_definitions(pybind11_headers INTERFACE PYBIND11_TYPE_CREATION_VIA_SPEC=0) + endif() else() # It is invalid to install a target twice, too. set(PYBIND11_INSTALL OFF) diff --git a/docs/compiling.rst b/docs/compiling.rst index a81b16fead..5169f5feab 100644 --- a/docs/compiling.rst +++ b/docs/compiling.rst @@ -435,8 +435,9 @@ Requirements and caveats: macros ``PYBIND11_INTERNALS_VERSION``, ``Py_GIL_DISABLED``, ``PYBIND11_SIMPLE_GIL_MANAGEMENT``, ``PYBIND11_DETAILED_ERROR_MESSAGES`` (defaults on in debug builds), - ``PYBIND11_HAS_SUBINTERPRETER_SUPPORT``, and - ``PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET``. A mismatch produces one + ``PYBIND11_HAS_SUBINTERPRETER_SUPPORT``, + ``PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET``, and + ``PYBIND11_TYPE_CREATION_VIA_SPEC``. A mismatch produces one readable undefined symbol at link time referencing ``pybind11_precompiled_config``. * Configuration macros that only change code inside the library (for example diff --git a/include/pybind11/detail/class-inl.h b/include/pybind11/detail/class-inl.h index 5cbaee50c3..36878c4d3d 100644 --- a/include/pybind11/detail/class-inl.h +++ b/include/pybind11/detail/class-inl.h @@ -36,6 +36,18 @@ PYBIND11_INLINE PyTypeObject *type_incref(PyTypeObject *type) { return type; } +#if PYBIND11_TYPE_CREATION_VIA_SPEC +PYBIND11_INLINE PyTypeObject * +type_from_spec(const char *caller, PyTypeObject *metaclass, PyType_Spec *spec, PyObject *bases) { + PyObject *type = PyType_FromMetaclass(metaclass, nullptr, spec, bases); + if (type == nullptr) { + pybind11_fail(std::string(caller) + + ": failure in PyType_FromMetaclass(): " + error_string()); + } + return reinterpret_cast(type); +} +#endif + #if !defined(PYPY_VERSION) extern "C" PYBIND11_INLINE PyObject * pybind11_static_get(PyObject *self, PyObject * /*ob*/, PyObject *cls) { @@ -48,6 +60,31 @@ pybind11_static_set(PyObject *self, PyObject *obj, PyObject *value) { return PyProperty_Type.tp_descr_set(self, cls, value); } +# if PYBIND11_TYPE_CREATION_VIA_SPEC + +PYBIND11_INLINE PyTypeObject *make_static_property_type() { + // Since Python-3.12 property-derived types are required to have dynamic attributes (to set + // `__doc__`), hence the GC and dict slots. + static PyType_Slot slots[] = {{Py_tp_descr_get, reinterpret_cast(pybind11_static_get)}, + {Py_tp_descr_set, reinterpret_cast(pybind11_static_set)}, + {Py_tp_traverse, reinterpret_cast(pybind11_traverse)}, + {Py_tp_clear, reinterpret_cast(pybind11_clear)}, + {Py_tp_getset, reinterpret_cast(dynamic_attr_getset())}, + {0, nullptr}}; + static PyType_Spec spec + = {PYBIND11_DUMMY_MODULE_NAME ".pybind11_static_property", + 0, // inherit from property + 0, + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_MANAGED_DICT, + slots}; + return type_from_spec("make_static_property_type()", + nullptr, + &spec, + reinterpret_cast(&PyProperty_Type)); +} + +# else // legacy: fill in a PyHeapTypeObject by hand + PYBIND11_INLINE PyTypeObject *make_static_property_type() { constexpr auto *name = "pybind11_static_property"; auto name_obj = reinterpret_steal(PYBIND11_FROM_STRING(name)); @@ -62,9 +99,9 @@ PYBIND11_INLINE PyTypeObject *make_static_property_type() { } heap_type->ht_name = name_obj.inc_ref().ptr(); -# ifdef PYBIND11_BUILTIN_QUALNAME +# ifdef PYBIND11_BUILTIN_QUALNAME heap_type->ht_qualname = name_obj.inc_ref().ptr(); -# endif +# endif auto *type = &heap_type->ht_type; type->tp_name = name; @@ -73,11 +110,11 @@ PYBIND11_INLINE PyTypeObject *make_static_property_type() { type->tp_descr_get = pybind11_static_get; type->tp_descr_set = pybind11_static_set; -# if PY_VERSION_HEX >= 0x030C0000 +# if PY_VERSION_HEX >= 0x030C0000 // Since Python-3.12 property-derived types are required to // have dynamic attributes (to set `__doc__`) enable_dynamic_attributes(heap_type); -# endif +# endif if (PyType_Ready(type) < 0) { pybind11_fail("make_static_property_type(): failure in PyType_Ready()!"); @@ -89,6 +126,8 @@ PYBIND11_INLINE PyTypeObject *make_static_property_type() { return type; } +# endif // PYBIND11_TYPE_CREATION_VIA_SPEC + #else // PYPY PYBIND11_INLINE PyTypeObject *make_static_property_type() { auto d = dict(); @@ -224,6 +263,26 @@ extern "C" PYBIND11_INLINE void pybind11_meta_dealloc(PyObject *obj) { PyType_Type.tp_dealloc(obj); } +#if PYBIND11_TYPE_CREATION_VIA_SPEC + +PYBIND11_INLINE PyTypeObject *make_default_metaclass() { + static PyType_Slot slots[] + = {{Py_tp_call, reinterpret_cast(pybind11_meta_call)}, + {Py_tp_setattro, reinterpret_cast(pybind11_meta_setattro)}, + {Py_tp_getattro, reinterpret_cast(pybind11_meta_getattro)}, + {Py_tp_dealloc, reinterpret_cast(pybind11_meta_dealloc)}, + {0, nullptr}}; + static PyType_Spec spec = {PYBIND11_DUMMY_MODULE_NAME ".pybind11_type", + 0, // inherit from type + 0, + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + slots}; + return type_from_spec( + "make_default_metaclass()", nullptr, &spec, reinterpret_cast(&PyType_Type)); +} + +#else // legacy: fill in a PyHeapTypeObject by hand + PYBIND11_INLINE PyTypeObject *make_default_metaclass() { constexpr auto *name = "pybind11_type"; auto name_obj = reinterpret_steal(PYBIND11_FROM_STRING(name)); @@ -238,9 +297,9 @@ PYBIND11_INLINE PyTypeObject *make_default_metaclass() { } heap_type->ht_name = name_obj.inc_ref().ptr(); -#ifdef PYBIND11_BUILTIN_QUALNAME +# ifdef PYBIND11_BUILTIN_QUALNAME heap_type->ht_qualname = name_obj.inc_ref().ptr(); -#endif +# endif auto *type = &heap_type->ht_type; type->tp_name = name; @@ -264,6 +323,8 @@ PYBIND11_INLINE PyTypeObject *make_default_metaclass() { return type; } +#endif // PYBIND11_TYPE_CREATION_VIA_SPEC + PYBIND11_INLINE void traverse_offset_bases(void *valueptr, const detail::type_info *tinfo, instance *self, @@ -490,6 +551,34 @@ extern "C" PYBIND11_INLINE void pybind11_object_dealloc(PyObject *self) { Py_DECREF(type); } +#if PYBIND11_TYPE_CREATION_VIA_SPEC + +PYBIND11_INLINE PyObject *make_object_base_type(PyTypeObject *metaclass) { + /* Support weak references (needed for the keep_alive feature) */ + static PyMemberDef members[] = { + {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(instance, weakrefs), Py_READONLY, nullptr}, + {nullptr, 0, 0, 0, nullptr}}; + static PyType_Slot slots[] + = {{Py_tp_new, reinterpret_cast(pybind11_object_new)}, + {Py_tp_init, reinterpret_cast(pybind11_object_init)}, + {Py_tp_dealloc, reinterpret_cast(pybind11_object_dealloc)}, + {Py_tp_members, reinterpret_cast(members)}, + {0, nullptr}}; + static PyType_Spec spec = {PYBIND11_DUMMY_MODULE_NAME ".pybind11_object", + static_cast(sizeof(instance)), + 0, + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + slots}; + PyTypeObject *type = type_from_spec("make_object_base_type()", + metaclass, + &spec, + reinterpret_cast(&PyBaseObject_Type)); + assert(!PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC)); + return reinterpret_cast(type); +} + +#else // legacy: fill in a PyHeapTypeObject by hand + PYBIND11_INLINE PyObject *make_object_base_type(PyTypeObject *metaclass) { constexpr auto *name = "pybind11_object"; auto name_obj = reinterpret_steal(PYBIND11_FROM_STRING(name)); @@ -504,9 +593,9 @@ PYBIND11_INLINE PyObject *make_object_base_type(PyTypeObject *metaclass) { } heap_type->ht_name = name_obj.inc_ref().ptr(); -#ifdef PYBIND11_BUILTIN_QUALNAME +# ifdef PYBIND11_BUILTIN_QUALNAME heap_type->ht_qualname = name_obj.inc_ref().ptr(); -#endif +# endif auto *type = &heap_type->ht_type; type->tp_name = name; @@ -532,6 +621,8 @@ PYBIND11_INLINE PyObject *make_object_base_type(PyTypeObject *metaclass) { return reinterpret_cast(heap_type); } +#endif // PYBIND11_TYPE_CREATION_VIA_SPEC + extern "C" PYBIND11_INLINE int pybind11_traverse(PyObject *self, visitproc visit, void *arg) { #if PY_VERSION_HEX >= 0x030D0000 int ret = PyObject_VisitManagedDict(self, visit, arg); @@ -557,6 +648,13 @@ extern "C" PYBIND11_INLINE int pybind11_clear(PyObject *self) { return 0; } +PYBIND11_INLINE PyGetSetDef *dynamic_attr_getset() { + static PyGetSetDef getset[] + = {{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, nullptr, nullptr}, + {nullptr, nullptr, nullptr, nullptr, nullptr}}; + return getset; +} + PYBIND11_INLINE void enable_dynamic_attributes(PyHeapTypeObject *heap_type) { auto *type = &heap_type->ht_type; type->tp_flags |= Py_TPFLAGS_HAVE_GC; @@ -568,11 +666,7 @@ PYBIND11_INLINE void enable_dynamic_attributes(PyHeapTypeObject *heap_type) { #endif type->tp_traverse = pybind11_traverse; type->tp_clear = pybind11_clear; - - static PyGetSetDef getset[] - = {{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, nullptr, nullptr}, - {nullptr, nullptr, nullptr, nullptr, nullptr}}; - type->tp_getset = getset; + type->tp_getset = dynamic_attr_getset(); } extern "C" PYBIND11_INLINE int pybind11_getbuffer(PyObject *obj, Py_buffer *view, int flags) { @@ -686,7 +780,96 @@ PYBIND11_INLINE void enable_buffer_protocol(PyHeapTypeObject *heap_type) { heap_type->as_buffer.bf_releasebuffer = pybind11_releasebuffer; } +#if PYBIND11_TYPE_CREATION_VIA_SPEC + +PYBIND11_INLINE PyObject *make_new_python_type(const type_record &rec) { + auto &internals = get_internals(); + auto *metaclass = rec.metaclass.ptr() ? reinterpret_cast(rec.metaclass.ptr()) + : internals.default_metaclass; + + // PyType_FromMetaclass() cannot run a pre-PyType_Ready callback, rejects metaclasses with a + // custom tp_new, and refuses a metaclass that is not at least as derived as the metaclass of + // each base. The legacy path accepts all three. + bool base_metaclass_mismatch = false; + for (handle b : rec.bases) { + base_metaclass_mismatch |= PyType_IsSubtype(metaclass, Py_TYPE(b.ptr())) == 0; + } + if (rec.custom_type_setup_callback || base_metaclass_mismatch + || metaclass->tp_new != PyType_Type.tp_new + || (metaclass != internals.default_metaclass + && PyType_IsSubtype(metaclass, internals.default_metaclass) == 0)) { + return make_new_python_type_legacy(rec); + } + + object module_ = get_module_name_if_available(rec.scope); + // PyType_FromMetaclass() copies the name, derives __module__ from the part before the last + // dot, and warns if there is none. + const std::string full_name + = (module_ ? str(module_).cast() : PYBIND11_DUMMY_MODULE_NAME) + "." + + rec.name; + + auto bases = tuple(rec.bases); + if (bases.empty()) { + bases = make_tuple(handle(internals.instance_base)); + } + object base = bases.size() == 1 ? bases[0].cast() : static_cast(bases); + + std::vector slots; + /* Don't inherit base __init__ */ + slots.push_back({Py_tp_init, reinterpret_cast(pybind11_object_init)}); + if (rec.doc && options::show_user_defined_docstrings()) { + slots.push_back({Py_tp_doc, const_cast(rec.doc)}); + } + unsigned int flags = Py_TPFLAGS_DEFAULT; + if (!rec.is_final) { + flags |= Py_TPFLAGS_BASETYPE; + } + if (rec.dynamic_attr) { + flags |= Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_MANAGED_DICT; + slots.push_back({Py_tp_traverse, reinterpret_cast(pybind11_traverse)}); + slots.push_back({Py_tp_clear, reinterpret_cast(pybind11_clear)}); + slots.push_back({Py_tp_getset, reinterpret_cast(dynamic_attr_getset())}); + } + if (rec.buffer_protocol) { + slots.push_back({Py_bf_getbuffer, reinterpret_cast(pybind11_getbuffer)}); + slots.push_back({Py_bf_releasebuffer, reinterpret_cast(pybind11_releasebuffer)}); + } + slots.push_back({0, nullptr}); + + // basicsize 0: inherit the instance layout from the base + PyType_Spec spec = {full_name.c_str(), 0, 0, flags, slots.data()}; + auto *type = type_from_spec(rec.name, metaclass, &spec, base.ptr()); + assert(!rec.dynamic_attr || PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC)); + + /* Register type with the parent scope */ + if (rec.scope) { + setattr(rec.scope, rec.name, reinterpret_cast(type)); + } else { + Py_INCREF(type); // Keep it alive forever (reference leak) + } + + if (module_) { // Needed by pydoc + setattr(reinterpret_cast(type), "__module__", module_); + } + if (rec.scope && !PyModule_Check(rec.scope.ptr()) && hasattr(rec.scope, "__qualname__")) { + setattr(reinterpret_cast(type), + "__qualname__", + reinterpret_steal(PyUnicode_FromFormat( + "%U.%s", rec.scope.attr("__qualname__").ptr(), rec.name))); + } + + return reinterpret_cast(type); +} + +#else + PYBIND11_INLINE PyObject *make_new_python_type(const type_record &rec) { + return make_new_python_type_legacy(rec); +} + +#endif // PYBIND11_TYPE_CREATION_VIA_SPEC + +PYBIND11_INLINE PyObject *make_new_python_type_legacy(const type_record &rec) { auto name = reinterpret_steal(PYBIND11_FROM_STRING(rec.name)); auto qualname = name; diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 8716223d68..a523d1722f 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -37,6 +37,12 @@ std::string get_fully_qualified_tp_name(PyTypeObject *type); PyTypeObject *type_incref(PyTypeObject *type); +#if PYBIND11_TYPE_CREATION_VIA_SPEC +/// PyType_FromMetaclass(), or pybind11_fail() with the Python error prefixed by `caller`. +PyTypeObject * +type_from_spec(const char *caller, PyTypeObject *metaclass, PyType_Spec *spec, PyObject *bases); +#endif + #if !defined(PYPY_VERSION) /// `pybind11_static_property.__get__()`: Always pass the class instead of the instance. @@ -146,6 +152,9 @@ extern "C" int pybind11_traverse(PyObject *self, visitproc visit, void *arg); /// dynamic_attr: Allow the GC to clear the dictionary. extern "C" int pybind11_clear(PyObject *self); +/// The `__dict__` descriptor for types with dynamic attributes. +PyGetSetDef *dynamic_attr_getset(); + /// Give instances of this type a `__dict__` and opt into garbage collection. void enable_dynamic_attributes(PyHeapTypeObject *heap_type); @@ -162,6 +171,11 @@ void enable_buffer_protocol(PyHeapTypeObject *heap_type); Return value: New reference. */ PyObject *make_new_python_type(const type_record &rec); +/// The hand-filled PyHeapTypeObject path: the only one before CPython 3.12 or with +/// PYBIND11_TYPE_CREATION_VIA_SPEC=0. Otherwise still used for `py::custom_type_setup` and for +/// metaclasses that PyType_FromMetaclass() rejects. +PyObject *make_new_python_type_legacy(const type_record &rec); + PYBIND11_WARNING_POP PYBIND11_NAMESPACE_END(detail) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 142c23c772..e706708527 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -237,6 +237,29 @@ # define PYBIND11_SIMPLE_GIL_MANAGEMENT #endif +// Create the pybind11 type objects (metaclass, static property, instance base and all bound +// classes) through PyType_FromMetaclass() with PyType_Spec instead of filling in raw +// PyHeapTypeObject fields. The default on CPython 3.12+; define PYBIND11_TYPE_CREATION_VIA_SPEC=0 +// to opt out. `py::custom_type_setup()`, metaclasses with a custom `tp_new`, and metaclass +// combinations that PyType_FromMetaclass() rejects still use the legacy path. A user-defined +// PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET also selects the legacy path: the spec path uses +// Py_TPFLAGS_MANAGED_DICT, and all bound classes must share one `__dict__` layout. +#if !defined(PYBIND11_TYPE_CREATION_VIA_SPEC) +# if PY_VERSION_HEX >= 0x030C0000 && !defined(PYPY_VERSION) && !defined(GRAALVM_PYTHON) \ + && !defined(PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET) +# define PYBIND11_TYPE_CREATION_VIA_SPEC 1 +# else +# define PYBIND11_TYPE_CREATION_VIA_SPEC 0 +# endif +#elif PYBIND11_TYPE_CREATION_VIA_SPEC +# if PY_VERSION_HEX < 0x030C0000 || defined(PYPY_VERSION) || defined(GRAALVM_PYTHON) +# error "PYBIND11_TYPE_CREATION_VIA_SPEC requires CPython 3.12 or newer." +# endif +# if defined(PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET) +# error "PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET needs the legacy type creation path" +# endif +#endif + #include #include #include diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 76bb3827a3..7e4b77ca84 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -79,18 +79,24 @@ # else # define PYBIND11_PRECOMPILED_CFG_TD 0 # endif +# if PYBIND11_TYPE_CREATION_VIA_SPEC +# define PYBIND11_PRECOMPILED_CFG_TS 1 +# else +# define PYBIND11_PRECOMPILED_CFG_TS 0 +# endif // PYBIND11_CONCAT does not macro-expand its arguments (## suppresses expansion). -# define PYBIND11_PRECOMPILED_CONFIG_NAME_(v, gd, sg, de, si, td) \ - pybind11_precompiled_config_v##v##_gd##gd##_sg##sg##_de##de##_si##si##_td##td -# define PYBIND11_PRECOMPILED_CONFIG_NAME(v, gd, sg, de, si, td) \ - PYBIND11_PRECOMPILED_CONFIG_NAME_(v, gd, sg, de, si, td) +# define PYBIND11_PRECOMPILED_CONFIG_NAME_(v, gd, sg, de, si, td, ts) \ + pybind11_precompiled_config_v##v##_gd##gd##_sg##sg##_de##de##_si##si##_td##td##_ts##ts +# define PYBIND11_PRECOMPILED_CONFIG_NAME(v, gd, sg, de, si, td, ts) \ + PYBIND11_PRECOMPILED_CONFIG_NAME_(v, gd, sg, de, si, td, ts) # define PYBIND11_PRECOMPILED_CONFIG_CHECK \ PYBIND11_PRECOMPILED_CONFIG_NAME(PYBIND11_INTERNALS_VERSION, \ PYBIND11_PRECOMPILED_CFG_GD, \ PYBIND11_PRECOMPILED_CFG_SG, \ PYBIND11_PRECOMPILED_CFG_DE, \ PYBIND11_PRECOMPILED_CFG_SI, \ - PYBIND11_PRECOMPILED_CFG_TD) + PYBIND11_PRECOMPILED_CFG_TD, \ + PYBIND11_PRECOMPILED_CFG_TS) # define PYBIND11_PRECOMPILED_CONFIG_GUARD \ ::pybind11::detail::PYBIND11_PRECOMPILED_CONFIG_CHECK(); #else diff --git a/tests/test_class.cpp b/tests/test_class.cpp index 47430d3a29..29663bcf9b 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -65,6 +65,20 @@ struct ConvertibleFromAnything { ConvertibleFromAnything(T &&) {} }; +template +struct NamedClass {}; + +template +py::class_> +bind_named_class(py::handle scope, const char *name, const Extra &...extra) { + return py::class_>(scope, name, extra...) + .def(py::init<>()) + .def( + "echo", + [](NamedClass &self) -> NamedClass & { return self; }, + py::return_value_policy::reference); +} + } // namespace test_class static_assert(py::detail::is_same_or_base_of::value, ""); @@ -522,6 +536,25 @@ TEST_SUBMODULE(class_, m) { base.def("g", [](NestBase &, Nested &) {}); base.def("h", []() { return NestBase(); }); + // test_class_name_spelling + auto names = m.def_submodule("class_names"); +#if defined(PYBIND11_TYPE_CREATION_VIA_SPEC) && PYBIND11_TYPE_CREATION_VIA_SPEC + names.attr("type_creation_via_spec") = true; +#else + names.attr("type_creation_via_spec") = false; +#endif + using test_class::bind_named_class; + bind_named_class<0>(names, "Simple"); + bind_named_class<1>(names, "a.b"); + bind_named_class<2>(names, "a.b.c"); + bind_named_class<3>(names, "a[b]"); + auto dotted = bind_named_class<4>(names, "a[b.c]"); + bind_named_class<5>(names, "a.b[c]"); + bind_named_class<6>(base, "a[b.c]"); + bind_named_class<7>(dotted, "Child"); + auto custom = names.def_submodule("custom"); + bind_named_class<8>(custom, "a[b.c]", py::custom_type_setup([](PyHeapTypeObject *) {})); + // test_error_after_conversion // The second-pass path through dispatcher() previously didn't // remember which overload was used, and would crash trying to diff --git a/tests/test_class.py b/tests/test_class.py index 81bf67e6f7..c504f63018 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -195,6 +195,46 @@ def test_qualname(doc): assert m.NestBase.Nested.__module__ == "pybind11_tests.class_" +@pytest.mark.parametrize( + ("placement", "name", "suffix", "legacy_qualname", "spec_qualname"), + [ + ("module", "Simple", "Simple", "Simple", "Simple"), + ("module", "a.b", "b", "a.b", "b"), + ("module", "a.b.c", "c", "a.b.c", "c"), + ("module", "a[b]", "a[b]", "a[b]", "a[b]"), + ("module", "a[b.c]", "c]", "a[b.c]", "c]"), + ("module", "a.b[c]", "b[c]", "a.b[c]", "b[c]"), + ("nested", "a[b.c]", "c]", "NestBase.a[b.c]", "NestBase.a[b.c]"), + ("child", "Child", "Child", "a[b.c].Child", "c].Child"), + ("custom", "a[b.c]", "c]", "a[b.c]", "a[b.c]"), + ], +) +def test_class_name_spelling(placement, name, suffix, legacy_qualname, spec_qualname): + """Characterize supplied names; spelling currently depends on the type-creation path.""" + scopes = { + "module": m.class_names, + "nested": m.NestBase, + "child": getattr(m.class_names, "a[b.c]"), + "custom": m.class_names.custom, + } + scope = scopes[placement] + cls = getattr(scope, name) + spec_path = m.class_names.type_creation_via_spec and placement != "custom" + truncated_name = ( + spec_path or env.GRAALPY or (env.PYPY and sys.version_info >= (3, 12)) + ) + expected_name = suffix if truncated_name else name + expected_qualname = spec_qualname if spec_path else legacy_qualname + expected_module = scope.__name__ if isinstance(scope, type(m)) else scope.__module__ + assert cls.__name__ == expected_name + assert cls.__qualname__ == expected_qualname + assert cls.__module__ == expected_module + assert (getattr(scope, cls.__name__, None) is cls) == (expected_name == name) + obj = cls() + assert obj.echo() is obj + assert f"{expected_module}.{expected_qualname}" in cls.echo.__doc__ + + def test_inheritance(msg): roger = m.Rabbit("Rabbit") assert roger.name() + " is a " + roger.species() == "Rabbit is a parrot" diff --git a/tests/test_custom_type_setup.cpp b/tests/test_custom_type_setup.cpp index 5235729e40..b262e38455 100644 --- a/tests/test_custom_type_setup.cpp +++ b/tests/test_custom_type_setup.cpp @@ -63,6 +63,17 @@ void add_gc_checkers_with_weakrefs(const py::object &obj) { } // namespace TEST_SUBMODULE(custom_type_setup, m) { + struct DynamicBase { + int value = 7; + }; + struct DynamicDerived : DynamicBase {}; + py::class_(m, "DynamicBase", py::dynamic_attr()) + .def(py::init<>()) + .def_readonly("value", &DynamicBase::value); + py::class_( + m, "DynamicDerived", py::custom_type_setup([](PyHeapTypeObject *) {})) + .def(py::init<>()); + py::class_ cls( m, "ContainerOwnsPythonObjects", diff --git a/tests/test_custom_type_setup.py b/tests/test_custom_type_setup.py index a200b02b5a..2216c768f6 100644 --- a/tests/test_custom_type_setup.py +++ b/tests/test_custom_type_setup.py @@ -35,6 +35,16 @@ def add_ref(obj): assert ref() is None +@pytest.mark.skipif("env.PYPY or env.GRAALPY", reason="Cannot reliably trigger GC") +def test_dynamic_base_custom_type_setup(gc_tester): + obj = m.DynamicDerived() + assert obj.value == 7 + obj.dynamic = 42 + assert obj.__dict__["dynamic"] == 42 + obj.cycle = obj + gc_tester(obj) + + # PyPy does not seem to reliably garbage collect. @pytest.mark.skipif("env.PYPY or env.GRAALPY") def test_self_cycle(gc_tester): diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index f46563cec3..ddfc00d22e 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -493,6 +493,20 @@ TEST_SUBMODULE(methods_and_attributes, m) { py::class_(m, "MetaclassOverride", py::metaclass((PyObject *) &PyType_Type)) .def_property_readonly_static("readonly", [](const py::object &) { return 1; }); +#if !defined(GRAALVM_PYTHON) + // test_metaclass_base_conflict: a derived class whose base has a metaclass that is neither a + // base nor a subtype of pybind11_type (rejected by PyType_FromMetaclass()). GraalPy cannot + // create a weak reference to a type whose metaclass was created with type(). + auto type_type = py::reinterpret_borrow((PyObject *) &PyType_Type); + py::object custom_meta = type_type("CustomMeta", py::make_tuple(type_type), py::dict()); + struct MetaclassConflictBase {}; + struct MetaclassConflictDerived : MetaclassConflictBase {}; + py::class_(m, "MetaclassConflictBase", py::metaclass(custom_meta)) + .def(py::init<>()); + py::class_(m, "MetaclassConflictDerived") + .def(py::init<>()); +#endif + // test_overload_ordering m.def("overload_order", [](const std::string &) { return 1; }); m.def("overload_order", [](const std::string &) { return 2; }); diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index c0943742da..99bbc8c2cd 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -239,6 +239,17 @@ def test_metaclass_override(): assert isinstance(m.MetaclassOverride.__dict__["readonly"], int) +@pytest.mark.skipif( + not hasattr(m, "MetaclassConflictBase"), reason="Not available on GraalPy" +) +def test_metaclass_base_conflict(): + """A base with an unrelated metaclass does not block binding a derived class""" + + assert type(m.MetaclassConflictBase).__name__ == "CustomMeta" + assert type(m.MetaclassConflictDerived).__name__ == "pybind11_type" + assert isinstance(m.MetaclassConflictDerived(), m.MetaclassConflictBase) + + def test_no_mixed_overloads(): from pybind11_tests import detailed_error_messages_enabled diff --git a/tools/check_inl_headers.py b/tools/check_inl_headers.py index 271f5e49df..aef0f0d3fe 100755 --- a/tools/check_inl_headers.py +++ b/tools/check_inl_headers.py @@ -45,6 +45,7 @@ "PYBIND11_HAS_SUBINTERPRETER_SUPPORT", "PYBIND11_INTERNALS_VERSION", "PYBIND11_SIMPLE_GIL_MANAGEMENT", + "PYBIND11_TYPE_CREATION_VIA_SPEC", "Py_GIL_DISABLED", }