diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 9b4f933e4e..98211ca552 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -33,7 +33,7 @@ jobs: nox -s build nox -s build_global - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: Packages path: dist/* @@ -44,7 +44,7 @@ jobs: needs: [build_wheel] runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: Packages path: dist diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index b7555a5a71..981884a955 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -72,13 +72,13 @@ jobs: run: twine check dist/* - name: Save standard package - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: standard path: dist/pybind11-* - name: Save global package - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: global path: dist/*global-* @@ -100,10 +100,10 @@ jobs: steps: # Downloads all to directories matching the artifact names - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@v3 + uses: actions/attest-build-provenance@v4 with: subject-path: "*/pybind11*" diff --git a/.github/workflows/tests-cibw.yml b/.github/workflows/tests-cibw.yml index 5dfb5dc940..bf534316af 100644 --- a/.github/workflows/tests-cibw.yml +++ b/.github/workflows/tests-cibw.yml @@ -70,24 +70,6 @@ jobs: if: contains(matrix.runs-on, 'macos') run: echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" - # Temporarily disable Android tests on ubuntu-latest due to emulator issues. - # See https://github.com/pybind/pybind11/pull/5914. - - name: "NOTE: Android tests are disabled on ubuntu-latest" - if: contains(matrix.runs-on, 'ubuntu') - run: | - echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" - echo '::warning::Android cibuildwheel tests are disabled on ubuntu-latest (CIBW_TEST_COMMAND is empty). See PR 5914.' - - # https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/ - - name: Enable KVM for Android emulator - if: contains(matrix.runs-on, 'ubuntu') - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - run: pipx install patchelf - - uses: pypa/cibuildwheel@v3.3 env: CIBW_PLATFORM: android diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 05626151c1..5ee5846b5f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,14 +25,14 @@ repos: # Clang format the codebase automatically - repo: https://github.com/pre-commit/mirrors-clang-format - rev: "v21.1.8" + rev: "v22.1.0" hooks: - id: clang-format types_or: [c++, c, cuda] # Ruff, the Python auto-correcting linter/formatter written in Rust - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.14 + rev: v0.15.4 hooks: - id: ruff-check args: ["--fix", "--show-fixes"] @@ -122,7 +122,7 @@ repos: # Use mirror because pre-commit autoupdate confuses tags in the upstream repo. # See https://github.com/crate-ci/typos/issues/390 - repo: https://github.com/adhtruong/mirrors-typos - rev: "v1.42.3" + rev: "v1.44.0" hooks: - id: typos args: [] @@ -144,14 +144,14 @@ repos: # PyLint has native support - not always usable, but works for us - repo: https://github.com/PyCQA/pylint - rev: "v4.0.4" + rev: "v4.0.5" hooks: - id: pylint files: ^pybind11 # Check schemas on some of our YAML files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.1 + rev: 0.37.0 hooks: - id: check-readthedocs - id: check-github-workflows diff --git a/docs/changelog.md b/docs/changelog.md index 209da97234..f993034f14 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -13,6 +13,62 @@ Changes will be added here periodically from the "Suggested changelog entry" block in pull request descriptions. +## Version 3.0.3 (March 31, 2026) + +Bug fixes: + +- Fixed TSS key exhaustion in `implicitly_convertible()` when many implicit conversions are registered across large module sets. + [#6020](https://github.com/pybind/pybind11/pull/6020) + +- Fixed heap-buffer-overflow in `pythonbuf` with undersized buffers by enforcing a minimum buffer size. + [#6019](https://github.com/pybind/pybind11/pull/6019) + +- Fixed virtual-inheritance pointer offset crashes when dispatching inherited methods through virtual bases. + [#6017](https://github.com/pybind/pybind11/pull/6017) + +- Fixed `free(): invalid pointer` crashes during interpreter shutdown with `py::enum_<>` by duplicating late-added `def_property_static` argument strings. + [#6015](https://github.com/pybind/pybind11/pull/6015) + +- Fixed `function_record` heap-type deallocation to call `PyObject_Free()` and decref the type. + [#6010](https://github.com/pybind/pybind11/pull/6010) + +- Hardened `PYBIND11_MODULE_PYINIT` and `get_internals()` against module-initialization crashes. + [#6018](https://github.com/pybind/pybind11/pull/6018) + +- Fixed `static_pointer_cast` build failure with virtual inheritance in `holder_caster_foreign_helpers.h`. + [#6014](https://github.com/pybind/pybind11/pull/6014) + +- Fixed ambiguous `factory` template specialization that caused compilation failures with nvcc + GCC 14. + [#6011](https://github.com/pybind/pybind11/pull/6011) + +- Fixed crash in `def_readwrite` for non-smart-holder properties of smart-holder classes. + [#6008](https://github.com/pybind/pybind11/pull/6008) + +- Fixed memory leak for `py::dynamic_attr()` objects on Python 3.13+ by clearing managed `__dict__` contents during deallocation. + [#5999](https://github.com/pybind/pybind11/pull/5999) + +- Fixed binding of `noexcept` and ref-qualified (`&`, `&&`) methods inherited from unregistered base classes. + [#5992](https://github.com/pybind/pybind11/pull/5992) + +Internal: + +- Moved `tomlkit` dependency to the dev dependency group. + [#5990](https://github.com/pybind/pybind11/pull/5990) + +- Switched to newer public CPython APIs (`PyType_GetFlags` and public vectorcall APIs where available). + [#6005](https://github.com/pybind/pybind11/pull/6005) + +Tests: + +- Made an async callback test deterministic by replacing fixed sleep with bounded waiting. + [#5986](https://github.com/pybind/pybind11/pull/5986) + +CI: + +- Re-enabled Android tests in the cibuildwheel workflow. + [#6001](https://github.com/pybind/pybind11/pull/6001) + + ## Version 3.0.2 (February 16, 2026) New Features: diff --git a/docs/requirements.txt b/docs/requirements.txt index 1b6bcf0c2f..c19fbf272b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -41,13 +41,13 @@ myst-parser==3.0.1 # via -r requirements.in packaging==24.0 # via sphinx -pygments==2.17.2 +pygments==2.20.0 # via # furo # sphinx pyyaml==6.0.2 # via myst-parser -requests==2.32.4 +requests==2.33.0 # via sphinx snowballstemmer==2.2.0 # via sphinx diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index b4486dc0f1..d337595a0b 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -378,7 +378,7 @@ struct type_record { #ifdef PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET dynamic_attr |= base_info->type->tp_dictoffset != 0; #else - dynamic_attr |= (base_info->type->tp_flags & Py_TPFLAGS_MANAGED_DICT) != 0; + dynamic_attr |= (PyType_GetFlags(base_info->type) & Py_TPFLAGS_MANAGED_DICT) != 0; #endif if (caster) { diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index f5a94da206..f07abfea3d 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -199,8 +199,7 @@ public: template >::value, \ - int> \ - = 0> \ + int> = 0> \ static ::pybind11::handle cast( \ T_ *src, ::pybind11::return_value_policy policy, ::pybind11::handle parent) { \ if (!src) \ @@ -1020,7 +1019,19 @@ struct copyable_holder_caster< return smart_holder_type_caster_support::smart_holder_from_shared_ptr( src, policy, parent, srcs.result); } - return type_caster_base::cast_holder(srcs, &src); + + auto *tinfo = srcs.result.tinfo; + if (tinfo != nullptr && tinfo->holder_enum_v == holder_enum_t::std_shared_ptr) { + return type_caster_base::cast_holder(srcs, &src); + } + + if (parent) { + return type_caster_base::cast( + srcs, return_value_policy::reference_internal, parent); + } + + throw cast_error("Unable to convert std::shared_ptr to Python when the bound type " + "does not use std::shared_ptr or py::smart_holder as its holder type"); } // This function will succeed even if the `responsible_parent` does not own the @@ -1662,8 +1673,7 @@ PYBIND11_NAMESPACE_END(detail) template ::value && !detail::is_same_ignoring_cvref::value, - int> - = 0> + int> = 0> T cast(const handle &handle) { using namespace detail; constexpr bool is_enum_cast = type_uses_type_caster_enum_type>::value; @@ -1698,8 +1708,7 @@ template ::value && detail::is_same_ignoring_cvref::value, - int> - = 0> + int> = 0> T cast(Handle &&handle) { return handle.inc_ref().ptr(); } @@ -1708,8 +1717,7 @@ template ::value && detail::is_same_ignoring_cvref::value, - int> - = 0> + int> = 0> T cast(Object &&obj) { return obj.release().ptr(); } @@ -2243,8 +2251,13 @@ class unpacking_collector { if (m_names) { nargs -= m_names.size(); } - PyObject *result = _PyObject_Vectorcall( - ptr, m_args.data() + 1, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, m_names.ptr()); + PyObject *result = +#if PY_VERSION_HEX >= 0x03090000 + PyObject_Vectorcall( +#else + _PyObject_Vectorcall( +#endif + ptr, m_args.data() + 1, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, m_names.ptr()); if (!result) { throw error_already_set(); } diff --git a/include/pybind11/chrono.h b/include/pybind11/chrono.h index d9bcc4bc40..668e458e99 100644 --- a/include/pybind11/chrono.h +++ b/include/pybind11/chrono.h @@ -64,8 +64,7 @@ class duration_caster { return src; } static const std::chrono::duration & - get_duration(const std::chrono::duration &&) - = delete; + get_duration(const std::chrono::duration &&) = delete; // If this is a time_point get the time_since_epoch template diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 480c369aa6..4b7422eee2 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -503,6 +503,17 @@ extern "C" inline void pybind11_object_dealloc(PyObject *self) { PyObject_GC_UnTrack(self); } +#if PY_VERSION_HEX >= 0x030D0000 + // PyObject_ClearManagedDict() is available from Python 3.13+. It must be + // called before tp_free() because on Python 3.14+ tp_free no longer + // implicitly clears the managed dict, which would abandon the refcounts of + // objects stored in __dict__ of py::dynamic_attr() types, causing permanent + // memory leaks. + if (PyType_HasFeature(type, Py_TPFLAGS_MANAGED_DICT)) { + PyObject_ClearManagedDict(self); + } +#endif + clear_instance(self); type->tp_free(self); diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index b03d0c25d3..7158923084 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -19,7 +19,7 @@ /* -- start version constants -- */ #define PYBIND11_VERSION_MAJOR 3 #define PYBIND11_VERSION_MINOR 0 -#define PYBIND11_VERSION_MICRO 2 +#define PYBIND11_VERSION_MICRO 3 // ALPHA = 0xA, BETA = 0xB, GAMMA = 0xC (release candidate), FINAL = 0xF (stable release) // - The release level is set to "alpha" for development versions. // Use 0xA0 (LEVEL=0xA, SERIAL=0) for development versions. @@ -27,7 +27,7 @@ #define PYBIND11_VERSION_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL #define PYBIND11_VERSION_RELEASE_SERIAL 0 // String version of (micro, release level, release serial), e.g.: 0a0, 0b1, 0rc1, 0 -#define PYBIND11_VERSION_PATCH 2 +#define PYBIND11_VERSION_PATCH 3 /* -- end version constants -- */ #if !defined(Py_PACK_FULL_VERSION) @@ -167,6 +167,14 @@ # define PYBIND11_NOINLINE __attribute__((noinline)) inline #endif +#if defined(_MSC_VER) +# define PYBIND11_ALWAYS_INLINE __forceinline +#elif defined(__GNUC__) +# define PYBIND11_ALWAYS_INLINE __attribute__((__always_inline__)) inline +#else +# define PYBIND11_ALWAYS_INLINE inline +#endif + #if defined(__MINGW32__) // For unknown reasons all PYBIND11_DEPRECATED member trigger a warning when declared // whether it is used or not @@ -449,19 +457,24 @@ PyModuleDef_Init should be treated like any other PyObject (so not shared across static int PYBIND11_CONCAT(pybind11_exec_, name)(PyObject *); \ PYBIND11_PLUGIN_IMPL(name) { \ PYBIND11_CHECK_PYTHON_VERSION \ - pybind11::detail::ensure_internals(); \ - static ::pybind11::detail::slots_array mod_def_slots = ::pybind11::detail::init_slots( \ - &PYBIND11_CONCAT(pybind11_exec_, name), ##__VA_ARGS__); \ - static PyModuleDef def{/* m_base */ PyModuleDef_HEAD_INIT, \ - /* m_name */ PYBIND11_TOSTRING(name), \ - /* m_doc */ nullptr, \ - /* m_size */ 0, \ - /* m_methods */ nullptr, \ - /* m_slots */ mod_def_slots.data(), \ - /* m_traverse */ nullptr, \ - /* m_clear */ nullptr, \ - /* m_free */ nullptr}; \ - return PyModuleDef_Init(&def); \ + try { \ + pybind11::detail::ensure_internals(); \ + static ::pybind11::detail::slots_array mod_def_slots \ + = ::pybind11::detail::init_slots(&PYBIND11_CONCAT(pybind11_exec_, name), \ + ##__VA_ARGS__); \ + static PyModuleDef def{/* m_base */ PyModuleDef_HEAD_INIT, \ + /* m_name */ PYBIND11_TOSTRING(name), \ + /* m_doc */ nullptr, \ + /* m_size */ 0, \ + /* m_methods */ nullptr, \ + /* m_slots */ mod_def_slots.data(), \ + /* m_traverse */ nullptr, \ + /* m_clear */ nullptr, \ + /* m_free */ nullptr}; \ + return PyModuleDef_Init(&def); \ + } \ + PYBIND11_CATCH_INIT_EXCEPTIONS \ + return nullptr; \ } #define PYBIND11_MODULE_EXEC(name, variable) \ @@ -1026,6 +1039,17 @@ struct is_instantiation> : std::true_type {}; template using is_shared_ptr = is_instantiation; +/// Detects whether static_cast(Base*) is valid, i.e. the inheritance is non-virtual. +/// Used to detect virtual bases: if this is false, pointer adjustments require the implicit_casts +/// chain rather than reinterpret_cast. +template +struct is_static_downcastable : std::false_type {}; +template +struct is_static_downcastable(std::declval()))>> + : std::true_type {}; + /// Check if T looks like an input iterator template struct is_input_iterator : std::false_type {}; @@ -1048,14 +1072,30 @@ struct strip_function_object { using type = typename remove_class::type; }; +// Strip noexcept from a free function type (C++17: noexcept is part of the type). +template +struct remove_noexcept { + using type = T; +}; +#ifdef __cpp_noexcept_function_type +template +struct remove_noexcept { + using type = R(A...); +}; +#endif +template +using remove_noexcept_t = typename remove_noexcept::type; + // Extracts the function signature from a function, function pointer or lambda. +// Strips noexcept from the result so that factory/pickle_factory partial specializations +// (which match plain Return(Args...)) work correctly with noexcept callables (issue #2234). template > -using function_signature_t = conditional_t< +using function_signature_t = remove_noexcept_t::value, F, typename conditional_t::value || std::is_member_pointer::value, std::remove_pointer, - strip_function_object>::type>; + strip_function_object>::type>>; /// Returns true if the type looks like a lambda: that is, isn't a function, pointer or member /// pointer. Note that this can catch all sorts of other things, too; this is intended to be used @@ -1204,6 +1244,36 @@ struct overload_cast_impl { -> decltype(pmf) { return pmf; } + + // Define const/non-const member-pointer selector pairs for qualifier combinations. + // The `qualifiers` parameter is used in type position, where extra parentheses are invalid. + // NOLINTBEGIN(bugprone-macro-parentheses) +#define PYBIND11_OVERLOAD_CAST_MEMBER_PTR(qualifiers) \ + template \ + constexpr auto operator()(Return (Class::*pmf)(Args...) qualifiers, std::false_type = {}) \ + const noexcept -> decltype(pmf) { \ + return pmf; \ + } \ + template \ + constexpr auto operator()(Return (Class::*pmf)(Args...) const qualifiers, std::true_type) \ + const noexcept -> decltype(pmf) { \ + return pmf; \ + } + PYBIND11_OVERLOAD_CAST_MEMBER_PTR(&) + PYBIND11_OVERLOAD_CAST_MEMBER_PTR(&&) + +#ifdef __cpp_noexcept_function_type + template + constexpr auto operator()(Return (*pf)(Args...) noexcept) const noexcept -> decltype(pf) { + return pf; + } + + PYBIND11_OVERLOAD_CAST_MEMBER_PTR(noexcept) + PYBIND11_OVERLOAD_CAST_MEMBER_PTR(& noexcept) + PYBIND11_OVERLOAD_CAST_MEMBER_PTR(&& noexcept) +#endif +#undef PYBIND11_OVERLOAD_CAST_MEMBER_PTR + // NOLINTEND(bugprone-macro-parentheses) }; PYBIND11_NAMESPACE_END(detail) diff --git a/include/pybind11/detail/holder_caster_foreign_helpers.h b/include/pybind11/detail/holder_caster_foreign_helpers.h index f636618e9f..cae571b65c 100644 --- a/include/pybind11/detail/holder_caster_foreign_helpers.h +++ b/include/pybind11/detail/holder_caster_foreign_helpers.h @@ -31,13 +31,31 @@ struct holder_caster_foreign_helpers { PyObject *o; }; + // Downcast shared_ptr from the enable_shared_from_this base to the target type. + // SFINAE probe: use static_pointer_cast when the static downcast is valid (common case), + // fall back to dynamic_pointer_cast when it isn't (virtual inheritance — issue #5989). + // We can't use dynamic_pointer_cast unconditionally because it requires polymorphic types; + // we can't use is_polymorphic to choose because that's orthogonal to virtual inheritance. + // (The implementation uses the "tag dispatch via overload priority" trick.) + template + static auto esft_downcast(const std::shared_ptr &existing, int /*preferred*/) + -> decltype(static_cast(std::declval()), std::shared_ptr()) { + return std::static_pointer_cast(existing); + } + + template + static std::shared_ptr esft_downcast(const std::shared_ptr &existing, + ... /*fallback*/) { + return std::dynamic_pointer_cast(existing); + } + template static auto set_via_shared_from_this(type *value, std::shared_ptr *holder_out) -> decltype(value->shared_from_this(), bool()) { // object derives from enable_shared_from_this; // try to reuse an existing shared_ptr if one is known if (auto existing = try_get_shared_from_this(value)) { - *holder_out = std::static_pointer_cast(existing); + *holder_out = esft_downcast(existing, 0); return true; } return false; diff --git a/include/pybind11/detail/init.h b/include/pybind11/detail/init.h index b7f8d5a52c..a1083f8457 100644 --- a/include/pybind11/detail/init.h +++ b/include/pybind11/detail/init.h @@ -304,11 +304,10 @@ struct constructor { extra...); } - template < - typename Class, - typename... Extra, - enable_if_t, Args...>::value, int> - = 0> + template , Args...>::value, + int> = 0> static void execute(Class &cl, const Extra &...extra) { cl.def( "__init__", @@ -325,11 +324,10 @@ struct constructor { extra...); } - template < - typename Class, - typename... Extra, - enable_if_t, Args...>::value, int> - = 0> + template , Args...>::value, + int> = 0> static void execute(Class &cl, const Extra &...extra) { cl.def( "__init__", @@ -345,11 +343,10 @@ struct constructor { // Implementing class for py::init_alias<...>() template struct alias_constructor { - template < - typename Class, - typename... Extra, - enable_if_t, Args...>::value, int> - = 0> + template , Args...>::value, + int> = 0> static void execute(Class &cl, const Extra &...extra) { cl.def( "__init__", @@ -370,8 +367,15 @@ template ` = `void_type()`, which the dual-factory +// specialization can also decompose as `AReturn(AArgs...)` with `AReturn=void_type` +// and `AArgs={}`. template -struct factory { +struct factory { remove_reference_t class_factory; // NOLINTNEXTLINE(google-explicit-constructor) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index b681529325..a49cdaa47c 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -59,15 +59,13 @@ using ExceptionTranslator = void (*)(std::exception_ptr); # define PYBIND11_TLS_KEY_INIT(var) \ _Pragma("clang diagnostic push") /**/ \ _Pragma("clang diagnostic ignored \"-Wmissing-field-initializers\"") /**/ \ - Py_tss_t var \ - = Py_tss_NEEDS_INIT; \ + Py_tss_t var = Py_tss_NEEDS_INIT; \ _Pragma("clang diagnostic pop") #elif defined(__GNUC__) && !defined(__INTEL_COMPILER) # define PYBIND11_TLS_KEY_INIT(var) \ _Pragma("GCC diagnostic push") /**/ \ _Pragma("GCC diagnostic ignored \"-Wmissing-field-initializers\"") /**/ \ - Py_tss_t var \ - = Py_tss_NEEDS_INIT; \ + Py_tss_t var = Py_tss_NEEDS_INIT; \ _Pragma("GCC diagnostic pop") #else # define PYBIND11_TLS_KEY_INIT(var) Py_tss_t var = Py_tss_NEEDS_INIT; @@ -864,7 +862,11 @@ inline internals_pp_manager &get_internals_pp_manager() { /// Return a reference to the current `internals` data PYBIND11_NOINLINE internals &get_internals() { auto &ppmgr = get_internals_pp_manager(); - auto &internals_ptr = *ppmgr.get_pp(); + auto *pp = ppmgr.get_pp(); + if (!pp) { + pybind11_fail("get_internals: get_pp() returned nullptr"); + } + auto &internals_ptr = *pp; if (!internals_ptr) { // Slow path, something needs fetched from the state dict or created gil_scoped_acquire_simple gil; @@ -872,6 +874,9 @@ PYBIND11_NOINLINE internals &get_internals() { ppmgr.create_pp_content_once(&internals_ptr); + if (!internals_ptr) { + pybind11_fail("get_internals: create_pp_content_once() produced nullptr"); + } if (!internals_ptr->instance_base) { // This calls get_internals, so cannot be called from within the internals constructor // called above because internals_ptr must be set before get_internals is called again diff --git a/include/pybind11/iostream.h b/include/pybind11/iostream.h index 1878089e31..44261e881e 100644 --- a/include/pybind11/iostream.h +++ b/include/pybind11/iostream.h @@ -117,8 +117,16 @@ class pythonbuf : public std::streambuf { int sync() override { return _sync(); } public: + // Minimum buffer size must accommodate the largest incomplete UTF-8 sequence + // (3 bytes) plus one position reserved for overflow(), i.e. 4 bytes total. + static constexpr size_t minimum_buffer_size = 4; + explicit pythonbuf(const object &pyostream, size_t buffer_size = 1024) - : buf_size(buffer_size), d_buffer(new char[buf_size]), pywrite(pyostream.attr("write")), + : buf_size(buffer_size < minimum_buffer_size // ternary avoids C++14 std::max ODR-use of + // static constexpr + ? minimum_buffer_size + : buffer_size), + d_buffer(new char[buf_size]), pywrite(pyostream.attr("write")), pyflush(pyostream.attr("flush")) { setp(d_buffer.get(), d_buffer.get() + buf_size - 1); } diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index 408d1699cf..10c0c9446c 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -2327,4 +2327,86 @@ Helper vectorize(Return (Class::*f)(Args...) const) { return Helper(std::mem_fn(f)); } +// Intentionally no &&/const&& overloads: vectorized method calls operate on the bound Python +// instance and should not consume/move-from self. +// Vectorize a class method (non-const, lvalue ref-qualified): +template ())), + Return, + Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) &) { + return Helper(std::mem_fn(f)); +} + +// Vectorize a class method (const, lvalue ref-qualified): +template ())), + Return, + const Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) const &) { + return Helper(std::mem_fn(f)); +} + +#ifdef __cpp_noexcept_function_type +// Vectorize a class method (non-const, noexcept): +template ())), + Return, + Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) noexcept) { + return Helper(std::mem_fn(f)); +} + +// Vectorize a class method (const, noexcept): +template ())), + Return, + const Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) const noexcept) { + return Helper(std::mem_fn(f)); +} + +// Vectorize a class method (non-const, lvalue ref-qualified, noexcept): +template ())), + Return, + Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) & noexcept) { + return Helper(std::mem_fn(f)); +} + +// Vectorize a class method (const, lvalue ref-qualified, noexcept): +template ())), + Return, + const Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) const & noexcept) { + return Helper(std::mem_fn(f)); +} +#endif + PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 0f31262c47..b5a97db866 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -369,6 +369,96 @@ class cpp_function : public function { extra...); } + /// Construct a cpp_function from a class method (non-const, rvalue ref-qualifier) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) &&, const Extra &...extra) { + initialize( + [f](Class *c, Arg... args) -> Return { + return (std::move(*c).*f)(std::forward(args)...); + }, + (Return (*)(Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (const, rvalue ref-qualifier) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) const &&, const Extra &...extra) { + initialize( + [f](const Class *c, Arg... args) -> Return { + return (std::move(*c).*f)(std::forward(args)...); + }, + (Return (*)(const Class *, Arg...)) nullptr, + extra...); + } + +#ifdef __cpp_noexcept_function_type + /// Construct a cpp_function from a class method (non-const, no ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) noexcept, const Extra &...extra) { + initialize( + [f](Class *c, Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (non-const, lvalue ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) & noexcept, const Extra &...extra) { + initialize( + [f](Class *c, Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (const, no ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) const noexcept, const Extra &...extra) { + initialize([f](const Class *c, + Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(const Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (const, lvalue ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) const & noexcept, const Extra &...extra) { + initialize([f](const Class *c, + Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(const Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (non-const, rvalue ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) && noexcept, const Extra &...extra) { + initialize( + [f](Class *c, Arg... args) -> Return { + return (std::move(*c).*f)(std::forward(args)...); + }, + (Return (*)(Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (const, rvalue ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) const && noexcept, const Extra &...extra) { + initialize( + [f](const Class *c, Arg... args) -> Return { + return (std::move(*c).*f)(std::forward(args)...); + }, + (Return (*)(const Class *, Arg...)) nullptr, + extra...); + } +#endif + /// Return the function name object name() const { return attr("__name__"); } @@ -1321,9 +1411,15 @@ PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods) // This implementation needs the definition of `class cpp_function`. inline void tp_dealloc_impl(PyObject *self) { + // Save type before PyObject_Free invalidates self. + auto *type = Py_TYPE(self); auto *py_func_rec = reinterpret_cast(self); cpp_function::destruct(py_func_rec->cpp_func_rec); py_func_rec->cpp_func_rec = nullptr; + // PyObject_New increments the heap type refcount and allocates via + // PyObject_Malloc; balance both here + PyObject_Free(self); + Py_DECREF(type); } PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods) @@ -1859,29 +1955,89 @@ inline void add_class_method(object &cls, const char *name_, const cpp_function } } -PYBIND11_NAMESPACE_END(detail) - -/// Given a pointer to a member function, cast it to its `Derived` version. -/// Forward everything else unchanged. -template -auto method_adaptor(F &&f) -> decltype(std::forward(f)) { - return std::forward(f); -} - +/// Type trait to rebind a member function pointer's class to `Derived`, preserving all +/// cv/ref/noexcept qualifiers. The primary template has no `type` member, providing SFINAE +/// failure for unsupported member function pointer types. `source_class` holds the original +/// class for use in `is_accessible_base_of` checks. +template +struct rebind_member_ptr {}; + +// Define one specialization per supported qualifier combination via a local macro. +// The qualifiers argument appears in type position, not expression position, so +// parenthesizing it would produce invalid C++. +// The no-qualifier specialization is written out explicitly to avoid invoking the macro with an +// empty argument, which triggers MSVC warning C4003. template -auto method_adaptor(Return (Class::*pmf)(Args...)) -> Return (Derived::*)(Args...) { +struct rebind_member_ptr { + using type = Return (Derived::*)(Args...); + using source_class = Class; +}; +// NOLINTBEGIN(bugprone-macro-parentheses) +#define PYBIND11_REBIND_MEMBER_PTR(qualifiers) \ + template \ + struct rebind_member_ptr { \ + using type = Return (Derived::*)(Args...) qualifiers; \ + using source_class = Class; \ + } +PYBIND11_REBIND_MEMBER_PTR(const); +PYBIND11_REBIND_MEMBER_PTR(&); +PYBIND11_REBIND_MEMBER_PTR(const &); +PYBIND11_REBIND_MEMBER_PTR(&&); +PYBIND11_REBIND_MEMBER_PTR(const &&); +#ifdef __cpp_noexcept_function_type +PYBIND11_REBIND_MEMBER_PTR(noexcept); +PYBIND11_REBIND_MEMBER_PTR(const noexcept); +PYBIND11_REBIND_MEMBER_PTR(& noexcept); +PYBIND11_REBIND_MEMBER_PTR(const & noexcept); +PYBIND11_REBIND_MEMBER_PTR(&& noexcept); +PYBIND11_REBIND_MEMBER_PTR(const && noexcept); +#endif +#undef PYBIND11_REBIND_MEMBER_PTR +// NOLINTEND(bugprone-macro-parentheses) + +/// Shared implementation body for all method_adaptor member-function-pointer overloads. +/// Asserts Base is accessible from Derived, then casts the member pointer. +template , + typename Adapted = typename Traits::type> +constexpr PYBIND11_ALWAYS_INLINE Adapted adapt_member_ptr(T pmf) { static_assert( - detail::is_accessible_base_of::value, + detail::is_accessible_base_of::value, "Cannot bind an inaccessible base class method; use a lambda definition instead"); return pmf; } -template -auto method_adaptor(Return (Class::*pmf)(Args...) const) -> Return (Derived::*)(Args...) const { - static_assert( - detail::is_accessible_base_of::value, - "Cannot bind an inaccessible base class method; use a lambda definition instead"); - return pmf; +PYBIND11_NAMESPACE_END(detail) + +/// Given a pointer to a member function, cast it to its `Derived` version. +/// For all other callables (lambdas, function pointers, etc.), forward unchanged. +/// +/// Two overloads cover all cases without explicit per-qualifier instantiations: +/// +/// (1) Generic fallback — disabled for member function pointers so that (2) wins +/// without any partial-ordering ambiguity. +/// (2) MFP overload — SFINAE on rebind_member_ptr::type, which exists for every +/// supported qualifier combination (const, &, &&, noexcept, ...). A single +/// template therefore covers all combinations that rebind_member_ptr handles. +template < + typename /*Derived*/, + typename F, + detail::enable_if_t>::value, + int> = 0> +constexpr auto method_adaptor(F &&f) -> decltype(std::forward(f)) { + return std::forward(f); +} + +template ::type> +constexpr Adapted method_adaptor(T pmf) { + // Expected to be redundant (SFINAE on rebind_member_ptr) but cheap and makes the intent + // explicit. + static_assert(std::is_member_function_pointer::value, + "method_adaptor: T must be a member function pointer"); + return detail::adapt_member_ptr(pmf); } PYBIND11_NAMESPACE_BEGIN(detail) @@ -2239,6 +2395,13 @@ class class_ : public detail::generic_type { rec.add_base(typeid(Base), [](void *src) -> void * { return static_cast(reinterpret_cast(src)); }); + // Virtual inheritance means the base subobject is at a dynamic offset, + // so the reinterpret_cast shortcut in load_impl Case 2a is invalid. + // Force the MI path (implicit_casts) for correct pointer adjustment. + // Detection: static_cast(Base*) is ill-formed for virtual bases. + if PYBIND11_MAYBE_CONSTEXPR (!detail::is_static_downcastable::value) { + rec.multiple_inheritance = true; + } } template ::value, int> = 0> @@ -2340,6 +2503,40 @@ class class_ : public detail::generic_type { return def_buffer([func](const type &obj) { return (obj.*func)(); }); } + // Intentionally no &&/const&& overloads: buffer protocol callbacks are invoked on an + // existing Python object and should not move-from self. + template + class_ &def_buffer(Return (Class::*func)(Args...) &) { + return def_buffer([func](type &obj) { return (obj.*func)(); }); + } + + template + class_ &def_buffer(Return (Class::*func)(Args...) const &) { + return def_buffer([func](const type &obj) { return (obj.*func)(); }); + } + +#ifdef __cpp_noexcept_function_type + template + class_ &def_buffer(Return (Class::*func)(Args...) noexcept) { + return def_buffer([func](type &obj) { return (obj.*func)(); }); + } + + template + class_ &def_buffer(Return (Class::*func)(Args...) const noexcept) { + return def_buffer([func](const type &obj) { return (obj.*func)(); }); + } + + template + class_ &def_buffer(Return (Class::*func)(Args...) & noexcept) { + return def_buffer([func](type &obj) { return (obj.*func)(); }); + } + + template + class_ &def_buffer(Return (Class::*func)(Args...) const & noexcept) { + return def_buffer([func](const type &obj) { return (obj.*func)(); }); + } +#endif + template class_ &def_readwrite(const char *name, D C::*pm, const Extra &...extra) { static_assert(std::is_same::value || std::is_base_of::value, @@ -2467,19 +2664,41 @@ class class_ : public detail::generic_type { if (rec_fget) { char *doc_prev = rec_fget->doc; /* 'extra' field may include a property-specific documentation string */ + auto args_before = rec_fget->args.size(); detail::process_attributes::init(extra..., rec_fget); if (rec_fget->doc && rec_fget->doc != doc_prev) { std::free(doc_prev); rec_fget->doc = PYBIND11_COMPAT_STRDUP(rec_fget->doc); } + // Args added by process_attributes (e.g. "self" via is_method + pos_only/kw_only) + // need their strings strdup'd: initialize_generic's strdup loop already ran during + // cpp_function construction, so it won't process these late additions. Without this, + // destruct() would call free() on string literals. See gh-5976. + for (auto i = args_before; i < rec_fget->args.size(); ++i) { + if (rec_fget->args[i].name) { + rec_fget->args[i].name = PYBIND11_COMPAT_STRDUP(rec_fget->args[i].name); + } + if (rec_fget->args[i].descr) { + rec_fget->args[i].descr = PYBIND11_COMPAT_STRDUP(rec_fget->args[i].descr); + } + } } if (rec_fset) { char *doc_prev = rec_fset->doc; + auto args_before = rec_fset->args.size(); detail::process_attributes::init(extra..., rec_fset); if (rec_fset->doc && rec_fset->doc != doc_prev) { std::free(doc_prev); rec_fset->doc = PYBIND11_COMPAT_STRDUP(rec_fset->doc); } + for (auto i = args_before; i < rec_fset->args.size(); ++i) { + if (rec_fset->args[i].name) { + rec_fset->args[i].name = PYBIND11_COMPAT_STRDUP(rec_fset->args[i].name); + } + if (rec_fset->args[i].descr) { + rec_fset->args[i].descr = PYBIND11_COMPAT_STRDUP(rec_fset->args[i].descr); + } + } if (!rec_active) { rec_active = rec_fset; } @@ -3299,13 +3518,10 @@ typing::Iterator make_value_iterator(Type &value, Extra &&...extra) { template void implicitly_convertible() { - static int tss_sentinel_pointee = 1; // arbitrary value struct set_flag { - thread_specific_storage &flag; - explicit set_flag(thread_specific_storage &flag_) : flag(flag_) { - flag = &tss_sentinel_pointee; // trick: the pointer itself is the sentinel - } - ~set_flag() { flag.reset(nullptr); } + bool &flag; + explicit set_flag(bool &flag_) : flag(flag_) { flag_ = true; } + ~set_flag() { flag = false; } // Prevent copying/moving to ensure RAII guard is used safely set_flag(const set_flag &) = delete; @@ -3314,7 +3530,7 @@ void implicitly_convertible() { set_flag &operator=(set_flag &&) = delete; }; auto implicit_caster = [](PyObject *obj, PyTypeObject *type) -> PyObject * { - static thread_specific_storage currently_used; + thread_local bool currently_used = false; if (currently_used) { // implicit conversions are non-reentrant return nullptr; } diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 30eae090f5..ff4be6a500 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -259,8 +259,7 @@ class handle : public detail::object_api { detail::enable_if_t, detail::is_pyobj_ptr_or_nullptr_t>, std::is_convertible>::value, - int> - = 0> + int> = 0> // NOLINTNEXTLINE(google-explicit-constructor) handle(T &obj) : m_ptr(obj) {} @@ -1694,8 +1693,7 @@ class str : public object { template >::value && std::is_constructible::value, - int> - = 0> + int> = 0> explicit str(T &&h) : object(raw_str(handle(std::forward(h)).ptr()), stolen_t{}) { if (!m_ptr) { throw error_already_set(); diff --git a/pyproject.toml b/pyproject.toml index 7a12eed1ac..6a300985ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,9 +57,11 @@ pybind11 = "pybind11.share.pkgconfig" test = [ "pytest", "build", +] +dev = [ "tomlkit", + { include-group = "test" } ] -dev = [{ include-group = "test" }] [tool.scikit-build] diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9a35052daa..e87b1e93b3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -172,6 +172,7 @@ set(PYBIND11_TEST_FILES test_scoped_critical_section test_sequences_and_iterators test_smart_ptr + test_standalone_enum_module.py test_stl test_stl_binders test_tagbased_polymorphic @@ -249,6 +250,7 @@ tests_extra_targets("test_exceptions.py" "cross_module_interleaved_error_already tests_extra_targets("test_gil_scoped.py" "cross_module_gil_utils") tests_extra_targets("test_cpp_conduit.py" "exo_planet_pybind11;exo_planet_c_api;home_planet_very_lonely_traveler") +tests_extra_targets("test_standalone_enum_module.py" "standalone_enum_module") set(PYBIND11_EIGEN_REPO "https://gitlab.com/libeigen/eigen.git" diff --git a/tests/env.py b/tests/env.py index 790a0108fc..12acde7b3a 100644 --- a/tests/env.py +++ b/tests/env.py @@ -4,6 +4,8 @@ import sys import sysconfig +import pytest + ANDROID = sys.platform.startswith("android") IOS = sys.platform.startswith("ios") LINUX = sys.platform.startswith("linux") @@ -50,6 +52,9 @@ def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None: import sys import textwrap + if ANDROID or IOS or sys.platform.startswith("emscripten"): + pytest.skip("Requires subprocess support") + code = textwrap.dedent(code).strip() try: for _ in range(rerun): # run flakily failing test multiple times diff --git a/tests/pybind11_tests.cpp b/tests/pybind11_tests.cpp index 4317925737..5dacd7aeb6 100644 --- a/tests/pybind11_tests.cpp +++ b/tests/pybind11_tests.cpp @@ -96,6 +96,12 @@ PYBIND11_MODULE(pybind11_tests, m, py::mod_gil_not_used()) { #else false; #endif + m.attr("PYBIND11_TEST_SMART_HOLDER") = +#if defined(PYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE) + true; +#else + false; +#endif bind_ConstructorStats(m); @@ -105,6 +111,12 @@ PYBIND11_MODULE(pybind11_tests, m, py::mod_gil_not_used()) { m.attr("detailed_error_messages_enabled") = false; #endif +#if defined(__cpp_noexcept_function_type) + m.attr("defined___cpp_noexcept_function_type") = true; +#else + m.attr("defined___cpp_noexcept_function_type") = false; +#endif + py::class_(m, "UserType", "A `py::class_` type for testing") .def(py::init<>()) .def(py::init()) diff --git a/tests/standalone_enum_module.cpp b/tests/standalone_enum_module.cpp new file mode 100644 index 0000000000..7e917b974e --- /dev/null +++ b/tests/standalone_enum_module.cpp @@ -0,0 +1,13 @@ +// Copyright (c) 2026 The pybind Community. + +#include + +namespace standalone_enum_module_ns { +enum SomeEnum {}; +} // namespace standalone_enum_module_ns + +using namespace standalone_enum_module_ns; + +PYBIND11_MODULE(standalone_enum_module, m) { // Added in PR #6015 + pybind11::enum_ some_enum_wrapper(m, "SomeEnum"); +} diff --git a/tests/test_buffers.cpp b/tests/test_buffers.cpp index 525be80bc0..5ac4553a53 100644 --- a/tests/test_buffers.cpp +++ b/tests/test_buffers.cpp @@ -439,4 +439,133 @@ TEST_SUBMODULE(buffers, m) { PyBuffer_Release(&buffer); return result; }); + + // test_noexcept_def_buffer (issue #2234) + // def_buffer(Return (Class::*)(Args...) noexcept) and + // def_buffer(Return (Class::*)(Args...) const noexcept) must compile and work correctly. + struct OneDBuffer { + // Declare m_data before m_n to match initialiser list order below. + float *m_data; + py::ssize_t m_n; + explicit OneDBuffer(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBuffer() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) noexcept) + py::buffer_info get_buffer() noexcept { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}); + } + }; + + // non-const noexcept member function form + py::class_(m, "OneDBuffer", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBuffer::get_buffer); + + // const noexcept member function form (separate class to avoid ambiguity) + struct OneDBufferConst { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferConst(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferConst() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) const noexcept) + py::buffer_info get_buffer() const noexcept { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}, + /*readonly=*/true); + } + }; + py::class_(m, "OneDBufferConst", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferConst::get_buffer); + + // test_ref_qualified_def_buffer + struct OneDBufferLRef { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferLRef(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferLRef() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) &) + py::buffer_info get_buffer() & { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}); + } + }; + py::class_(m, "OneDBufferLRef", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferLRef::get_buffer); + + struct OneDBufferConstLRef { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferConstLRef(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferConstLRef() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) const &) + py::buffer_info get_buffer() const & { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}, + /*readonly=*/true); + } + }; + py::class_(m, "OneDBufferConstLRef", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferConstLRef::get_buffer); + +#ifdef __cpp_noexcept_function_type + struct OneDBufferLRefNoexcept { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferLRefNoexcept(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferLRefNoexcept() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) & noexcept) + py::buffer_info get_buffer() & noexcept { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}); + } + }; + py::class_(m, "OneDBufferLRefNoexcept", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferLRefNoexcept::get_buffer); + + struct OneDBufferConstLRefNoexcept { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferConstLRefNoexcept(py::ssize_t n) + : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferConstLRefNoexcept() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) const & noexcept) + py::buffer_info get_buffer() const & noexcept { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}, + /*readonly=*/true); + } + }; + py::class_( + m, "OneDBufferConstLRefNoexcept", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferConstLRefNoexcept::get_buffer); +#endif } diff --git a/tests/test_buffers.py b/tests/test_buffers.py index f666df5bad..c1d5770ab0 100644 --- a/tests/test_buffers.py +++ b/tests/test_buffers.py @@ -7,7 +7,7 @@ import pytest import env -from pybind11_tests import ConstructorStats +from pybind11_tests import ConstructorStats, defined___cpp_noexcept_function_type from pybind11_tests import buffers as m np = pytest.importorskip("numpy") @@ -399,3 +399,73 @@ def check_strides(mat): m.get_py_buffer(dmat, m.PyBUF_ANY_CONTIGUOUS) with pytest.raises(expected_exception): m.get_py_buffer(dmat, m.PyBUF_F_CONTIGUOUS) + + +def test_noexcept_def_buffer(): + """Test issue #2234: def_buffer with noexcept member function pointers. + + Covers both new def_buffer specialisations: + - def_buffer(Return (Class::*)(Args...) noexcept) + - def_buffer(Return (Class::*)(Args...) const noexcept) + """ + # non-const noexcept member function form + buf = m.OneDBuffer(5) + arr = np.frombuffer(buf, dtype=np.float32) + assert arr.shape == (5,) + arr[2] = 3.14 + arr2 = np.frombuffer(buf, dtype=np.float32) + assert arr2[2] == pytest.approx(3.14) + + # const noexcept member function form + cbuf = m.OneDBufferConst(4) + carr = np.frombuffer(cbuf, dtype=np.float32) + assert carr.shape == (4,) + assert carr.flags["WRITEABLE"] is False + + +def test_ref_qualified_def_buffer(): + """Test issue #2234 follow-up: def_buffer with ref-qualified member function pointers. + + Covers: + - def_buffer(Return (Class::*)(Args...) &) + - def_buffer(Return (Class::*)(Args...) const &) + - def_buffer(Return (Class::*)(Args...) & noexcept) + - def_buffer(Return (Class::*)(Args...) const & noexcept) + """ + # non-const lvalue ref-qualified member function form + buf = m.OneDBufferLRef(5) + arr = np.frombuffer(buf, dtype=np.float32) + assert arr.shape == (5,) + arr[1] = 2.5 + arr2 = np.frombuffer(buf, dtype=np.float32) + assert arr2[1] == pytest.approx(2.5) + + # const lvalue ref-qualified member function form + cbuf = m.OneDBufferConstLRef(4) + carr = np.frombuffer(cbuf, dtype=np.float32) + assert carr.shape == (4,) + assert carr.flags["WRITEABLE"] is False + + +@pytest.mark.skipif( + not defined___cpp_noexcept_function_type, + reason="Requires __cpp_noexcept_function_type", +) +def test_ref_qualified_noexcept_def_buffer(): + """Test issue #2234 follow-up: def_buffer with noexcept ref-qualified member pointers. + + Covers: + - def_buffer(Return (Class::*)(Args...) & noexcept) + - def_buffer(Return (Class::*)(Args...) const & noexcept) + """ + nbuf = m.OneDBufferLRefNoexcept(3) + narr = np.frombuffer(nbuf, dtype=np.float32) + assert narr.shape == (3,) + narr[2] = 7.0 + narr2 = np.frombuffer(nbuf, dtype=np.float32) + assert narr2[2] == pytest.approx(7.0) + + ncbuf = m.OneDBufferConstLRefNoexcept(2) + ncarr = np.frombuffer(ncbuf, dtype=np.float32) + assert ncarr.shape == (2,) + assert ncarr.flags["WRITEABLE"] is False diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index c0a57a7b86..327e41eb33 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -179,10 +179,11 @@ def gen_f(): # do some work async work = [1, 2, 3, 4] m.test_async_callback(gen_f(), work) - # wait until work is done - from time import sleep - - sleep(0.5) + # Wait for all detached worker threads to finish. + deadline = time.monotonic() + 5.0 + while len(res) < len(work) and time.monotonic() < deadline: + time.sleep(0.01) + assert len(res) == len(work), f"Timed out waiting for callbacks: res={res!r}" assert sum(res) == sum(x + 3 for x in work) diff --git a/tests/test_class_sh_property.cpp b/tests/test_class_sh_property.cpp index 8863ad7d7b..8777bdc932 100644 --- a/tests/test_class_sh_property.cpp +++ b/tests/test_class_sh_property.cpp @@ -43,6 +43,24 @@ struct WithConstCharPtrMember { const char *const_char_ptr_member = "ConstChar*"; }; +// See PR #6008 +enum class EnumAB { + A = 0, + B = 1, +}; + +struct ShWithEnumABMember { + EnumAB level = EnumAB::A; +}; + +struct SimpleStruct { + int value = 7; +}; + +struct ShWithSimpleStructMember { + SimpleStruct legacy; +}; + } // namespace test_class_sh_property TEST_SUBMODULE(class_sh_property, m) { @@ -91,4 +109,21 @@ TEST_SUBMODULE(class_sh_property, m) { py::classh(m, "WithConstCharPtrMember") .def(py::init<>()) .def_readonly("const_char_ptr_member", &WithConstCharPtrMember::const_char_ptr_member); + + // See PR #6008 + py::enum_(m, "EnumAB").value("A", EnumAB::A).value("B", EnumAB::B); + + py::classh(m, "ShWithEnumABMember") + .def(py::init<>()) + .def_readwrite("level", &ShWithEnumABMember::level); + + py::class_(m, "SimpleStruct") + .def(py::init<>()) + .def_readwrite("value", &SimpleStruct::value); + + py::classh(m, "ShWithSimpleStructMember") + .def(py::init<>()) + .def_readwrite("legacy", &ShWithSimpleStructMember::legacy); + + m.def("getSimpleStructAsShared", []() { return std::make_shared(); }); } diff --git a/tests/test_class_sh_property.py b/tests/test_class_sh_property.py index 0250a7f78e..4a7b77c69a 100644 --- a/tests/test_class_sh_property.py +++ b/tests/test_class_sh_property.py @@ -5,6 +5,7 @@ import pytest import env # noqa: F401 +import pybind11_tests from pybind11_tests import class_sh_property as m @@ -164,3 +165,42 @@ def test_readonly_char6_member(): def test_readonly_const_char_ptr_member(): obj = m.WithConstCharPtrMember() assert obj.const_char_ptr_member == "ConstChar*" + + +# See PR #6008 +def test_enum_member_with_smart_holder_def_readwrite(): + obj = m.ShWithEnumABMember() + assert obj.level == m.EnumAB.A + for _ in range(100): + v = obj.level + assert v == m.EnumAB.A + del v + + +# See PR #6008 +def test_non_smart_holder_member_type_with_smart_holder_owner(): + obj = m.ShWithSimpleStructMember() + for _ in range(1000): + v = obj.legacy + assert v.value == 7 + del v + + +# See PR #6008, previously this was UB +@pytest.mark.skipif( + pybind11_tests.PYBIND11_TEST_SMART_HOLDER, + reason="PYBIND11_TEST_SMART_HOLDER changes the default holder", +) +def test_shared_ptr_return_for_unique_ptr_holder(): + with pytest.raises( + RuntimeError, + match="Unable to convert std::shared_ptr to Python when the bound type does not use std::shared_ptr or py::smart_holder as its holder type", + ): + m.getSimpleStructAsShared() + + +def test_non_smart_holder_member_type_with_smart_holder_owner_aliases_member(): + obj = m.ShWithSimpleStructMember() + legacy = obj.legacy + legacy.value = 13 + assert obj.legacy.value == 13 diff --git a/tests/test_custom_type_setup.py b/tests/test_custom_type_setup.py index 4c6b9510ae..a200b02b5a 100644 --- a/tests/test_custom_type_setup.py +++ b/tests/test_custom_type_setup.py @@ -2,7 +2,6 @@ import gc import os -import sys import weakref import pytest @@ -52,10 +51,6 @@ def test_indirect_cycle(gc_tester): gc_tester(obj) -@pytest.mark.skipif( - env.IOS or sys.platform.startswith("emscripten"), - reason="Requires subprocess support", -) @pytest.mark.skipif("env.PYPY or env.GRAALPY") def test_py_cast_useable_on_shutdown(): """Test that py::cast works during interpreter shutdown. diff --git a/tests/test_gil_scoped.py b/tests/test_gil_scoped.py index 84a7a999ab..fc998b0ed2 100644 --- a/tests/test_gil_scoped.py +++ b/tests/test_gil_scoped.py @@ -168,6 +168,9 @@ def _intentional_deadlock(): def _run_in_process(target, *args, **kwargs): + if env.ANDROID or env.IOS or sys.platform.startswith("emscripten"): + pytest.skip("Requires subprocess support") + test_fn = target if len(args) == 0 else args[0] # Do not need to wait much, 10s should be more than enough. timeout = 0.1 if test_fn is _intentional_deadlock else 10 diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index e324c8bdd4..86b749314f 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -11,6 +11,12 @@ #include "constructor_stats.h" #include "pybind11_tests.h" +#if !defined(PYPY_VERSION) +// Flag set by the capsule destructor in test_dynamic_attr_dealloc_frees_dict_contents. +// File scope so the captureless capsule destructor (void(*)(void*)) can access it. +static bool s_dynamic_attr_capsule_freed = false; +#endif + #if !defined(PYBIND11_OVERLOAD_CAST) template using overload_cast_ = pybind11::detail::overload_cast_impl; @@ -161,6 +167,123 @@ class RegisteredDerived : public UnregisteredBase { double sum() const { return rw_value + ro_value; } }; +// Issue #2234: noexcept methods in an unregistered base should be bindable on the derived class. +// In C++17, noexcept is part of the function type, so &Derived::method resolves to +// a Base member function pointer with noexcept, requiring explicit template specializations. +class NoexceptUnregisteredBase { +public: + // Exercises cpp_function(Return (Class::*)(Args...) const noexcept, ...) + int value() const noexcept { return m_value; } + // Exercises cpp_function(Return (Class::*)(Args...) noexcept, ...) + void set_value(int v) noexcept { m_value = v; } + // Exercises cpp_function(Return (Class::*)(Args...) & noexcept, ...) + void increment() & noexcept { ++m_value; } + // Exercises cpp_function(Return (Class::*)(Args...) const & noexcept, ...) + int capped_value() const & noexcept { return m_value < 100 ? m_value : 100; } + +private: + int m_value = 99; +}; +class NoexceptDerived : public NoexceptUnregisteredBase { +public: + using NoexceptUnregisteredBase::NoexceptUnregisteredBase; +}; + +// Exercises cpp_function(Return (Class::*)(Args...) &&, ...) and +// cpp_function(Return (Class::*)(Args...) const &&, ...) via an unregistered base. +class RValueRefUnregisteredBase { +public: + // Exercises cpp_function(Return (Class::*)(Args...) &&, ...). + // Moves m_payload to verify that std::move(*c).*f is used in the lambda body. + std::string take() && { // NOLINT(readability-make-member-function-const) + return std::move(m_payload); + } + // Exercises cpp_function(Return (Class::*)(Args...) const &&, ...) + int peek() const && { return m_value; } +#ifdef __cpp_noexcept_function_type + // Exercises cpp_function(Return (Class::*)(Args...) && noexcept, ...) + std::string take_noexcept() && noexcept { // NOLINT(readability-make-member-function-const) + return std::move(m_payload); + } + // Exercises cpp_function(Return (Class::*)(Args...) const && noexcept, ...) + int peek_noexcept() const && noexcept { return m_value; } +#endif + +private: + int m_value = 77; + std::string m_payload{"rref_payload"}; +}; +class RValueRefDerived : public RValueRefUnregisteredBase { +public: + using RValueRefUnregisteredBase::RValueRefUnregisteredBase; +}; + +// Exercises overload_cast with noexcept member function pointers (issue #2234). +// In C++17, overload_cast must have noexcept variants to resolve noexcept overloads. +struct NoexceptOverloaded { + py::str method(int) noexcept { return "(int)"; } + py::str method(int) const noexcept { return "(int) const"; } + py::str method(float) noexcept { return "(float)"; } +}; +// Exercises overload_cast with noexcept free function pointers. +int noexcept_free_func(int x) noexcept { return x + 1; } +int noexcept_free_func(float x) noexcept { return static_cast(x) + 2; } + +// Exercises overload_cast with ref-qualified member function pointers. +struct RefQualifiedOverloaded { + py::str method(int) & { return "(int) &"; } + py::str method(int) const & { return "(int) const &"; } + py::str method(float) && { return "(float) &&"; } + py::str method(float) const && { return "(float) const &&"; } +#ifdef __cpp_noexcept_function_type + py::str method(long) & noexcept { return "(long) & noexcept"; } + py::str method(long) const & noexcept { return "(long) const & noexcept"; } + py::str method(double) && noexcept { return "(double) && noexcept"; } + py::str method(double) const && noexcept { return "(double) const && noexcept"; } +#endif +}; + +// Compile-only guard: catch overload_cast resolution regressions/ambiguities early. +using RefQualifiedOverloadedIntCast = py::detail::overload_cast_impl; +using RefQualifiedOverloadedFloatCast = py::detail::overload_cast_impl; +static_assert( + std::is_same::value, + ""); +static_assert(std::is_same::value, + ""); +static_assert( + std::is_same::value, + ""); +static_assert(std::is_same::value, + ""); + +#ifdef __cpp_noexcept_function_type +using RefQualifiedOverloadedLongCast = py::detail::overload_cast_impl; +using RefQualifiedOverloadedDoubleCast = py::detail::overload_cast_impl; +static_assert( + std::is_same::value, + ""); +static_assert(std::is_same::value, + ""); +static_assert( + std::is_same::value, + ""); +static_assert(std::is_same::value, + ""); +#endif + // Test explicit lvalue ref-qualification struct RefQualified { int value = 0; @@ -388,6 +511,24 @@ TEST_SUBMODULE(methods_and_attributes, m) { class CppDerivedDynamicClass : public DynamicClass {}; py::class_(m, "CppDerivedDynamicClass").def(py::init()); + + // test_dynamic_attr_dealloc_frees_dict_contents + // Regression test: pybind11_object_dealloc() must call PyObject_ClearManagedDict() + // before tp_free() so that objects stored in a py::dynamic_attr() instance __dict__ + // have their refcounts decremented when the pybind11 object is freed. On Python 3.14+ + // tp_free no longer implicitly clears the managed dict, causing permanent leaks. + m.def("make_dynamic_attr_with_capsule", []() -> py::object { + s_dynamic_attr_capsule_freed = false; + auto *dummy = new int(0); + py::capsule cap(dummy, [](void *ptr) { + delete static_cast(ptr); + s_dynamic_attr_capsule_freed = true; + }); + py::object obj = py::cast(new DynamicClass(), py::return_value_policy::take_ownership); + obj.attr("data") = cap; + return obj; + }); + m.def("is_dynamic_attr_capsule_freed", []() { return s_dynamic_attr_capsule_freed; }); #endif // test_bad_arg_default @@ -474,6 +615,161 @@ TEST_SUBMODULE(methods_and_attributes, m) { = decltype(py::method_adaptor(&RegisteredDerived::do_nothing)); static_assert(std::is_same::value, ""); + // test_noexcept_base (issue #2234) + // In C++17, noexcept is part of the function type. Binding a noexcept method from an + // unregistered base class must resolve `self` to the derived type, not the base type. + py::class_(m, "NoexceptDerived") + .def(py::init<>()) + // cpp_function(Return (Class::*)(Args...) const noexcept, ...) + .def("value", &NoexceptDerived::value) + // cpp_function(Return (Class::*)(Args...) noexcept, ...) + .def("set_value", &NoexceptDerived::set_value) + // cpp_function(Return (Class::*)(Args...) & noexcept, ...) + .def("increment", &NoexceptDerived::increment) + // cpp_function(Return (Class::*)(Args...) const & noexcept, ...) + .def("capped_value", &NoexceptDerived::capped_value); + + // test_rvalue_ref_qualified_methods: rvalue-ref-qualified methods from an unregistered base. + // method_adaptor must rebind &&/const&& member pointers to the derived type. + py::class_(m, "RValueRefDerived") + .def(py::init<>()) + // cpp_function(Return (Class::*)(Args...) &&, ...) + .def("take", &RValueRefDerived::take) + // cpp_function(Return (Class::*)(Args...) const &&, ...) + .def("peek", &RValueRefDerived::peek) +#ifdef __cpp_noexcept_function_type + // cpp_function(Return (Class::*)(Args...) && noexcept, ...) + .def("take_noexcept", &RValueRefDerived::take_noexcept) + // cpp_function(Return (Class::*)(Args...) const && noexcept, ...) + .def("peek_noexcept", &RValueRefDerived::peek_noexcept) +#endif + ; + + // Verify that &&-qualified methods cannot be called on lvalues, only on rvalues. + // This confirms that the cpp_function lambda must use std::move(*c).*f, not c->*f. +#if __cplusplus >= 201703L + static_assert(!std::is_invocable::value, + "&&-qualified method must not be callable on lvalue"); + static_assert(std::is_invocable::value, + "&&-qualified method must be callable on rvalue"); +#endif + + // Verify method_adaptor preserves &&/const&& qualifiers when rebinding. + using AdaptedRRef = decltype(py::method_adaptor(&RValueRefDerived::take)); + static_assert(std::is_same::value, ""); + using AdaptedConstRRef + = decltype(py::method_adaptor(&RValueRefDerived::peek)); + static_assert(std::is_same::value, ""); + +#ifdef __cpp_noexcept_function_type + // method_adaptor must also handle noexcept member function pointers (issue #2234). + // Verify the noexcept specifier is preserved in the resulting Derived pointer type. + using AdaptedConstNoexcept + = decltype(py::method_adaptor(&NoexceptDerived::value)); + static_assert( + std::is_same::value, ""); + using AdaptedNoexcept + = decltype(py::method_adaptor(&NoexceptDerived::set_value)); + static_assert(std::is_same::value, + ""); + using AdaptedRRefNoexcept + = decltype(py::method_adaptor(&RValueRefDerived::take_noexcept)); + static_assert(std::is_same < AdaptedRRefNoexcept, + std::string (RValueRefDerived::*)() && noexcept > ::value, + ""); + using AdaptedConstRRefNoexcept + = decltype(py::method_adaptor(&RValueRefDerived::peek_noexcept)); + static_assert(std::is_same < AdaptedConstRRefNoexcept, + int (RValueRefDerived::*)() const && noexcept > ::value, + ""); +#endif + + // test_noexcept_overload_cast (issue #2234) + // overload_cast must have noexcept operator() overloads so it can resolve noexcept methods. +#ifdef PYBIND11_OVERLOAD_CAST + py::class_(m, "NoexceptOverloaded") + .def(py::init<>()) + // overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) + .def("method", py::overload_cast(&NoexceptOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const noexcept, true_type) + .def("method_const", py::overload_cast(&NoexceptOverloaded::method, py::const_)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) float + .def("method_float", py::overload_cast(&NoexceptOverloaded::method)); + // overload_cast_impl::operator()(Return (*)(Args...) noexcept) + m.def("noexcept_free_func", py::overload_cast(noexcept_free_func)); + m.def("noexcept_free_func_float", py::overload_cast(noexcept_free_func)); + + py::class_(m, "RefQualifiedOverloaded") + .def(py::init<>()) + // overload_cast_impl::operator()(Return (Class::*)(Args...) &, false_type) + .def("method_lref", py::overload_cast(&RefQualifiedOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const &, true_type) + .def("method_const_lref", + py::overload_cast(&RefQualifiedOverloaded::method, py::const_)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) &&, false_type) + .def("method_rref", py::overload_cast(&RefQualifiedOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const &&, true_type) + .def("method_const_rref", + py::overload_cast(&RefQualifiedOverloaded::method, py::const_)) +# ifdef __cpp_noexcept_function_type + // overload_cast_impl::operator()(Return (Class::*)(Args...) & noexcept, false_type) + .def("method_lref_noexcept", py::overload_cast(&RefQualifiedOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const & noexcept, true_type) + .def("method_const_lref_noexcept", + py::overload_cast(&RefQualifiedOverloaded::method, py::const_)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) && noexcept, false_type) + .def("method_rref_noexcept", py::overload_cast(&RefQualifiedOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const && noexcept, true_type) + .def("method_const_rref_noexcept", + py::overload_cast(&RefQualifiedOverloaded::method, py::const_)) +# endif + ; +#else + // Fallback using explicit static_cast for C++11/14 + py::class_(m, "NoexceptOverloaded") + .def(py::init<>()) + .def("method", + static_cast(&NoexceptOverloaded::method)) + .def("method_const", + static_cast(&NoexceptOverloaded::method)) + .def("method_float", + static_cast(&NoexceptOverloaded::method)); + m.def("noexcept_free_func", static_cast(noexcept_free_func)); + m.def("noexcept_free_func_float", static_cast(noexcept_free_func)); + + py::class_(m, "RefQualifiedOverloaded") + .def(py::init<>()) + .def("method_lref", + static_cast( + &RefQualifiedOverloaded::method)) + .def("method_const_lref", + static_cast( + &RefQualifiedOverloaded::method)) + .def("method_rref", + static_cast( + &RefQualifiedOverloaded::method)) + .def("method_const_rref", + static_cast( + &RefQualifiedOverloaded::method)) +# ifdef __cpp_noexcept_function_type + .def("method_lref_noexcept", + static_cast( + &RefQualifiedOverloaded::method)) + .def("method_const_lref_noexcept", + static_cast( + &RefQualifiedOverloaded::method)) + .def("method_rref_noexcept", + static_cast < py::str (RefQualifiedOverloaded::*)(double) + && noexcept > (&RefQualifiedOverloaded::method)) + .def("method_const_rref_noexcept", + static_cast < py::str (RefQualifiedOverloaded::*)(double) const && noexcept + > (&RefQualifiedOverloaded::method)) +# endif + ; +#endif + // test_methods_and_attributes py::class_(m, "RefQualified") .def(py::init<>()) diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 553d5bfc1b..c0943742da 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -5,7 +5,7 @@ import pytest import env -from pybind11_tests import ConstructorStats +from pybind11_tests import ConstructorStats, defined___cpp_noexcept_function_type from pybind11_tests import methods_and_attributes as m NO_GETTER_MSG = ( @@ -383,6 +383,23 @@ def test_cyclic_gc(): assert cstats.alive() == 0 +@pytest.mark.xfail("env.PYPY", strict=False) +@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") +def test_dynamic_attr_dealloc_frees_dict_contents(): + """Regression: py::dynamic_attr() objects must free __dict__ contents on dealloc. + + pybind11_object_dealloc() did not call PyObject_ClearManagedDict() before tp_free(), + causing objects stored in __dict__ to have their refcounts permanently abandoned on + Python 3.14+ (where tp_free no longer implicitly clears the managed dict). + This caused capsule destructors to never run, leaking the underlying C++ data. + """ + instance = m.make_dynamic_attr_with_capsule() + assert not m.is_dynamic_attr_capsule_freed() + del instance + pytest.gc_collect() + assert m.is_dynamic_attr_capsule_freed() + + def test_bad_arg_default(msg): from pybind11_tests import detailed_error_messages_enabled @@ -517,6 +534,130 @@ def test_unregistered_base_implementations(): assert a.ro_value_prop == 1.75 +def test_noexcept_base(): + """Test issue #2234: binding noexcept methods inherited from an unregistered base class. + + In C++17 noexcept is part of the function type, so &Derived::noexcept_method resolves + to a Base member-function pointer with noexcept specifier. pybind11 must use the Derived + type as `self`, not the Base type, otherwise the call raises TypeError at runtime. + + Covers all four new cpp_function constructor specialisations: + - Return (Class::*)(Args...) noexcept (set_value) + - Return (Class::*)(Args...) const noexcept (value) + - Return (Class::*)(Args...) & noexcept (increment) + - Return (Class::*)(Args...) const & noexcept (capped_value) + """ + obj = m.NoexceptDerived() + # const noexcept + assert obj.value() == 99 + # noexcept (non-const) + obj.set_value(7) + assert obj.value() == 7 + # & noexcept (non-const lvalue ref-qualified) + obj.increment() + assert obj.value() == 8 + # const & noexcept (const lvalue ref-qualified) + assert obj.capped_value() == 8 + obj.set_value(200) + assert obj.capped_value() == 100 # capped at 100 + + +def test_rvalue_ref_qualified_methods(): + """Test that rvalue-ref-qualified (&&/const&&) methods from an unregistered base bind + correctly with `self` resolved to the derived type. + + take() moves m_payload out on each call, so the second call returns "". + This confirms that the cpp_function lambda uses std::move(*c).*f rather than c->*f. + + Covers: + - Return (Class::*)(Args...) && (take) + - Return (Class::*)(Args...) const && (peek) + """ + obj = m.RValueRefDerived() + # && moves m_payload: first call gets the value, second gets empty string + assert obj.take() == "rref_payload" + assert obj.take() == "" + # const && doesn't move: peek() is stable across calls + assert obj.peek() == 77 + assert obj.peek() == 77 + + +@pytest.mark.skipif( + not defined___cpp_noexcept_function_type, + reason="Requires __cpp_noexcept_function_type", +) +def test_noexcept_rvalue_ref_qualified_methods(): + """Test noexcept rvalue-ref-qualified methods from an unregistered base. + + Covers: + - Return (Class::*)(Args...) && noexcept (take_noexcept) + - Return (Class::*)(Args...) const && noexcept (peek_noexcept) + """ + obj = m.RValueRefDerived() + assert obj.take_noexcept() == "rref_payload" + assert obj.take_noexcept() == "" + assert obj.peek_noexcept() == 77 + assert obj.peek_noexcept() == 77 + + +def test_noexcept_overload_cast(): + """Test issue #2234: overload_cast must handle noexcept member and free function pointers. + + In C++17 noexcept is part of the function type, so overload_cast_impl needs dedicated + operator() overloads for noexcept free functions and non-const/const member functions. + """ + obj = m.NoexceptOverloaded() + # overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) + assert obj.method(1) == "(int)" + # overload_cast_impl::operator()(Return (Class::*)(Args...) const noexcept, true_type) + assert obj.method_const(2) == "(int) const" + # overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) float + assert obj.method_float(3.0) == "(float)" + # overload_cast_impl::operator()(Return (*)(Args...) noexcept) + assert m.noexcept_free_func(10) == 11 + assert m.noexcept_free_func_float(10.0) == 12 + + +def test_ref_qualified_overload_cast(): + """Test issue #2234 follow-up: overload_cast with ref-qualified member pointers. + + Covers: + - overload_cast_impl::operator()(Return (Class::*)(Args...) &, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const &, true_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) &&, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const &&, true_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) & noexcept, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const & noexcept, true_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) && noexcept, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const && noexcept, true_type) + """ + obj = m.RefQualifiedOverloaded() + assert obj.method_lref(1) == "(int) &" + assert obj.method_const_lref(1) == "(int) const &" + assert obj.method_rref(1.0) == "(float) &&" + assert obj.method_const_rref(1.0) == "(float) const &&" + + +@pytest.mark.skipif( + not defined___cpp_noexcept_function_type, + reason="Requires __cpp_noexcept_function_type", +) +def test_noexcept_ref_qualified_overload_cast(): + """Test issue #2234 follow-up: overload_cast with noexcept ref-qualified member pointers. + + Covers: + - overload_cast_impl::operator()(Return (Class::*)(Args...) & noexcept, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const & noexcept, true_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) && noexcept, false_type) + - overload_cast_impl::operator()(Return (Class::*)(Args...) const && noexcept, true_type) + """ + obj = m.RefQualifiedOverloaded() + assert obj.method_lref_noexcept(1) == "(long) & noexcept" + assert obj.method_const_lref_noexcept(1) == "(long) const & noexcept" + assert obj.method_rref_noexcept(1.0) == "(double) && noexcept" + assert obj.method_const_rref_noexcept(1.0) == "(double) const && noexcept" + + def test_ref_qualified(): """Tests that explicit lvalue ref-qualified methods can be called just like their non ref-qualified counterparts.""" diff --git a/tests/test_numpy_vectorize.cpp b/tests/test_numpy_vectorize.cpp index dcc4c6ac25..2334521658 100644 --- a/tests/test_numpy_vectorize.cpp +++ b/tests/test_numpy_vectorize.cpp @@ -78,6 +78,27 @@ TEST_SUBMODULE(numpy_vectorize, m) { struct VectorizeTestClass { explicit VectorizeTestClass(int v) : value{v} {}; float method(int x, float y) const { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) &) + // NOLINTNEXTLINE(readability-make-member-function-const) + float method_lref(int x, float y) & { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) const &) + float method_const_lref(int x, float y) const & { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) noexcept) + // NOLINTNEXTLINE(readability-make-member-function-const) + float method_noexcept(int x, float y) noexcept { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) const noexcept) + float method_const_noexcept(int x, float y) const noexcept { + return y + (float) (x + value); + } +#ifdef __cpp_noexcept_function_type + // Exercises vectorize(Return (Class::*)(Args...) & noexcept) + // NOLINTNEXTLINE(readability-make-member-function-const) + float method_lref_noexcept(int x, float y) & noexcept { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) const & noexcept) + float method_const_lref_noexcept(int x, float y) const & noexcept { + return y + (float) (x + value); + } +#endif int value = 0; }; py::class_ vtc(m, "VectorizeTestClass"); @@ -85,6 +106,15 @@ TEST_SUBMODULE(numpy_vectorize, m) { // Automatic vectorizing of methods vtc.def("method", py::vectorize(&VectorizeTestClass::method)); + vtc.def("method_lref", py::vectorize(&VectorizeTestClass::method_lref)); + vtc.def("method_const_lref", py::vectorize(&VectorizeTestClass::method_const_lref)); + vtc.def("method_noexcept", py::vectorize(&VectorizeTestClass::method_noexcept)); + vtc.def("method_const_noexcept", py::vectorize(&VectorizeTestClass::method_const_noexcept)); +#ifdef __cpp_noexcept_function_type + vtc.def("method_lref_noexcept", py::vectorize(&VectorizeTestClass::method_lref_noexcept)); + vtc.def("method_const_lref_noexcept", + py::vectorize(&VectorizeTestClass::method_const_lref_noexcept)); +#endif // test_trivial_broadcasting // Internal optimization test for whether the input is trivially broadcastable: diff --git a/tests/test_numpy_vectorize.py b/tests/test_numpy_vectorize.py index 05f7c704f5..030abeceaa 100644 --- a/tests/test_numpy_vectorize.py +++ b/tests/test_numpy_vectorize.py @@ -2,6 +2,7 @@ import pytest +from pybind11_tests import defined___cpp_noexcept_function_type from pybind11_tests import numpy_vectorize as m np = pytest.importorskip("numpy") @@ -246,6 +247,56 @@ def test_method_vectorization(): assert np.all(o.method(x, y) == [[14, 15], [24, 25]]) +def test_ref_qualified_method_vectorization(): + """Test issue #2234 follow-up: vectorize with lvalue-ref-qualified member pointers. + + Covers: + - vectorize(Return (Class::*)(Args...) &) + - vectorize(Return (Class::*)(Args...) const &) + - vectorize(Return (Class::*)(Args...) & noexcept) + - vectorize(Return (Class::*)(Args...) const & noexcept) + """ + o = m.VectorizeTestClass(3) + x = np.array([1, 2], dtype="int") + y = np.array([[10], [20]], dtype="float32") + assert np.all(o.method_lref(x, y) == [[14, 15], [24, 25]]) + assert np.all(o.method_const_lref(x, y) == [[14, 15], [24, 25]]) + + +@pytest.mark.skipif( + not defined___cpp_noexcept_function_type, + reason="Requires __cpp_noexcept_function_type", +) +def test_noexcept_ref_qualified_method_vectorization(): + """Test issue #2234 follow-up: vectorize with noexcept lvalue-ref-qualified member pointers. + + Covers: + - vectorize(Return (Class::*)(Args...) & noexcept) + - vectorize(Return (Class::*)(Args...) const & noexcept) + """ + o = m.VectorizeTestClass(3) + x = np.array([1, 2], dtype="int") + y = np.array([[10], [20]], dtype="float32") + assert np.all(o.method_lref_noexcept(x, y) == [[14, 15], [24, 25]]) + assert np.all(o.method_const_lref_noexcept(x, y) == [[14, 15], [24, 25]]) + + +def test_noexcept_method_vectorization(): + """Test issue #2234: vectorize must handle noexcept member function pointers. + + Covers both new vectorize specialisations: + - vectorize(Return (Class::*)(Args...) noexcept) + - vectorize(Return (Class::*)(Args...) const noexcept) + """ + o = m.VectorizeTestClass(3) + x = np.array([1, 2], dtype="int") + y = np.array([[10], [20]], dtype="float32") + # vectorize(Return (Class::*)(Args...) noexcept) + assert np.all(o.method_noexcept(x, y) == [[14, 15], [24, 25]]) + # vectorize(Return (Class::*)(Args...) const noexcept) + assert np.all(o.method_const_noexcept(x, y) == [[14, 15], [24, 25]]) + + def test_array_collapse(): assert not isinstance(m.vectorized_func(1, 2, 3), np.ndarray) assert not isinstance(m.vectorized_func(np.array(1), 2, 3), np.ndarray) diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index e214350015..ff77940965 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -1086,8 +1086,7 @@ TEST_SUBMODULE(pytypes, m) { static_class.def(py::init()); static_class.attr_with_type_hint>("x") = 1.0; static_class.attr_with_type_hint>>( - "dict_str_int") - = py::dict(); + "dict_str_int") = py::dict(); struct Instance {}; auto instance = py::class_(m, "Instance", py::dynamic_attr()); diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index 0ac1a41bd9..3617fa3e85 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -247,6 +247,28 @@ struct SharedFromThisVBase : std::enable_shared_from_this { }; struct SharedFromThisVirt : virtual SharedFromThisVBase {}; +// Issue #5989: static_pointer_cast where dynamic_pointer_cast is needed +// (virtual inheritance with shared_ptr holder) +struct SftVirtBase : std::enable_shared_from_this { + SftVirtBase() = default; + virtual ~SftVirtBase() = default; + static std::shared_ptr create() { return std::make_shared(); } + virtual std::string name() { return "SftVirtBase"; } +}; +struct SftVirtDerived : SftVirtBase { + using SftVirtBase::SftVirtBase; + static std::shared_ptr create() { return std::make_shared(); } + std::string name() override { return "SftVirtDerived"; } +}; +struct SftVirtDerived2 : virtual SftVirtDerived { + using SftVirtDerived::SftVirtDerived; + static std::shared_ptr create() { + return std::make_shared(); + } + std::string name() override { return "SftVirtDerived2"; } + std::string call_name(const std::shared_ptr &d2) { return d2->name(); } +}; + // test_move_only_holder struct C { C() { print_created(this); } @@ -522,6 +544,17 @@ TEST_SUBMODULE(smart_ptr, m) { py::class_>(m, "SharedFromThisVirt") .def_static("get", []() { return sft.get(); }); + // Issue #5989: static_pointer_cast where dynamic_pointer_cast is needed + py::class_>(m, "SftVirtBase") + .def(py::init<>(&SftVirtBase::create)) + .def("name", &SftVirtBase::name); + py::class_>(m, "SftVirtDerived") + .def(py::init<>(&SftVirtDerived::create)); + py::class_>( + m, "SftVirtDerived2") + .def(py::init<>(&SftVirtDerived2::create)) + .def("call_name", &SftVirtDerived2::call_name, py::arg("d2")); + // test_move_only_holder py::class_>(m, "TypeWithMoveOnlyHolder") .def_static("make", []() { return custom_unique_ptr(new C); }) diff --git a/tests/test_smart_ptr.py b/tests/test_smart_ptr.py index 2d48aac78d..76ebd8cf20 100644 --- a/tests/test_smart_ptr.py +++ b/tests/test_smart_ptr.py @@ -251,6 +251,19 @@ def test_shared_ptr_from_this_and_references(): assert y is z +def test_shared_from_this_virt_shared_ptr_arg(): + """Issue #5989: static_pointer_cast fails with virtual inheritance.""" + b = m.SftVirtBase() + assert b.name() == "SftVirtBase" + + d = m.SftVirtDerived() + assert d.name() == "SftVirtDerived" + + d2 = m.SftVirtDerived2() + assert d2.name() == "SftVirtDerived2" + assert d2.call_name(d2) == "SftVirtDerived2" + + @pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") def test_move_only_holder(): a = m.TypeWithMoveOnlyHolder.make() diff --git a/tests/test_standalone_enum_module.py b/tests/test_standalone_enum_module.py new file mode 100644 index 0000000000..3358b887f7 --- /dev/null +++ b/tests/test_standalone_enum_module.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import os + +import env + + +def test_enum_import_exit_no_crash(): + # Added in PR #6015. Modeled after reproducer under issue #5976 + env.check_script_success_in_subprocess( + f""" + import sys + sys.path.insert(0, {os.path.dirname(env.__file__)!r}) + import standalone_enum_module as m + assert m.SomeEnum.__class__.__name__ == "pybind11_type" + """, + rerun=1, + ) diff --git a/tests/test_virtual_functions.cpp b/tests/test_virtual_functions.cpp index a6164eb81d..78a4fdfa50 100644 --- a/tests/test_virtual_functions.cpp +++ b/tests/test_virtual_functions.cpp @@ -173,8 +173,7 @@ struct AdderBase { using DataVisitor = std::function; virtual void - operator()(const Data &first, const Data &second, const DataVisitor &visitor) const - = 0; + operator()(const Data &first, const Data &second, const DataVisitor &visitor) const = 0; virtual ~AdderBase() = default; AdderBase() = default; AdderBase(const AdderBase &) = delete; diff --git a/tests/test_with_catch/catch_skip.h b/tests/test_with_catch/catch_skip.h index 72ffdb62b6..9e2954d3b6 100644 --- a/tests/test_with_catch/catch_skip.h +++ b/tests/test_with_catch/catch_skip.h @@ -4,13 +4,18 @@ #pragma once +#include + #include #define PYBIND11_CATCH2_SKIP_IF(condition, reason) \ do { \ + PYBIND11_WARNING_PUSH \ + PYBIND11_WARNING_DISABLE_MSVC(4127) \ if (condition) { \ Catch::cout() << "[ SKIPPED ] " << (reason) << '\n'; \ Catch::cout().flush(); \ return; \ } \ + PYBIND11_WARNING_POP \ } while (0) diff --git a/tests/test_with_catch/test_interpreter.cpp b/tests/test_with_catch/test_interpreter.cpp index d227eecddc..4103c0f5ff 100644 --- a/tests/test_with_catch/test_interpreter.cpp +++ b/tests/test_with_catch/test_interpreter.cpp @@ -5,6 +5,8 @@ // catch 2.0.1; this should be fixed in the next catch release after 2.0.1). PYBIND11_WARNING_DISABLE_MSVC(4996) +#include "catch_skip.h" + #include #include #include @@ -84,6 +86,14 @@ PYBIND11_EMBEDDED_MODULE(trampoline_module, m) { .def("func", &test_override_cache_helper::func); } +enum class SomeEnum { value1, value2 }; // Added in PR #6015 + +PYBIND11_EMBEDDED_MODULE(enum_module, m, py::multiple_interpreters::per_interpreter_gil()) { + py::enum_(m, "SomeEnum") + .value("value1", SomeEnum::value1) + .value("value2", SomeEnum::value2); +} + PYBIND11_EMBEDDED_MODULE(throw_exception, ) { throw std::runtime_error("C++ Error"); } PYBIND11_EMBEDDED_MODULE(throw_error_already_set, ) { @@ -343,6 +353,24 @@ TEST_CASE("Restart the interpreter") { REQUIRE(py_widget.attr("the_message").cast() == "Hello after restart"); } +TEST_CASE("Enum module survives restart") { // Added in PR #6015 + // Regression test for gh-5976: py::enum_ uses def_property_static, which + // calls process_attributes::init after initialize_generic's strdup loop, + // leaving arg names as string literals. Without the fix, destruct() would + // call free() on those literals during interpreter finalization. + PYBIND11_CATCH2_SKIP_IF(PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION == 12, + "Pre-existing crash in enum cleanup during finalize on Python 3.12"); + + auto enum_mod = py::module_::import("enum_module"); + REQUIRE(enum_mod.attr("SomeEnum").attr("value1").attr("name").cast() == "value1"); + + py::finalize_interpreter(); + py::initialize_interpreter(); + + enum_mod = py::module_::import("enum_module"); + REQUIRE(enum_mod.attr("SomeEnum").attr("value2").attr("name").cast() == "value2"); +} + TEST_CASE("Execution frame") { // When the interpreter is embedded, there is no execution frame, but `py::exec` // should still function by using reasonable globals: `__main__.__dict__`.