From d218b160a638b1345310abb45498fd3768f94fc4 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 13 Jun 2025 18:46:07 -0400 Subject: [PATCH 01/15] ci: avoid 3.13.4 on Windows (#5725) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17c0bf788c..d6d3088988 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,7 +136,7 @@ jobs: python-version: '3.10' cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON -DCMAKE_CXX_FLAGS="/GR /EHsc" - runs-on: windows-2022 - python-version: '3.13' + python-version: '3.13.3' cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL - runs-on: windows-latest python-version: '3.13t' @@ -850,7 +850,7 @@ jobs: args: -DCMAKE_CXX_STANDARD=17 - python: '3.10' args: -DCMAKE_CXX_STANDARD=20 - - python: '3.13' + - python: '3.13.3' name: "🐍 ${{ matrix.python }} • MSVC 2022 • x86 ${{ matrix.args }}" From f3bb00732d88557fadc5b6e710b0b8563fbfd91c Mon Sep 17 00:00:00 2001 From: Rosdf <36959923+Rosdf@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:38:09 +0400 Subject: [PATCH 02/15] better test for const only smart ptr (#5727) --- tests/test_smart_ptr.cpp | 23 ++++++++++++++++++----- tests/test_smart_ptr.py | 8 +++----- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index 7d009c9a84..5fdd69db38 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -193,6 +193,20 @@ class MyObject5 { // managed by huge_unique_ptr int value; }; +// test const_only_shared_ptr +class MyObject6 { +public: + static const_only_shared_ptr createObject(std::string value) { + return const_only_shared_ptr(new MyObject6(std::move(value))); + } + + const std::string &value() const { return value_; } + +private: + explicit MyObject6(std::string &&value) : value_{std::move(value)} {} + std::string value_; +}; + // test_shared_ptr_and_references struct SharedPtrRef { struct A { @@ -412,11 +426,6 @@ TEST_SUBMODULE(smart_ptr, m) { m.def("print_myobject2_4", [](const std::shared_ptr *obj) { py::print((*obj)->toString()); }); - m.def("make_myobject2_3", - [](int val) { return const_only_shared_ptr(new MyObject2(val)); }); - m.def("print_myobject2_5", - [](const const_only_shared_ptr &obj) { py::print(obj.get()->toString()); }); - py::class_>(m, "MyObject3").def(py::init()); m.def("make_myobject3_1", []() { return new MyObject3(8); }); m.def("make_myobject3_2", []() { return std::make_shared(9); }); @@ -459,6 +468,10 @@ TEST_SUBMODULE(smart_ptr, m) { .def(py::init()) .def_readwrite("value", &MyObject5::value); + py::class_>(m, "MyObject6") + .def(py::init([](const std::string &value) { return MyObject6::createObject(value); })) + .def_property_readonly("value", &MyObject6::value); + // test_shared_ptr_and_references using A = SharedPtrRef::A; py::class_>(m, "A"); diff --git a/tests/test_smart_ptr.py b/tests/test_smart_ptr.py index e1d51ca06c..2d48aac78d 100644 --- a/tests/test_smart_ptr.py +++ b/tests/test_smart_ptr.py @@ -352,8 +352,6 @@ def test_move_only_holder_caster_shared_ptr_with_smart_holder_support_enabled(): ) -def test_const_only_holder(capture): - o = m.make_myobject2_3(4) - with capture: - m.print_myobject2_5(o) - assert capture == "MyObject2[4]\n" +def test_const_only_holder(): + o = m.MyObject6("my_data") + assert o.value == "my_data" From e2f86af216d969188ca0fb42e2059aa517dd3a6d Mon Sep 17 00:00:00 2001 From: Jan Iwaszkiewicz Date: Tue, 17 Jun 2025 20:03:58 +0200 Subject: [PATCH 03/15] docs: add documentation entry for warnings (#5356) * [docs] Add entry for warnings * Fix code formatting * Update docs * Fix issue with docs * [skip ci] Minor edits. --------- Co-authored-by: Ralf W. Grosse-Kunstleve --- docs/advanced/exceptions.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/advanced/exceptions.rst b/docs/advanced/exceptions.rst index 8f0e9c93a4..921b4367f5 100644 --- a/docs/advanced/exceptions.rst +++ b/docs/advanced/exceptions.rst @@ -328,6 +328,28 @@ Alternately, to ignore the error, call `PyErr_Clear Any Python error must be thrown or cleared, or Python/pybind11 will be left in an invalid state. +Handling warnings from the Python C API +======================================= + +Wrappers for handling Python warnings are provided in ``pybind11/warnings.h``. +This header must be included explicitly; it is not transitively included via +``pybind11/pybind11.h``. + +Warnings can be raised with the ``warn`` function: + +.. code-block:: cpp + + py::warnings::warn("This is a warning!", PyExc_Warning); + + // Optionally, a `stack_level` can be specified. + py::warnings::warn("Another one!", PyExc_DeprecationWarning, 3); + +New warning types can be registered at the module level using ``new_warning_type``: + +.. code-block:: cpp + + py::warnings::new_warning_type(m, "CustomWarning", PyExc_RuntimeWarning); + Chaining exceptions ('raise from') ================================== From 365d41a4baaeb941be7f808246cf52b7ef668118 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 17 Jun 2025 12:14:50 -0700 Subject: [PATCH 04/15] Eliminate cross-DSO RTTI reliance in `smart_holder` functionality (for platforms like macOS). (#5728) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert PR #5700 production code change (pybind11/detail/struct_smart_holder.h). ``` git checkout b19489145b2c7a117138632d624809dfb3b380bb~1 include/pybind11/detail/struct_smart_holder.h ``` * Introduce `get_internals().get_memory_guarded_delete()` * [skip ci] Only pass around `memory::get_guarded_delete` function pointer. * [skip ci] Change a variable name for internal consistency. Add 3 x NOTE: PYBIND11_INTERNALS_VERSION needs to be bumped if changes are made to this struct. * Add comment: get_internals().get_memory_guarded_delete does not need with_internals() * Traverse all DSOs to find memory::guarded_delete with matching RTTI. * Add nullptr check to dynamic_cast overload. Suggested by ChatGPT for these reasons: * Prevents runtime RTTI lookups on nullptr. * Helps avoid undefined behavior if users pass in nulls from failed casts or optional paths. * Ensures consistent return value semantics and no accidental access to vtable. * Improve smart_holder unique_ptr deleter compatibility checks across DSOs: * Replace RTTI-based detection of std::default_delete with a constexpr check to avoid RTTI reliance * Add type_info_equal_across_dso_boundaries() fallback using type_info::name() for RTTI equality across macOS DSOs * Rename related flags and functions for clarity (e.g., builtin → std_default) * Improves ABI robustness and clarity of ownership checks in smart_holder * Trivial renaming for internal consistency: builtin_delete → std_default_delete * Add get_trampoline_self_life_support to detail::type_info (passes local testing). * Polish previous commit slightly. * [skip ci] Store memory::get_guarded_delete in `detail::type_info` instead of `detail::internals` (no searching across DSOs required). * Revert change suggested by ChatGPT. After double-checking, ChatGPT agrees this isn't needed. * Minor polishing. --- include/pybind11/attr.h | 7 ++ include/pybind11/cast.h | 14 ++- include/pybind11/detail/internals.h | 14 ++- include/pybind11/detail/struct_smart_holder.h | 111 ++++++++++-------- include/pybind11/detail/type_caster_base.h | 44 +++---- include/pybind11/pybind11.h | 11 ++ .../pybind11/trampoline_self_life_support.h | 5 + tests/pure_cpp/smart_holder_poc.h | 6 +- tests/pure_cpp/smart_holder_poc_test.cpp | 12 +- 9 files changed, 135 insertions(+), 89 deletions(-) diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index 786133afe1..9b631fa48d 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -12,6 +12,7 @@ #include "detail/common.h" #include "cast.h" +#include "trampoline_self_life_support.h" #include @@ -312,6 +313,12 @@ struct type_record { /// Function pointer to class_<..>::dealloc void (*dealloc)(detail::value_and_holder &) = nullptr; + /// Function pointer for casting alias class (aka trampoline) pointer to + /// trampoline_self_life_support pointer. Sidesteps cross-DSO RTTI issues + /// on platforms like macOS (see PR #5728 for details). + get_trampoline_self_life_support_fn get_trampoline_self_life_support + = [](void *) -> trampoline_self_life_support * { return nullptr; }; + /// List of base classes of the newly created type list bases; diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 7a6edf25b7..60dfea5b6f 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -980,7 +980,7 @@ struct copyable_holder_caster< explicit operator std::shared_ptr &() { if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { - shared_ptr_storage = sh_load_helper.load_as_shared_ptr(value); + shared_ptr_storage = sh_load_helper.load_as_shared_ptr(typeinfo, value); } return shared_ptr_storage; } @@ -989,7 +989,8 @@ struct copyable_holder_caster< if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { // Reusing shared_ptr code to minimize code complexity. shared_ptr_storage - = sh_load_helper.load_as_shared_ptr(value, + = sh_load_helper.load_as_shared_ptr(typeinfo, + value, /*responsible_parent=*/nullptr, /*force_potentially_slicing_shared_ptr=*/true); } @@ -1019,7 +1020,8 @@ struct copyable_holder_caster< copyable_holder_caster loader; loader.load(responsible_parent, /*convert=*/false); assert(loader.typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder); - return loader.sh_load_helper.load_as_shared_ptr(loader.value, responsible_parent); + return loader.sh_load_helper.load_as_shared_ptr( + loader.typeinfo, loader.value, responsible_parent); } protected: @@ -1240,7 +1242,7 @@ struct move_only_holder_caster< explicit operator std::unique_ptr() { if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { - return sh_load_helper.template load_as_unique_ptr(value); + return sh_load_helper.template load_as_unique_ptr(typeinfo, value); } pybind11_fail("Expected to be UNREACHABLE: " __FILE__ ":" PYBIND11_TOSTRING(__LINE__)); } @@ -1248,12 +1250,12 @@ struct move_only_holder_caster< explicit operator const std::unique_ptr &() { if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { // Get shared_ptr to ensure that the Python object is not disowned elsewhere. - shared_ptr_storage = sh_load_helper.load_as_shared_ptr(value); + shared_ptr_storage = sh_load_helper.load_as_shared_ptr(typeinfo, value); // Build a temporary unique_ptr that is meant to never expire. unique_ptr_storage = std::shared_ptr>( new std::unique_ptr{ sh_load_helper.template load_as_const_unique_ptr( - shared_ptr_storage.get())}, + typeinfo, shared_ptr_storage.get())}, [](std::unique_ptr *ptr) { if (!ptr) { pybind11_fail("FATAL: `const std::unique_ptr &` was disowned " diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 972682e5eb..414ab897e7 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -12,8 +12,10 @@ #include #include #include +#include #include "common.h" +#include "struct_smart_holder.h" #include #include @@ -35,11 +37,11 @@ /// further ABI-incompatible changes may be made before the ABI is officially /// changed to the new version. #ifndef PYBIND11_INTERNALS_VERSION -# define PYBIND11_INTERNALS_VERSION 10 +# define PYBIND11_INTERNALS_VERSION 11 #endif -#if PYBIND11_INTERNALS_VERSION < 10 -# error "PYBIND11_INTERNALS_VERSION 10 is the minimum for all platforms for pybind11v3." +#if PYBIND11_INTERNALS_VERSION < 11 +# error "PYBIND11_INTERNALS_VERSION 11 is the minimum for all platforms for pybind11v3." #endif PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) @@ -308,6 +310,12 @@ struct type_info { void *(*operator_new)(size_t); void (*init_instance)(instance *, const void *); void (*dealloc)(value_and_holder &v_h); + + // Cross-DSO-safe function pointers, to sidestep cross-DSO RTTI issues + // on platforms like macOS (see PR #5728 for details): + memory::get_guarded_delete_fn get_memory_guarded_delete = memory::get_guarded_delete; + get_trampoline_self_life_support_fn get_trampoline_self_life_support = nullptr; + std::vector implicit_conversions; std::vector> implicit_casts; std::vector *direct_conversions; diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h index 9b2da87837..5b65b4a9b2 100644 --- a/include/pybind11/detail/struct_smart_holder.h +++ b/include/pybind11/detail/struct_smart_holder.h @@ -50,6 +50,7 @@ High-level aspects: #include "pybind11_namespace_macros.h" +#include #include #include #include @@ -58,19 +59,6 @@ High-level aspects: #include #include -// IMPORTANT: This code block must stay BELOW the #include above. -// This is only required on some builds with libc++ (one of three implementations -// in -// https://github.com/llvm/llvm-project/blob/a9b64bb3180dab6d28bf800a641f9a9ad54d2c0c/libcxx/include/typeinfo#L271-L276 -// require it) -#if !defined(PYBIND11_EXPORT_GUARDED_DELETE) -# if defined(_LIBCPP_VERSION) && !defined(WIN32) && !defined(_WIN32) -# define PYBIND11_EXPORT_GUARDED_DELETE __attribute__((visibility("default"))) -# else -# define PYBIND11_EXPORT_GUARDED_DELETE -# endif -#endif - PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) PYBIND11_NAMESPACE_BEGIN(memory) @@ -91,7 +79,8 @@ static constexpr bool type_has_shared_from_this(const void *) { return false; } -struct PYBIND11_EXPORT_GUARDED_DELETE guarded_delete { +struct guarded_delete { + // NOTE: PYBIND11_INTERNALS_VERSION needs to be bumped if changes are made to this struct. std::weak_ptr released_ptr; // Trick to keep the smart_holder memory footprint small. std::function del_fun; // Rare case. void (*del_ptr)(void *); // Common case. @@ -113,13 +102,19 @@ struct PYBIND11_EXPORT_GUARDED_DELETE guarded_delete { } }; +inline guarded_delete *get_guarded_delete(const std::shared_ptr &ptr) { + return std::get_deleter(ptr); +} + +using get_guarded_delete_fn = guarded_delete *(*) (const std::shared_ptr &); + template ::value, int>::type = 0> -inline void builtin_delete_if_destructible(void *raw_ptr) { +inline void std_default_delete_if_destructible(void *raw_ptr) { std::default_delete{}(static_cast(raw_ptr)); } template ::value, int>::type = 0> -inline void builtin_delete_if_destructible(void *) { +inline void std_default_delete_if_destructible(void *) { // This noop operator is needed to avoid a compilation error (for `delete raw_ptr;`), but // throwing an exception from a destructor will std::terminate the process. Therefore the // runtime check for lifetime-management correctness is implemented elsewhere (in @@ -127,12 +122,13 @@ inline void builtin_delete_if_destructible(void *) { } template -guarded_delete make_guarded_builtin_delete(bool armed_flag) { - return guarded_delete(builtin_delete_if_destructible, armed_flag); +guarded_delete make_guarded_std_default_delete(bool armed_flag) { + return guarded_delete(std_default_delete_if_destructible, armed_flag); } template struct custom_deleter { + // NOTE: PYBIND11_INTERNALS_VERSION needs to be bumped if changes are made to this struct. D deleter; explicit custom_deleter(D &&deleter) : deleter{std::forward(deleter)} {} void operator()(void *raw_ptr) { deleter(static_cast(raw_ptr)); } @@ -144,17 +140,25 @@ guarded_delete make_guarded_custom_deleter(D &&uqp_del, bool armed_flag) { std::function(custom_deleter(std::forward(uqp_del))), armed_flag); } -template -inline bool is_std_default_delete(const std::type_info &rtti_deleter) { - return rtti_deleter == typeid(std::default_delete) - || rtti_deleter == typeid(std::default_delete); +template +constexpr bool uqp_del_is_std_default_delete() { + return std::is_same>::value + || std::is_same>::value; +} + +inline bool type_info_equal_across_dso_boundaries(const std::type_info &a, + const std::type_info &b) { + // RTTI pointer comparison may fail across DSOs (e.g., macOS libc++). + // Fallback to name comparison, which is generally safe and ABI-stable enough for our use. + return a == b || std::strcmp(a.name(), b.name()) == 0; } struct smart_holder { + // NOTE: PYBIND11_INTERNALS_VERSION needs to be bumped if changes are made to this struct. const std::type_info *rtti_uqp_del = nullptr; std::shared_ptr vptr; bool vptr_is_using_noop_deleter : 1; - bool vptr_is_using_builtin_delete : 1; + bool vptr_is_using_std_default_delete : 1; bool vptr_is_external_shared_ptr : 1; bool is_populated : 1; bool is_disowned : 1; @@ -166,7 +170,7 @@ struct smart_holder { smart_holder &operator=(const smart_holder &) = delete; smart_holder() - : vptr_is_using_noop_deleter{false}, vptr_is_using_builtin_delete{false}, + : vptr_is_using_noop_deleter{false}, vptr_is_using_std_default_delete{false}, vptr_is_external_shared_ptr{false}, is_populated{false}, is_disowned{false} {} bool has_pointee() const { return vptr != nullptr; } @@ -191,7 +195,7 @@ struct smart_holder { } } - void ensure_vptr_is_using_builtin_delete(const char *context) const { + void ensure_vptr_is_using_std_default_delete(const char *context) const { if (vptr_is_external_shared_ptr) { throw std::invalid_argument(std::string("Cannot disown external shared_ptr (") + context + ")."); @@ -200,24 +204,26 @@ struct smart_holder { throw std::invalid_argument(std::string("Cannot disown non-owning holder (") + context + ")."); } - if (!vptr_is_using_builtin_delete) { + if (!vptr_is_using_std_default_delete) { throw std::invalid_argument(std::string("Cannot disown custom deleter (") + context + ")."); } } template - void ensure_compatible_rtti_uqp_del(const char *context) const { - const std::type_info *rtti_requested = &typeid(D); + void ensure_compatible_uqp_del(const char *context) const { if (!rtti_uqp_del) { - if (!is_std_default_delete(*rtti_requested)) { + if (!uqp_del_is_std_default_delete()) { throw std::invalid_argument(std::string("Missing unique_ptr deleter (") + context + ")."); } - ensure_vptr_is_using_builtin_delete(context); - } else if (!(*rtti_requested == *rtti_uqp_del) - && !(vptr_is_using_builtin_delete - && is_std_default_delete(*rtti_requested))) { + ensure_vptr_is_using_std_default_delete(context); + return; + } + if (uqp_del_is_std_default_delete() && vptr_is_using_std_default_delete) { + return; + } + if (!type_info_equal_across_dso_boundaries(typeid(D), *rtti_uqp_del)) { throw std::invalid_argument(std::string("Incompatible unique_ptr deleter (") + context + ")."); } @@ -244,19 +250,20 @@ struct smart_holder { } } - void reset_vptr_deleter_armed_flag(bool armed_flag) const { - auto *vptr_del_ptr = std::get_deleter(vptr); - if (vptr_del_ptr == nullptr) { + void reset_vptr_deleter_armed_flag(const get_guarded_delete_fn ggd_fn, bool armed_flag) const { + auto *gd = ggd_fn(vptr); + if (gd == nullptr) { throw std::runtime_error( "smart_holder::reset_vptr_deleter_armed_flag() called in an invalid context."); } - vptr_del_ptr->armed_flag = armed_flag; + gd->armed_flag = armed_flag; } - // Caller is responsible for precondition: ensure_compatible_rtti_uqp_del() must succeed. + // Caller is responsible for precondition: ensure_compatible_uqp_del() must succeed. template - std::unique_ptr extract_deleter(const char *context) const { - const auto *gd = std::get_deleter(vptr); + std::unique_ptr extract_deleter(const char *context, + const get_guarded_delete_fn ggd_fn) const { + auto *gd = ggd_fn(vptr); if (gd && gd->use_del_fun) { const auto &custom_deleter_ptr = gd->del_fun.template target>(); if (custom_deleter_ptr == nullptr) { @@ -288,28 +295,28 @@ struct smart_holder { static smart_holder from_raw_ptr_take_ownership(T *raw_ptr, bool void_cast_raw_ptr = false) { ensure_pointee_is_destructible("from_raw_ptr_take_ownership"); smart_holder hld; - auto gd = make_guarded_builtin_delete(true); + auto gd = make_guarded_std_default_delete(true); if (void_cast_raw_ptr) { hld.vptr.reset(static_cast(raw_ptr), std::move(gd)); } else { hld.vptr.reset(raw_ptr, std::move(gd)); } - hld.vptr_is_using_builtin_delete = true; + hld.vptr_is_using_std_default_delete = true; hld.is_populated = true; return hld; } // Caller is responsible for ensuring the complex preconditions // (see `smart_holder_type_caster_support::load_helper`). - void disown() { - reset_vptr_deleter_armed_flag(false); + void disown(const get_guarded_delete_fn ggd_fn) { + reset_vptr_deleter_armed_flag(ggd_fn, false); is_disowned = true; } // Caller is responsible for ensuring the complex preconditions // (see `smart_holder_type_caster_support::load_helper`). - void reclaim_disowned() { - reset_vptr_deleter_armed_flag(true); + void reclaim_disowned(const get_guarded_delete_fn ggd_fn) { + reset_vptr_deleter_armed_flag(ggd_fn, true); is_disowned = false; } @@ -319,14 +326,14 @@ struct smart_holder { void ensure_can_release_ownership(const char *context = "ensure_can_release_ownership") const { ensure_is_not_disowned(context); - ensure_vptr_is_using_builtin_delete(context); + ensure_vptr_is_using_std_default_delete(context); ensure_use_count_1(context); } // Caller is responsible for ensuring the complex preconditions // (see `smart_holder_type_caster_support::load_helper`). - void release_ownership() { - reset_vptr_deleter_armed_flag(false); + void release_ownership(const get_guarded_delete_fn ggd_fn) { + reset_vptr_deleter_armed_flag(ggd_fn, false); release_disowned(); } @@ -335,10 +342,10 @@ struct smart_holder { void *void_ptr = nullptr) { smart_holder hld; hld.rtti_uqp_del = &typeid(D); - hld.vptr_is_using_builtin_delete = is_std_default_delete(*hld.rtti_uqp_del); + hld.vptr_is_using_std_default_delete = uqp_del_is_std_default_delete(); guarded_delete gd{nullptr, false}; - if (hld.vptr_is_using_builtin_delete) { - gd = make_guarded_builtin_delete(true); + if (hld.vptr_is_using_std_default_delete) { + gd = make_guarded_std_default_delete(true); } else { gd = make_guarded_custom_deleter(std::move(unq_ptr.get_deleter()), true); } diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index aa8f2cf7e7..1b23c5c681 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -531,8 +531,8 @@ struct value_and_holder_helper { } // have_holder() must be true or this function will fail. - void throw_if_instance_is_currently_owned_by_shared_ptr() const { - auto *vptr_gd_ptr = std::get_deleter(holder().vptr); + void throw_if_instance_is_currently_owned_by_shared_ptr(const type_info *tinfo) const { + auto *vptr_gd_ptr = tinfo->get_memory_guarded_delete(holder().vptr); if (vptr_gd_ptr != nullptr && !vptr_gd_ptr->released_ptr.expired()) { throw value_error("Python instance is currently owned by a std::shared_ptr."); } @@ -564,8 +564,7 @@ handle smart_holder_from_unique_ptr(std::unique_ptr &&src, assert(st.second != nullptr); const detail::type_info *tinfo = st.second; if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { - auto *self_life_support - = dynamic_raw_ptr_cast_if_possible(src.get()); + auto *self_life_support = tinfo->get_trampoline_self_life_support(src.get()); if (self_life_support != nullptr) { value_and_holder &v_h = self_life_support->v_h; if (v_h.inst != nullptr && v_h.vh != nullptr) { @@ -576,7 +575,7 @@ handle smart_holder_from_unique_ptr(std::unique_ptr &&src, } // Critical transfer-of-ownership section. This must stay together. self_life_support->deactivate_life_support(); - holder.reclaim_disowned(); + holder.reclaim_disowned(tinfo->get_memory_guarded_delete); (void) src.release(); // Critical section end. return existing_inst; @@ -742,7 +741,8 @@ struct load_helper : value_and_holder_helper { return std::shared_ptr(raw_ptr, shared_ptr_parent_life_support(parent.ptr())); } - std::shared_ptr load_as_shared_ptr(void *void_raw_ptr, + std::shared_ptr load_as_shared_ptr(const type_info *tinfo, + void *void_raw_ptr, handle responsible_parent = nullptr, // to support py::potentially_slicing_weak_ptr // with minimal added code complexity: @@ -763,7 +763,7 @@ struct load_helper : value_and_holder_helper { } auto *type_raw_ptr = static_cast(void_raw_ptr); if (python_instance_is_alias && !force_potentially_slicing_shared_ptr) { - auto *vptr_gd_ptr = std::get_deleter(hld.vptr); + auto *vptr_gd_ptr = tinfo->get_memory_guarded_delete(holder().vptr); if (vptr_gd_ptr != nullptr) { std::shared_ptr released_ptr = vptr_gd_ptr->released_ptr.lock(); if (released_ptr) { @@ -800,31 +800,32 @@ struct load_helper : value_and_holder_helper { } template - std::unique_ptr load_as_unique_ptr(void *raw_void_ptr, + std::unique_ptr load_as_unique_ptr(const type_info *tinfo, + void *raw_void_ptr, const char *context = "load_as_unique_ptr") { if (!have_holder()) { return unique_with_deleter(nullptr, std::unique_ptr()); } throw_if_uninitialized_or_disowned_holder(typeid(T)); - throw_if_instance_is_currently_owned_by_shared_ptr(); + throw_if_instance_is_currently_owned_by_shared_ptr(tinfo); holder().ensure_is_not_disowned(context); - holder().template ensure_compatible_rtti_uqp_del(context); + holder().template ensure_compatible_uqp_del(context); holder().ensure_use_count_1(context); T *raw_type_ptr = static_cast(raw_void_ptr); - auto *self_life_support - = dynamic_raw_ptr_cast_if_possible(raw_type_ptr); + auto *self_life_support = tinfo->get_trampoline_self_life_support(raw_type_ptr); // This is enforced indirectly by a static_assert in the class_ implementation: assert(!python_instance_is_alias || self_life_support); - std::unique_ptr extracted_deleter = holder().template extract_deleter(context); + std::unique_ptr extracted_deleter + = holder().template extract_deleter(context, tinfo->get_memory_guarded_delete); // Critical transfer-of-ownership section. This must stay together. if (self_life_support != nullptr) { - holder().disown(); + holder().disown(tinfo->get_memory_guarded_delete); } else { - holder().release_ownership(); + holder().release_ownership(tinfo->get_memory_guarded_delete); } auto result = unique_with_deleter(raw_type_ptr, std::move(extracted_deleter)); if (self_life_support != nullptr) { @@ -842,14 +843,17 @@ struct load_helper : value_and_holder_helper { // This assumes load_as_shared_ptr succeeded(), and the returned shared_ptr is still alive. // The returned unique_ptr is meant to never expire (the behavior is undefined otherwise). template - std::unique_ptr - load_as_const_unique_ptr(T *raw_type_ptr, const char *context = "load_as_const_unique_ptr") { + std::unique_ptr load_as_const_unique_ptr(const type_info *tinfo, + T *raw_type_ptr, + const char *context + = "load_as_const_unique_ptr") { if (!have_holder()) { return unique_with_deleter(nullptr, std::unique_ptr()); } - holder().template ensure_compatible_rtti_uqp_del(context); - return unique_with_deleter( - raw_type_ptr, std::move(holder().template extract_deleter(context))); + holder().template ensure_compatible_uqp_del(context); + return unique_with_deleter(raw_type_ptr, + std::move(holder().template extract_deleter( + context, tinfo->get_memory_guarded_delete))); } }; diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index e747e274d1..06be7f1d4f 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1571,6 +1571,7 @@ class generic_type : public object { tinfo->holder_size_in_ptrs = size_in_ptrs(rec.holder_size); tinfo->init_instance = rec.init_instance; tinfo->dealloc = rec.dealloc; + tinfo->get_trampoline_self_life_support = rec.get_trampoline_self_life_support; tinfo->simple_type = true; tinfo->simple_ancestors = true; tinfo->module_local = rec.module_local; @@ -2066,6 +2067,16 @@ class class_ : public detail::generic_type { record.dealloc = dealloc_without_manipulating_gil; } + if (std::is_base_of::value) { + // Store a cross-DSO-safe getter. + // This lambda is defined in the same DSO that instantiates + // class_, but it can be called safely from any other DSO. + record.get_trampoline_self_life_support = [](void *type_ptr) { + return dynamic_raw_ptr_cast_if_possible( + static_cast(type_ptr)); + }; + } + generic_type::initialize(record); if (has_alias) { diff --git a/include/pybind11/trampoline_self_life_support.h b/include/pybind11/trampoline_self_life_support.h index 484045bb17..cbfec7f974 100644 --- a/include/pybind11/trampoline_self_life_support.h +++ b/include/pybind11/trampoline_self_life_support.h @@ -19,6 +19,7 @@ PYBIND11_NAMESPACE_END(detail) // https://github.com/google/clif/blob/07f95d7e69dca2fcf7022978a55ef3acff506c19/clif/python/runtime.cc#L37 // URL provided here mainly to give proper credit. struct trampoline_self_life_support { + // NOTE: PYBIND11_INTERNALS_VERSION needs to be bumped if changes are made to this struct. detail::value_and_holder v_h; trampoline_self_life_support() = default; @@ -57,4 +58,8 @@ struct trampoline_self_life_support { trampoline_self_life_support &operator=(trampoline_self_life_support &&) = delete; }; +PYBIND11_NAMESPACE_BEGIN(detail) +using get_trampoline_self_life_support_fn = trampoline_self_life_support *(*) (void *); +PYBIND11_NAMESPACE_END(detail) + PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/tests/pure_cpp/smart_holder_poc.h b/tests/pure_cpp/smart_holder_poc.h index 37160f1e64..038cddc7ab 100644 --- a/tests/pure_cpp/smart_holder_poc.h +++ b/tests/pure_cpp/smart_holder_poc.h @@ -36,17 +36,17 @@ T *as_raw_ptr_release_ownership(smart_holder &hld, const char *context = "as_raw_ptr_release_ownership") { hld.ensure_can_release_ownership(context); T *raw_ptr = hld.as_raw_ptr_unowned(); - hld.release_ownership(); + hld.release_ownership(get_guarded_delete); return raw_ptr; } template > std::unique_ptr as_unique_ptr(smart_holder &hld) { static const char *context = "as_unique_ptr"; - hld.ensure_compatible_rtti_uqp_del(context); + hld.ensure_compatible_uqp_del(context); hld.ensure_use_count_1(context); T *raw_ptr = hld.as_raw_ptr_unowned(); - hld.release_ownership(); + hld.release_ownership(get_guarded_delete); // KNOWN DEFECT (see PR #4850): Does not copy the deleter. return std::unique_ptr(raw_ptr); } diff --git a/tests/pure_cpp/smart_holder_poc_test.cpp b/tests/pure_cpp/smart_holder_poc_test.cpp index 55018e65b2..09720432cf 100644 --- a/tests/pure_cpp/smart_holder_poc_test.cpp +++ b/tests/pure_cpp/smart_holder_poc_test.cpp @@ -14,6 +14,7 @@ #define CATCH_CONFIG_MAIN #include "catch.hpp" +using pybind11::memory::guarded_delete; using pybind11::memory::smart_holder; namespace poc = pybind11::memory::smart_holder_poc; @@ -160,11 +161,12 @@ TEST_CASE("from_raw_ptr_take_ownership+as_shared_ptr", "[S]") { TEST_CASE("from_raw_ptr_take_ownership+disown+reclaim_disowned", "[S]") { auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); std::unique_ptr new_owner(hld.as_raw_ptr_unowned()); - hld.disown(); + hld.disown(pybind11::memory::get_guarded_delete); REQUIRE(poc::as_lvalue_ref(hld) == 19); REQUIRE(*new_owner == 19); - hld.reclaim_disowned(); // Manually veriified: without this, clang++ -fsanitize=address reports - // "detected memory leaks". + // Manually verified: without this, clang++ -fsanitize=address reports + // "detected memory leaks". + hld.reclaim_disowned(pybind11::memory::get_guarded_delete); // NOLINTNEXTLINE(bugprone-unused-return-value) (void) new_owner.release(); // Manually verified: without this, clang++ -fsanitize=address // reports "attempting double-free". @@ -175,7 +177,7 @@ TEST_CASE("from_raw_ptr_take_ownership+disown+reclaim_disowned", "[S]") { TEST_CASE("from_raw_ptr_take_ownership+disown+release_disowned", "[S]") { auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); std::unique_ptr new_owner(hld.as_raw_ptr_unowned()); - hld.disown(); + hld.disown(pybind11::memory::get_guarded_delete); REQUIRE(poc::as_lvalue_ref(hld) == 19); REQUIRE(*new_owner == 19); hld.release_disowned(); @@ -187,7 +189,7 @@ TEST_CASE("from_raw_ptr_take_ownership+disown+ensure_is_not_disowned", "[E]") { auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); hld.ensure_is_not_disowned(context); // Does not throw. std::unique_ptr new_owner(hld.as_raw_ptr_unowned()); - hld.disown(); + hld.disown(pybind11::memory::get_guarded_delete); REQUIRE_THROWS_WITH(hld.ensure_is_not_disowned(context), "Holder was disowned already (test_case)."); } From 86e82ddbc218debbeadb6ec18763edd5e6261b74 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 17 Jun 2025 12:16:56 -0700 Subject: [PATCH 05/15] Add support for `shared_ptr` in `py::init()` with `smart_holder` (#5731) * Add overload to enable `.def(py::init(&rtrn_shcp))`. Also uncomment `.def(py::init(&rtrn_uqcp))` and `.def(py::init(&rtrn_udcp))`, which happen to work already (not sure what change in the past made those work). * Introduce `construct_from_shared_ptr()` helper for DRY-ness. --- include/pybind11/detail/init.h | 28 ++++++++++++++++---- tests/test_class_sh_factory_constructors.cpp | 14 +++------- tests/test_class_sh_factory_constructors.py | 6 ++--- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/include/pybind11/detail/init.h b/include/pybind11/detail/init.h index 913515c17b..9589d74d2a 100644 --- a/include/pybind11/detail/init.h +++ b/include/pybind11/detail/init.h @@ -246,20 +246,38 @@ void construct(value_and_holder &v_h, v_h.type->init_instance(v_h.inst, &smhldr); } -template >::value, int> = 0> -void construct(value_and_holder &v_h, std::shared_ptr> &&shd_ptr, bool need_alias) { - PYBIND11_WORKAROUND_INCORRECT_MSVC_C4100(need_alias); +template +void construct_from_shared_ptr(value_and_holder &v_h, + std::shared_ptr &&shd_ptr, + bool need_alias) { + static_assert(std::is_same>::value + || std::is_same>::value, + "Expected (const) Cpp as shared_ptr pointee"); auto *ptr = shd_ptr.get(); no_nullptr(ptr); if (Class::has_alias && need_alias && !is_alias(ptr)) { throw type_error("pybind11::init(): construction failed: returned std::shared_ptr pointee " "is not an alias instance"); } - auto smhldr = smart_holder::from_shared_ptr(shd_ptr); - v_h.value_ptr() = ptr; + // Cast to non-const if needed, consistent with internal design + auto smhldr + = smart_holder::from_shared_ptr(std::const_pointer_cast>(std::move(shd_ptr))); + v_h.value_ptr() = const_cast *>(ptr); v_h.type->init_instance(v_h.inst, &smhldr); } +template >::value, int> = 0> +void construct(value_and_holder &v_h, std::shared_ptr> &&shd_ptr, bool need_alias) { + construct_from_shared_ptr, Class>(v_h, std::move(shd_ptr), need_alias); +} + +template >::value, int> = 0> +void construct(value_and_holder &v_h, + std::shared_ptr> &&shd_ptr, + bool need_alias) { + construct_from_shared_ptr, Class>(v_h, std::move(shd_ptr), need_alias); +} + template >::value, int> = 0> void construct(value_and_holder &v_h, std::shared_ptr> &&shd_ptr, diff --git a/tests/test_class_sh_factory_constructors.cpp b/tests/test_class_sh_factory_constructors.cpp index 574ab26a2b..0718b569ff 100644 --- a/tests/test_class_sh_factory_constructors.cpp +++ b/tests/test_class_sh_factory_constructors.cpp @@ -108,10 +108,7 @@ TEST_SUBMODULE(class_sh_factory_constructors, m) { .def("get_mtxt", get_mtxt); py::classh(m, "atyp_shcp") - // py::class_>(m, "atyp_shcp") - // class_: ... must return a compatible ... - // classh: ... cannot pass object of non-trivial type ... - // .def(py::init(&rtrn_shcp)) + .def(py::init(&rtrn_shcp)) .def("get_mtxt", get_mtxt); py::classh(m, "atyp_uqmp") @@ -119,9 +116,7 @@ TEST_SUBMODULE(class_sh_factory_constructors, m) { .def("get_mtxt", get_mtxt); py::classh(m, "atyp_uqcp") - // class_: ... cannot pass object of non-trivial type ... - // classh: ... cannot pass object of non-trivial type ... - // .def(py::init(&rtrn_uqcp)) + .def(py::init(&rtrn_uqcp)) .def("get_mtxt", get_mtxt); py::classh(m, "atyp_udmp") @@ -129,10 +124,7 @@ TEST_SUBMODULE(class_sh_factory_constructors, m) { .def("get_mtxt", get_mtxt); py::classh(m, "atyp_udcp") - // py::class_>(m, "atyp_udcp") - // class_: ... must return a compatible ... - // classh: ... cannot pass object of non-trivial type ... - // .def(py::init(&rtrn_udcp)) + .def(py::init(&rtrn_udcp)) .def("get_mtxt", get_mtxt); py::classh(m, "with_alias") diff --git a/tests/test_class_sh_factory_constructors.py b/tests/test_class_sh_factory_constructors.py index 5d45db6fd5..6288c00124 100644 --- a/tests/test_class_sh_factory_constructors.py +++ b/tests/test_class_sh_factory_constructors.py @@ -13,11 +13,11 @@ def test_atyp_factories(): # sert m.atyp_cptr().get_mtxt() == "Cptr" assert m.atyp_mptr().get_mtxt() == "Mptr" assert m.atyp_shmp().get_mtxt() == "Shmp" - # sert m.atyp_shcp().get_mtxt() == "Shcp" + assert m.atyp_shcp().get_mtxt() == "Shcp" assert m.atyp_uqmp().get_mtxt() == "Uqmp" - # sert m.atyp_uqcp().get_mtxt() == "Uqcp" + assert m.atyp_uqcp().get_mtxt() == "Uqcp" assert m.atyp_udmp().get_mtxt() == "Udmp" - # sert m.atyp_udcp().get_mtxt() == "Udcp" + assert m.atyp_udcp().get_mtxt() == "Udcp" @pytest.mark.parametrize( From 2e0662ab140fd213fbe5b21dc13e93a5a8544f9b Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 13 Jun 2025 02:10:49 -0400 Subject: [PATCH 06/15] feat: add NumPy scalars Co-authored-by: Steve R. Sun <1638650145@qq.com> Signed-off-by: Henry Schreiner --- docs/advanced/pycpp/numpy.rst | 40 +++++++ include/pybind11/numpy.h | 196 +++++++++++++++++++++++++++------- tests/CMakeLists.txt | 1 + tests/test_numpy_scalars.cpp | 52 +++++++++ tests/test_numpy_scalars.py | 62 +++++++++++ 5 files changed, 315 insertions(+), 36 deletions(-) create mode 100644 tests/test_numpy_scalars.cpp create mode 100644 tests/test_numpy_scalars.py diff --git a/docs/advanced/pycpp/numpy.rst b/docs/advanced/pycpp/numpy.rst index d09a2cea2c..29638eb821 100644 --- a/docs/advanced/pycpp/numpy.rst +++ b/docs/advanced/pycpp/numpy.rst @@ -232,6 +232,46 @@ prevent many types of unsupported structures, it is still the user's responsibility to use only "plain" structures that can be safely manipulated as raw memory without violating invariants. +Scalar types +============ + +In some cases we may want to accept or return NumPy scalar values such as +``np.float32`` or ``np.float64``. We hope to be able to handle single-precision +and double-precision on the C-side. However, both are bound to Python's +double-precision builtin float by default, so they cannot be processed separately. +We used the ``py::buffer`` trick to implement the previous approach, which +will cause the readability of the code to drop significantly. + +Luckily, there's a helper type for this occasion - ``py::numpy_scalar``: + +.. code-block:: cpp + + m.def("add", [](py::numpy_scalar a, py::numpy_scalar b) { + return py::make_scalar(a + b); + }); + m.def("add", [](py::numpy_scalar a, py::numpy_scalar b) { + return py::make_scalar(a + b); + }); + +This type is trivially convertible to and from the type it wraps; currently +supported scalar types are NumPy arithmetic types: ``bool_``, ``int8``, +``int16``, ``int32``, ``int64``, ``uint8``, ``uint16``, ``uint32``, +``uint64``, ``float32``, ``float64``, ``complex64``, ``complex128``, all of +them mapping to respective C++ counterparts. + +.. note:: + + This is a strict type, it will only allows to specify NumPy type as input + arguments, and does not allow other types of input parameters (e.g., + ``py::numpy_scalar`` will not accept Python's builtin ``int`` ). + +.. note:: + + Native C types are mapped to NumPy types in a platform specific way: for + instance, ``char`` may be mapped to either ``np.int8`` or ``np.uint8`` + and ``long`` may use 4 or 8 bytes depending on the platform. Unless you + clearly understand the difference and your needs, please use ````. + Vectorizing functions ===================== diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index f65b5c9d7a..1e971fb311 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -49,6 +49,9 @@ PYBIND11_WARNING_DISABLE_MSVC(4127) class dtype; // Forward declaration class array; // Forward declaration +template +struct numpy_scalar; // Forward declaration + PYBIND11_NAMESPACE_BEGIN(detail) template <> @@ -245,6 +248,21 @@ struct npy_api { NPY_UINT64_ = platform_lookup( NPY_ULONG_, NPY_ULONGLONG_, NPY_UINT_), + NPY_FLOAT32_ = platform_lookup( + NPY_DOUBLE_, NPY_FLOAT_, NPY_LONGDOUBLE_), + NPY_FLOAT64_ = platform_lookup( + NPY_DOUBLE_, NPY_FLOAT_, NPY_LONGDOUBLE_), + NPY_COMPLEX64_ + = platform_lookup, + std::complex, + std::complex, + std::complex>(NPY_DOUBLE_, NPY_FLOAT_, NPY_LONGDOUBLE_), + NPY_COMPLEX128_ + = platform_lookup, + std::complex, + std::complex, + std::complex>(NPY_DOUBLE_, NPY_FLOAT_, NPY_LONGDOUBLE_), + NPY_CHAR_ = std::is_signed::value ? NPY_BYTE_ : NPY_UBYTE_, }; unsigned int PyArray_RUNTIME_VERSION_; @@ -268,6 +286,7 @@ struct npy_api { unsigned int (*PyArray_GetNDArrayCFeatureVersion_)(); PyObject *(*PyArray_DescrFromType_)(int); + PyObject *(*PyArray_TypeObjectFromType_)(int); PyObject *(*PyArray_NewFromDescr_)(PyTypeObject *, PyObject *, int, @@ -284,6 +303,8 @@ struct npy_api { PyTypeObject *PyVoidArrType_Type_; PyTypeObject *PyArrayDescr_Type_; PyObject *(*PyArray_DescrFromScalar_)(PyObject *); + PyObject *(*PyArray_Scalar_)(void *, PyObject *, PyObject *); + void (*PyArray_ScalarAsCtype_)(PyObject *, void *); PyObject *(*PyArray_FromAny_)(PyObject *, PyObject *, int, int, int, PyObject *); int (*PyArray_DescrConverter_)(PyObject *, PyObject **); bool (*PyArray_EquivTypes_)(PyObject *, PyObject *); @@ -301,7 +322,10 @@ struct npy_api { API_PyArrayDescr_Type = 3, API_PyVoidArrType_Type = 39, API_PyArray_DescrFromType = 45, + API_PyArray_TypeObjectFromType = 46, API_PyArray_DescrFromScalar = 57, + API_PyArray_Scalar = 60, + API_PyArray_ScalarAsCtype = 62, API_PyArray_FromAny = 69, API_PyArray_Resize = 80, // CopyInto was slot 82 and 50 was effectively an alias. NumPy 2 removed 82. @@ -336,7 +360,10 @@ struct npy_api { DECL_NPY_API(PyVoidArrType_Type); DECL_NPY_API(PyArrayDescr_Type); DECL_NPY_API(PyArray_DescrFromType); + DECL_NPY_API(PyArray_TypeObjectFromType); DECL_NPY_API(PyArray_DescrFromScalar); + DECL_NPY_API(PyArray_Scalar); + DECL_NPY_API(PyArray_ScalarAsCtype); DECL_NPY_API(PyArray_FromAny); DECL_NPY_API(PyArray_Resize); DECL_NPY_API(PyArray_CopyInto); @@ -355,6 +382,88 @@ struct npy_api { } }; +template +struct is_complex : std::false_type {}; +template +struct is_complex> : std::true_type {}; + +template +struct npy_format_descriptor_name; + +template +struct npy_format_descriptor_name::value>> { + static constexpr auto name = const_name::value>( + const_name("bool"), + const_name::value>("int", "uint") + const_name()); +}; + +template +struct npy_format_descriptor_name::value>> { + static constexpr auto name + = const_name < std::is_same::value + || std::is_same::value + > (const_name("float") + const_name(), const_name("longdouble")); +}; + +template +struct npy_format_descriptor_name::value>> { + static constexpr auto name + = const_name < std::is_same::value + || std::is_same::value + > (const_name("complex") + const_name(), + const_name("longcomplex")); +}; + +template +struct numpy_scalar_info {}; + +#define DECL_NPY_SCALAR(ctype_, typenum_) \ + template <> \ + struct numpy_scalar_info { \ + static constexpr auto name = npy_format_descriptor_name::name; \ + static constexpr int typenum = npy_api::typenum_##_; \ + } + +// boolean type +DECL_NPY_SCALAR(bool, NPY_BOOL); + +// character types +DECL_NPY_SCALAR(char, NPY_CHAR); +DECL_NPY_SCALAR(signed char, NPY_BYTE); +DECL_NPY_SCALAR(unsigned char, NPY_UBYTE); + +// signed integer types +DECL_NPY_SCALAR(std::int16_t, NPY_SHORT); +DECL_NPY_SCALAR(std::int32_t, NPY_INT); +DECL_NPY_SCALAR(std::int64_t, NPY_LONG); +#if defined(__linux__) +DECL_NPY_SCALAR(long long, NPY_LONG); +#else +DECL_NPY_SCALAR(long, NPY_LONG); +#endif + +// unsigned integer types +DECL_NPY_SCALAR(std::uint16_t, NPY_USHORT); +DECL_NPY_SCALAR(std::uint32_t, NPY_UINT); +DECL_NPY_SCALAR(std::uint64_t, NPY_ULONG); +#if defined(__linux__) +DECL_NPY_SCALAR(unsigned long long, NPY_ULONG); +#else +DECL_NPY_SCALAR(unsigned long, NPY_ULONG); +#endif + +// floating point types +DECL_NPY_SCALAR(float, NPY_FLOAT); +DECL_NPY_SCALAR(double, NPY_DOUBLE); +DECL_NPY_SCALAR(long double, NPY_LONGDOUBLE); + +// complex types +DECL_NPY_SCALAR(std::complex, NPY_CFLOAT); +DECL_NPY_SCALAR(std::complex, NPY_CDOUBLE); +DECL_NPY_SCALAR(std::complex, NPY_CLONGDOUBLE); + +#undef DECL_NPY_SCALAR + // This table normalizes typenums by mapping NPY_INT_, NPY_LONG, ... to NPY_INT32_, NPY_INT64, ... // This is needed to correctly handle situations where multiple typenums map to the same type, // e.g. NPY_LONG_ may be equivalent to NPY_INT_ or NPY_LONGLONG_ despite having a different @@ -453,10 +562,6 @@ template struct is_std_array : std::false_type {}; template struct is_std_array> : std::true_type {}; -template -struct is_complex : std::false_type {}; -template -struct is_complex> : std::true_type {}; template struct array_info_scalar { @@ -670,8 +775,59 @@ template struct type_caster> : type_caster> {}; +template +struct type_caster> { + using value_type = T; + using type_info = numpy_scalar_info; + + PYBIND11_TYPE_CASTER(numpy_scalar, type_info::name); + + static handle &target_type() { + static handle tp = npy_api::get().PyArray_TypeObjectFromType_(type_info::typenum); + return tp; + } + + static handle &target_dtype() { + static handle tp = npy_api::get().PyArray_DescrFromType_(type_info::typenum); + return tp; + } + + bool load(handle src, bool) { + if (isinstance(src, target_type())) { + npy_api::get().PyArray_ScalarAsCtype_(src.ptr(), &value.value); + return true; + } + return false; + } + + static handle cast(numpy_scalar src, return_value_policy, handle) { + return npy_api::get().PyArray_Scalar_(&src.value, target_dtype().ptr(), nullptr); + } +}; + PYBIND11_NAMESPACE_END(detail) +template +struct numpy_scalar { + using value_type = T; + + value_type value; + + numpy_scalar() = default; + numpy_scalar(value_type value) : value(value) {} + + operator value_type() { return value; } + numpy_scalar &operator=(value_type value) { + this->value = value; + return *this; + } +}; + +template +numpy_scalar make_scalar(T value) { + return numpy_scalar(value); +} + class dtype : public object { public: PYBIND11_OBJECT_DEFAULT(dtype, object, detail::npy_api::get().PyArrayDescr_Check_) @@ -1409,38 +1565,6 @@ struct compare_buffer_info::valu } }; -template -struct npy_format_descriptor_name; - -template -struct npy_format_descriptor_name::value>> { - static constexpr auto name = const_name::value>( - const_name("bool"), - const_name::value>("numpy.int", "numpy.uint") - + const_name()); -}; - -template -struct npy_format_descriptor_name::value>> { - static constexpr auto name = const_name < std::is_same::value - || std::is_same::value - || std::is_same::value - || std::is_same::value - > (const_name("numpy.float") + const_name(), - const_name("numpy.longdouble")); -}; - -template -struct npy_format_descriptor_name::value>> { - static constexpr auto name = const_name < std::is_same::value - || std::is_same::value - || std::is_same::value - || std::is_same::value - > (const_name("numpy.complex") - + const_name(), - const_name("numpy.longcomplex")); -}; - template struct npy_format_descriptor< T, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2cf18c3547..ebd3fff1c2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -159,6 +159,7 @@ set(PYBIND11_TEST_FILES test_native_enum test_numpy_array test_numpy_dtypes + test_numpy_scalars test_numpy_vectorize test_opaque_types test_operator_overloading diff --git a/tests/test_numpy_scalars.cpp b/tests/test_numpy_scalars.cpp new file mode 100644 index 0000000000..046a9c07a9 --- /dev/null +++ b/tests/test_numpy_scalars.cpp @@ -0,0 +1,52 @@ +/* + tests/test_numpy_scalars.cpp -- strict NumPy scalars + + Copyright (c) 2021 Steve R. Sun + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#include + +#include "pybind11_tests.h" + +#include +#include + +namespace py = pybind11; + +template +struct add { + T x; + add(T x) : x(x) {} + T operator()(T y) const { return static_cast(x + y); } +}; + +template +void register_test(py::module &m, const char *name, F &&func) { + m.def((std::string("test_") + name).c_str(), + [=](py::numpy_scalar v) { + return std::make_tuple(name, py::make_scalar(static_cast(func(v.value)))); + }, + py::arg("x")); +} + +TEST_SUBMODULE(numpy_scalars, m) { + using cfloat = std::complex; + using cdouble = std::complex; + + register_test(m, "bool", [](bool x) { return !x; }); + register_test(m, "int8", add(-8)); + register_test(m, "int16", add(-16)); + register_test(m, "int32", add(-32)); + register_test(m, "int64", add(-64)); + register_test(m, "uint8", add(8)); + register_test(m, "uint16", add(16)); + register_test(m, "uint32", add(32)); + register_test(m, "uint64", add(64)); + register_test(m, "float32", add(0.125f)); + register_test(m, "float64", add(0.25f)); + register_test(m, "complex64", add({0, -0.125f})); + register_test(m, "complex128", add({0, -0.25f})); +} diff --git a/tests/test_numpy_scalars.py b/tests/test_numpy_scalars.py new file mode 100644 index 0000000000..52c2861a1c --- /dev/null +++ b/tests/test_numpy_scalars.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import sys + +import pytest + +from pybind11_tests import numpy_scalars as m + +np = pytest.importorskip("numpy") + +SCALAR_TYPES = { + np.bool_: False, + np.int8: -7, + np.int16: -15, + np.int32: -31, + np.int64: -63, + np.uint8: 9, + np.uint16: 17, + np.uint32: 33, + np.uint64: 65, + np.single: 1.125, + np.double: 1.25, + np.complex64: 1 - 0.125j, + np.complex128: 1 - 0.25j, +} +ALL_TYPES = [int, bool, float, bytes, str] + list(SCALAR_TYPES) + + +def type_name(tp): + try: + return tp.__name__.rstrip("_") + except BaseException: + # no numpy + return str(tp) + + +@pytest.fixture(scope="module", params=list(SCALAR_TYPES), ids=type_name) +def scalar_type(request): + return request.param + + +def expected_signature(tp): + s = "str" if sys.version_info[0] >= 3 else "unicode" + t = type_name(tp) + return f"test_{t}(x: {t}) -> tuple[{s}, {t}]\n" + + +def test_numpy_scalars(scalar_type): + expected = SCALAR_TYPES[scalar_type] + name = type_name(scalar_type) + func = getattr(m, "test_" + name) + assert func.__doc__ == expected_signature(scalar_type) + for tp in ALL_TYPES: + value = tp(1) + if tp is scalar_type: + result = func(value) + assert result[0] == name + assert isinstance(result[1], tp) + assert result[1] == tp(expected) + else: + with pytest.raises(TypeError): + func(value) From d435a02e55c0a4b624ee55a0731f8eb4ef9d1c68 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 13 Jun 2025 12:12:01 -0400 Subject: [PATCH 07/15] fix: fixes to make the tests pass --- include/pybind11/numpy.h | 27 ++++++++++++++++----------- tests/test_numpy_scalars.py | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index 1e971fb311..988d8cccac 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -393,25 +393,30 @@ struct npy_format_descriptor_name; template struct npy_format_descriptor_name::value>> { static constexpr auto name = const_name::value>( - const_name("bool"), - const_name::value>("int", "uint") + const_name()); + const_name("numpy.bool"), + const_name::value>("numpy.int", "numpy.uint") + + const_name()); }; template struct npy_format_descriptor_name::value>> { - static constexpr auto name - = const_name < std::is_same::value - || std::is_same::value - > (const_name("float") + const_name(), const_name("longdouble")); + static constexpr auto name = const_name < std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value + > (const_name("numpy.float") + const_name(), + const_name("numpy.longdouble")); }; template struct npy_format_descriptor_name::value>> { - static constexpr auto name - = const_name < std::is_same::value - || std::is_same::value - > (const_name("complex") + const_name(), - const_name("longcomplex")); + static constexpr auto name = const_name < std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value + > (const_name("numpy.complex") + + const_name(), + const_name("numpy.longcomplex")); }; template diff --git a/tests/test_numpy_scalars.py b/tests/test_numpy_scalars.py index 52c2861a1c..020465f2d1 100644 --- a/tests/test_numpy_scalars.py +++ b/tests/test_numpy_scalars.py @@ -42,7 +42,7 @@ def scalar_type(request): def expected_signature(tp): s = "str" if sys.version_info[0] >= 3 else "unicode" t = type_name(tp) - return f"test_{t}(x: {t}) -> tuple[{s}, {t}]\n" + return f"test_{t}(x: numpy.{t}) -> tuple[{s}, numpy.{t}]\n" def test_numpy_scalars(scalar_type): From e5e6522e5bfc4f0288e7d7d170cfdfa2683f0c22 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 13 Jun 2025 15:31:56 -0400 Subject: [PATCH 08/15] fix: use simpler definitions for ints --- include/pybind11/numpy.h | 26 ++++++++------------------ tests/test_numpy_scalars.cpp | 2 +- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index 988d8cccac..cdfdcf85dc 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -438,24 +438,14 @@ DECL_NPY_SCALAR(signed char, NPY_BYTE); DECL_NPY_SCALAR(unsigned char, NPY_UBYTE); // signed integer types -DECL_NPY_SCALAR(std::int16_t, NPY_SHORT); -DECL_NPY_SCALAR(std::int32_t, NPY_INT); -DECL_NPY_SCALAR(std::int64_t, NPY_LONG); -#if defined(__linux__) -DECL_NPY_SCALAR(long long, NPY_LONG); -#else -DECL_NPY_SCALAR(long, NPY_LONG); -#endif +DECL_NPY_SCALAR(std::int16_t, NPY_INT16); +DECL_NPY_SCALAR(std::int32_t, NPY_INT32); +DECL_NPY_SCALAR(std::int64_t, NPY_INT64); // unsigned integer types -DECL_NPY_SCALAR(std::uint16_t, NPY_USHORT); -DECL_NPY_SCALAR(std::uint32_t, NPY_UINT); -DECL_NPY_SCALAR(std::uint64_t, NPY_ULONG); -#if defined(__linux__) -DECL_NPY_SCALAR(unsigned long long, NPY_ULONG); -#else -DECL_NPY_SCALAR(unsigned long, NPY_ULONG); -#endif +DECL_NPY_SCALAR(std::uint16_t, NPY_UINT16); +DECL_NPY_SCALAR(std::uint32_t, NPY_UINT32); +DECL_NPY_SCALAR(std::uint64_t, NPY_UINT64); // floating point types DECL_NPY_SCALAR(float, NPY_FLOAT); @@ -819,9 +809,9 @@ struct numpy_scalar { value_type value; numpy_scalar() = default; - numpy_scalar(value_type value) : value(value) {} + explicit numpy_scalar(value_type value) : value(value) {} - operator value_type() { return value; } + explicit operator value_type() { return value; } numpy_scalar &operator=(value_type value) { this->value = value; return *this; diff --git a/tests/test_numpy_scalars.cpp b/tests/test_numpy_scalars.cpp index 046a9c07a9..abedc0045f 100644 --- a/tests/test_numpy_scalars.cpp +++ b/tests/test_numpy_scalars.cpp @@ -19,7 +19,7 @@ namespace py = pybind11; template struct add { T x; - add(T x) : x(x) {} + explicit add(T x) : x(x) {} T operator()(T y) const { return static_cast(x + y); } }; From 8379d254784ccb8fcb6b7f920851b94b0715de55 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 17 Jun 2025 15:21:34 -0400 Subject: [PATCH 09/15] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/advanced/pycpp/numpy.rst | 2 +- include/pybind11/numpy.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced/pycpp/numpy.rst b/docs/advanced/pycpp/numpy.rst index 29638eb821..5293610a72 100644 --- a/docs/advanced/pycpp/numpy.rst +++ b/docs/advanced/pycpp/numpy.rst @@ -261,7 +261,7 @@ them mapping to respective C++ counterparts. .. note:: - This is a strict type, it will only allows to specify NumPy type as input + This is a strict type, it will only allow to specify NumPy type as input arguments, and does not allow other types of input parameters (e.g., ``py::numpy_scalar`` will not accept Python's builtin ``int`` ). diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index cdfdcf85dc..318aaaefc4 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -811,7 +811,7 @@ struct numpy_scalar { numpy_scalar() = default; explicit numpy_scalar(value_type value) : value(value) {} - explicit operator value_type() { return value; } + explicit operator value_type() const { return value; } numpy_scalar &operator=(value_type value) { this->value = value; return *this; From f928a5467bdd532873b666850ddf2675b186b6e9 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 Jun 2025 14:06:34 -0700 Subject: [PATCH 10/15] Modernize test_numpy_scalars.py --- tests/test_numpy_scalars.py | 53 +++++++++++++------------------------ 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/tests/test_numpy_scalars.py b/tests/test_numpy_scalars.py index 020465f2d1..ca9e3a63b6 100644 --- a/tests/test_numpy_scalars.py +++ b/tests/test_numpy_scalars.py @@ -1,14 +1,12 @@ from __future__ import annotations -import sys - import pytest from pybind11_tests import numpy_scalars as m np = pytest.importorskip("numpy") -SCALAR_TYPES = { +NPY_SCALAR_TYPES = { np.bool_: False, np.int8: -7, np.int16: -15, @@ -23,40 +21,27 @@ np.complex64: 1 - 0.125j, np.complex128: 1 - 0.25j, } -ALL_TYPES = [int, bool, float, bytes, str] + list(SCALAR_TYPES) - - -def type_name(tp): - try: - return tp.__name__.rstrip("_") - except BaseException: - # no numpy - return str(tp) - - -@pytest.fixture(scope="module", params=list(SCALAR_TYPES), ids=type_name) -def scalar_type(request): - return request.param - -def expected_signature(tp): - s = "str" if sys.version_info[0] >= 3 else "unicode" - t = type_name(tp) - return f"test_{t}(x: numpy.{t}) -> tuple[{s}, numpy.{t}]\n" +ALL_SCALAR_TYPES = tuple(NPY_SCALAR_TYPES.keys()) + (int, bool, float, bytes, str) -def test_numpy_scalars(scalar_type): - expected = SCALAR_TYPES[scalar_type] - name = type_name(scalar_type) - func = getattr(m, "test_" + name) - assert func.__doc__ == expected_signature(scalar_type) - for tp in ALL_TYPES: +@pytest.mark.parametrize( + ("npy_scalar_type", "expected_value"), NPY_SCALAR_TYPES.items() +) +def test_numpy_scalars(npy_scalar_type, expected_value): + tpnm = npy_scalar_type.__name__.rstrip("_") + test_tpnm = getattr(m, "test_" + tpnm) + assert ( + test_tpnm.__doc__ + == f"test_{tpnm}(x: numpy.{tpnm}) -> tuple[str, numpy.{tpnm}]\n" + ) + for tp in ALL_SCALAR_TYPES: value = tp(1) - if tp is scalar_type: - result = func(value) - assert result[0] == name - assert isinstance(result[1], tp) - assert result[1] == tp(expected) + if tp is npy_scalar_type: + result_tpnm, result_value = test_tpnm(value) + assert result_tpnm == tpnm + assert isinstance(result_value, npy_scalar_type) + assert result_value == tp(expected_value) else: with pytest.raises(TypeError): - func(value) + test_tpnm(value) From 437f93117d2db46a3323a3e3a000d1486959170f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 Jun 2025 14:08:22 -0700 Subject: [PATCH 11/15] Apply doc change suggested in review. --- docs/advanced/pycpp/numpy.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/advanced/pycpp/numpy.rst b/docs/advanced/pycpp/numpy.rst index 5293610a72..0c0447667a 100644 --- a/docs/advanced/pycpp/numpy.rst +++ b/docs/advanced/pycpp/numpy.rst @@ -261,9 +261,9 @@ them mapping to respective C++ counterparts. .. note:: - This is a strict type, it will only allow to specify NumPy type as input - arguments, and does not allow other types of input parameters (e.g., - ``py::numpy_scalar`` will not accept Python's builtin ``int`` ). + ``py::numpy_scalar`` strictly matches NumPy scalar types. For example, + ``py::numpy_scalar`` will accept ``np.int64(123)``, + but **not** a regular Python ``int`` like ``123``. .. note:: From b066f5f18e935ef5e4b172c0b3b981ea6f9c2ef5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 Jun 2025 14:11:50 -0700 Subject: [PATCH 12/15] =?UTF-8?q?Change=20DECL=5FNPY=5FSCALAR=20=E2=86=92?= =?UTF-8?q?=20PYBIND11=5FNUMPY=5FSCALAR=5FIMPL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/pybind11/numpy.h | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index 318aaaefc4..cd84ddc417 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -422,7 +422,7 @@ struct npy_format_descriptor_name::value>> { template struct numpy_scalar_info {}; -#define DECL_NPY_SCALAR(ctype_, typenum_) \ +#define PYBIND11_NUMPY_SCALAR_IMPL(ctype_, typenum_) \ template <> \ struct numpy_scalar_info { \ static constexpr auto name = npy_format_descriptor_name::name; \ @@ -430,34 +430,34 @@ struct numpy_scalar_info {}; } // boolean type -DECL_NPY_SCALAR(bool, NPY_BOOL); +PYBIND11_NUMPY_SCALAR_IMPL(bool, NPY_BOOL); // character types -DECL_NPY_SCALAR(char, NPY_CHAR); -DECL_NPY_SCALAR(signed char, NPY_BYTE); -DECL_NPY_SCALAR(unsigned char, NPY_UBYTE); +PYBIND11_NUMPY_SCALAR_IMPL(char, NPY_CHAR); +PYBIND11_NUMPY_SCALAR_IMPL(signed char, NPY_BYTE); +PYBIND11_NUMPY_SCALAR_IMPL(unsigned char, NPY_UBYTE); // signed integer types -DECL_NPY_SCALAR(std::int16_t, NPY_INT16); -DECL_NPY_SCALAR(std::int32_t, NPY_INT32); -DECL_NPY_SCALAR(std::int64_t, NPY_INT64); +PYBIND11_NUMPY_SCALAR_IMPL(std::int16_t, NPY_INT16); +PYBIND11_NUMPY_SCALAR_IMPL(std::int32_t, NPY_INT32); +PYBIND11_NUMPY_SCALAR_IMPL(std::int64_t, NPY_INT64); // unsigned integer types -DECL_NPY_SCALAR(std::uint16_t, NPY_UINT16); -DECL_NPY_SCALAR(std::uint32_t, NPY_UINT32); -DECL_NPY_SCALAR(std::uint64_t, NPY_UINT64); +PYBIND11_NUMPY_SCALAR_IMPL(std::uint16_t, NPY_UINT16); +PYBIND11_NUMPY_SCALAR_IMPL(std::uint32_t, NPY_UINT32); +PYBIND11_NUMPY_SCALAR_IMPL(std::uint64_t, NPY_UINT64); // floating point types -DECL_NPY_SCALAR(float, NPY_FLOAT); -DECL_NPY_SCALAR(double, NPY_DOUBLE); -DECL_NPY_SCALAR(long double, NPY_LONGDOUBLE); +PYBIND11_NUMPY_SCALAR_IMPL(float, NPY_FLOAT); +PYBIND11_NUMPY_SCALAR_IMPL(double, NPY_DOUBLE); +PYBIND11_NUMPY_SCALAR_IMPL(long double, NPY_LONGDOUBLE); // complex types -DECL_NPY_SCALAR(std::complex, NPY_CFLOAT); -DECL_NPY_SCALAR(std::complex, NPY_CDOUBLE); -DECL_NPY_SCALAR(std::complex, NPY_CLONGDOUBLE); +PYBIND11_NUMPY_SCALAR_IMPL(std::complex, NPY_CFLOAT); +PYBIND11_NUMPY_SCALAR_IMPL(std::complex, NPY_CDOUBLE); +PYBIND11_NUMPY_SCALAR_IMPL(std::complex, NPY_CLONGDOUBLE); -#undef DECL_NPY_SCALAR +#undef PYBIND11_NUMPY_SCALAR_IMPL // This table normalizes typenums by mapping NPY_INT_, NPY_LONG, ... to NPY_INT32_, NPY_INT64, ... // This is needed to correctly handle situations where multiple typenums map to the same type, From ac71c07daa8b7bb5bed902b4b50134bed449f354 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 Jun 2025 14:22:00 -0700 Subject: [PATCH 13/15] Add numpy_scalar operator==, operator!= --- include/pybind11/numpy.h | 6 ++++++ tests/test_numpy_scalars.cpp | 3 +++ tests/test_numpy_scalars.py | 7 +++++++ 3 files changed, 16 insertions(+) diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index cd84ddc417..7f62157f5c 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -816,6 +816,12 @@ struct numpy_scalar { this->value = value; return *this; } + + friend bool operator==(const numpy_scalar &a, const numpy_scalar &b) { + return a.value == b.value; + } + + friend bool operator!=(const numpy_scalar &a, const numpy_scalar &b) { return !(a == b); } }; template diff --git a/tests/test_numpy_scalars.cpp b/tests/test_numpy_scalars.cpp index abedc0045f..6d73518188 100644 --- a/tests/test_numpy_scalars.cpp +++ b/tests/test_numpy_scalars.cpp @@ -49,4 +49,7 @@ TEST_SUBMODULE(numpy_scalars, m) { register_test(m, "float64", add(0.25f)); register_test(m, "complex64", add({0, -0.125f})); register_test(m, "complex128", add({0, -0.25f})); + + m.def("test_eq", [](py::numpy_scalar a, py::numpy_scalar b) { return a == b; }); + m.def("test_ne", [](py::numpy_scalar a, py::numpy_scalar b) { return a != b; }); } diff --git a/tests/test_numpy_scalars.py b/tests/test_numpy_scalars.py index ca9e3a63b6..fe9b71f22e 100644 --- a/tests/test_numpy_scalars.py +++ b/tests/test_numpy_scalars.py @@ -45,3 +45,10 @@ def test_numpy_scalars(npy_scalar_type, expected_value): else: with pytest.raises(TypeError): test_tpnm(value) + + +def test_eq_ne(): + assert m.test_eq(np.int32(3), np.int32(3)) + assert not m.test_eq(np.int32(3), np.int32(5)) + assert not m.test_ne(np.int32(3), np.int32(3)) + assert m.test_ne(np.int32(3), np.int32(5)) From c38245a96c8f1078d16418880e46660a63e6107f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 Jun 2025 14:24:07 -0700 Subject: [PATCH 14/15] Move C++ test code into namespace pybind11_test_numpy_scalars --- tests/test_numpy_scalars.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_numpy_scalars.cpp b/tests/test_numpy_scalars.cpp index 6d73518188..739e6a15c8 100644 --- a/tests/test_numpy_scalars.cpp +++ b/tests/test_numpy_scalars.cpp @@ -16,6 +16,8 @@ namespace py = pybind11; +namespace pybind11_test_numpy_scalars { + template struct add { T x; @@ -32,6 +34,10 @@ void register_test(py::module &m, const char *name, F &&func) { py::arg("x")); } +} // namespace pybind11_test_numpy_scalars + +using namespace pybind11_test_numpy_scalars; + TEST_SUBMODULE(numpy_scalars, m) { using cfloat = std::complex; using cdouble = std::complex; From 59e2a6204f410c038762d27bad483be1efbbd883 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 Jun 2025 14:32:54 -0700 Subject: [PATCH 15/15] =?UTF-8?q?Fix=20oversight=20(int=20=E2=86=92=20int3?= =?UTF-8?q?2=5Ft)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_numpy_scalars.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_numpy_scalars.cpp b/tests/test_numpy_scalars.cpp index 739e6a15c8..79393ebdd3 100644 --- a/tests/test_numpy_scalars.cpp +++ b/tests/test_numpy_scalars.cpp @@ -56,6 +56,8 @@ TEST_SUBMODULE(numpy_scalars, m) { register_test(m, "complex64", add({0, -0.125f})); register_test(m, "complex128", add({0, -0.25f})); - m.def("test_eq", [](py::numpy_scalar a, py::numpy_scalar b) { return a == b; }); - m.def("test_ne", [](py::numpy_scalar a, py::numpy_scalar b) { return a != b; }); + m.def("test_eq", + [](py::numpy_scalar a, py::numpy_scalar b) { return a == b; }); + m.def("test_ne", + [](py::numpy_scalar a, py::numpy_scalar b) { return a != b; }); }