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 }}" 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') ================================== diff --git a/docs/advanced/pycpp/numpy.rst b/docs/advanced/pycpp/numpy.rst index d09a2cea2c..0c0447667a 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:: + + ``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:: + + 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/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/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/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/numpy.h b/include/pybind11/numpy.h index f65b5c9d7a..7f62157f5c 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,83 @@ 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("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 + || 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 numpy_scalar_info {}; + +#define PYBIND11_NUMPY_SCALAR_IMPL(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 +PYBIND11_NUMPY_SCALAR_IMPL(bool, NPY_BOOL); + +// character types +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 +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 +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 +PYBIND11_NUMPY_SCALAR_IMPL(float, NPY_FLOAT); +PYBIND11_NUMPY_SCALAR_IMPL(double, NPY_DOUBLE); +PYBIND11_NUMPY_SCALAR_IMPL(long double, NPY_LONGDOUBLE); + +// complex types +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 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, // e.g. NPY_LONG_ may be equivalent to NPY_INT_ or NPY_LONGLONG_ despite having a different @@ -453,10 +557,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 +770,65 @@ 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; + explicit numpy_scalar(value_type value) : value(value) {} + + explicit operator value_type() const { return value; } + numpy_scalar &operator=(value_type value) { + 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 +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 +1566,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/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/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/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)."); } 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( diff --git a/tests/test_numpy_scalars.cpp b/tests/test_numpy_scalars.cpp new file mode 100644 index 0000000000..79393ebdd3 --- /dev/null +++ b/tests/test_numpy_scalars.cpp @@ -0,0 +1,63 @@ +/* + 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; + +namespace pybind11_test_numpy_scalars { + +template +struct add { + T x; + explicit 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")); +} + +} // namespace pybind11_test_numpy_scalars + +using namespace pybind11_test_numpy_scalars; + +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})); + + 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 new file mode 100644 index 0000000000..fe9b71f22e --- /dev/null +++ b/tests/test_numpy_scalars.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import pytest + +from pybind11_tests import numpy_scalars as m + +np = pytest.importorskip("numpy") + +NPY_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_SCALAR_TYPES = tuple(NPY_SCALAR_TYPES.keys()) + (int, bool, float, bytes, str) + + +@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 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): + 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)) 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"