diff --git a/CMakeLists.txt b/CMakeLists.txt index 097b4eba25..bca0c22dc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,7 @@ set(PYBIND11_HEADERS include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h include/pybind11/detail/exception_translation.h include/pybind11/detail/function_record_pyobject.h + include/pybind11/detail/function_ref.h include/pybind11/detail/holder_caster_foreign_helpers.h include/pybind11/detail/init.h include/pybind11/detail/internals.h diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst index c647ae0f0a..76602709bf 100644 --- a/docs/advanced/functions.rst +++ b/docs/advanced/functions.rst @@ -437,10 +437,10 @@ Certain argument types may support conversion from one type to another. Some examples of conversions are: * :ref:`implicit_conversions` declared using ``py::implicitly_convertible()`` -* Calling a method accepting a double with an integer argument -* Calling a ``std::complex`` argument with a non-complex python type - (for example, with a float). (Requires the optional ``pybind11/complex.h`` - header). +* Passing an argument that implements ``__float__`` or ``__index__`` to ``float`` or ``double``. +* Passing an argument that implements ``__int__`` or ``__index__`` to ``int``. +* Passing an argument that implements ``__complex__``, ``__float__``, or ``__index__`` to ``std::complex``. + (Requires the optional ``pybind11/complex.h`` header). * Calling a function taking an Eigen matrix reference with a numpy array of the wrong type or of an incompatible data layout. (Requires the optional ``pybind11/eigen.h`` header). @@ -452,24 +452,37 @@ object, such as: .. code-block:: cpp - m.def("floats_only", [](double f) { return 0.5 * f; }, py::arg("f").noconvert()); - m.def("floats_preferred", [](double f) { return 0.5 * f; }, py::arg("f")); + m.def("supports_float", [](double f) { return 0.5 * f; }, py::arg("f")); + m.def("only_float", [](double f) { return 0.5 * f; }, py::arg("f").noconvert()); -Attempting the call the second function (the one without ``.noconvert()``) with -an integer will succeed, but attempting to call the ``.noconvert()`` version -will fail with a ``TypeError``: +``supports_float`` will accept any argument that implements ``__float__`` or ``__index__``. +``only_float`` will only accept a float or int argument. Anything else will fail with a ``TypeError``: + +.. note:: + + The noconvert behaviour of float, double and complex has changed to match PEP 484. + A float/double argument marked noconvert will accept float or int. + A std::complex argument will accept complex, float or int. .. code-block:: pycon - >>> floats_preferred(4) + class MyFloat: + def __init__(self, value: float) -> None: + self._value = float(value) + def __repr__(self) -> str: + return f"MyFloat({self._value})" + def __float__(self) -> float: + return self._value + + >>> supports_float(MyFloat(4)) 2.0 - >>> floats_only(4) + >>> only_float(MyFloat(4)) Traceback (most recent call last): File "", line 1, in - TypeError: floats_only(): incompatible function arguments. The following argument types are supported: + TypeError: only_float(): incompatible function arguments. The following argument types are supported: 1. (f: float) -> float - Invoked with: 4 + Invoked with: MyFloat(4) You may, of course, combine this with the :var:`_a` shorthand notation (see :ref:`keyword_args`) and/or :ref:`default_args`. It is also permitted to omit diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index f5a94da206..1c4973b748 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -244,29 +244,28 @@ struct type_caster::value && !is_std_char_t return false; } -#if !defined(PYPY_VERSION) - auto index_check = [](PyObject *o) { return PyIndex_Check(o); }; -#else - // In PyPy 7.3.3, `PyIndex_Check` is implemented by calling `__index__`, - // while CPython only considers the existence of `nb_index`/`__index__`. - auto index_check = [](PyObject *o) { return hasattr(o, "__index__"); }; -#endif - if (std::is_floating_point::value) { - if (convert || PyFloat_Check(src.ptr())) { + if (convert || PyFloat_Check(src.ptr()) || PYBIND11_LONG_CHECK(src.ptr())) { py_value = (py_type) PyFloat_AsDouble(src.ptr()); } else { return false; } } else if (PyFloat_Check(src.ptr()) - || (!convert && !PYBIND11_LONG_CHECK(src.ptr()) && !index_check(src.ptr()))) { + || !(convert || PYBIND11_LONG_CHECK(src.ptr()) + || PYBIND11_INDEX_CHECK(src.ptr()))) { + // Explicitly reject float → int conversion even in convert mode. + // This prevents silent truncation (e.g., 1.9 → 1). + // Only int → float conversion is allowed (widening, no precision loss). + // Also reject if none of the conversion conditions are met. return false; } else { handle src_or_index = src; // PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls. #if defined(PYPY_VERSION) object index; - if (!PYBIND11_LONG_CHECK(src.ptr())) { // So: index_check(src.ptr()) + // If not a PyLong, we need to call PyNumber_Index explicitly on PyPy. + // When convert is false, we only reach here if PYBIND11_INDEX_CHECK passed above. + if (!PYBIND11_LONG_CHECK(src.ptr())) { index = reinterpret_steal(PyNumber_Index(src.ptr())); if (!index) { PyErr_Clear(); @@ -286,8 +285,10 @@ struct type_caster::value && !is_std_char_t } } - // Python API reported an error - bool py_err = py_value == (py_type) -1 && PyErr_Occurred(); + bool py_err = (PyErr_Occurred() != nullptr); + if (py_err) { + assert(py_value == static_cast(-1)); + } // Check to see if the conversion is valid (integers should match exactly) // Signed/unsigned checks happen elsewhere diff --git a/include/pybind11/complex.h b/include/pybind11/complex.h index 0b6f49365d..8f285d778f 100644 --- a/include/pybind11/complex.h +++ b/include/pybind11/complex.h @@ -51,7 +51,9 @@ class type_caster> { if (!src) { return false; } - if (!convert && !PyComplex_Check(src.ptr())) { + if (!convert + && !(PyComplex_Check(src.ptr()) || PyFloat_Check(src.ptr()) + || PYBIND11_LONG_CHECK(src.ptr()))) { return false; } handle src_or_index = src; diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 9fc5b2d618..d1ab6deeb4 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -167,6 +167,14 @@ # define PYBIND11_NOINLINE __attribute__((noinline)) inline #endif +#if defined(_MSC_VER) +# define PYBIND11_ALWAYS_INLINE __forceinline +#elif defined(__GNUC__) +# define PYBIND11_ALWAYS_INLINE __attribute__((__always_inline__)) inline +#else +# define PYBIND11_ALWAYS_INLINE inline +#endif + #if defined(__MINGW32__) // For unknown reasons all PYBIND11_DEPRECATED member trigger a warning when declared // whether it is used or not diff --git a/include/pybind11/detail/function_ref.h b/include/pybind11/detail/function_ref.h new file mode 100644 index 0000000000..a81bdfe13f --- /dev/null +++ b/include/pybind11/detail/function_ref.h @@ -0,0 +1,101 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +//===- llvm/ADT/STLFunctionalExtras.h - Extras for -*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file contains a header-only class template that provides functionality +// similar to std::function but with non-owning semantics. It is a template-only +// implementation that requires no additional library linking. +// +//===----------------------------------------------------------------------===// + +/// An efficient, type-erasing, non-owning reference to a callable. This is +/// intended for use as the type of a function parameter that is not used +/// after the function in question returns. +/// +/// This class does not own the callable, so it is not in general safe to store +/// a FunctionRef. + +// pybind11: modified again from executorch::runtime::FunctionRef +// - renamed back to function_ref +// - use pybind11 enable_if_t, remove_cvref_t, and remove_reference_t +// - lint suppressions + +// torch::executor: modified from llvm::function_ref +// - renamed to FunctionRef +// - removed LLVM_GSL_POINTER and LLVM_LIFETIME_BOUND macro uses +// - use namespaced internal::remove_cvref_t + +#pragma once + +#include + +#include +#include +#include + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) +PYBIND11_NAMESPACE_BEGIN(detail) + +//===----------------------------------------------------------------------===// +// Features from C++20 +//===----------------------------------------------------------------------===// + +template +class function_ref; + +template +class function_ref { + Ret (*callback)(intptr_t callable, Params... params) = nullptr; + intptr_t callable; + + template + // NOLINTNEXTLINE(performance-unnecessary-value-param) + static Ret callback_fn(intptr_t callable, Params... params) { + // NOLINTNEXTLINE(performance-no-int-to-ptr) + return (*reinterpret_cast(callable))(std::forward(params)...); + } + +public: + function_ref() = default; + // NOLINTNEXTLINE(google-explicit-constructor) + function_ref(std::nullptr_t) {} + + template + // NOLINTNEXTLINE(google-explicit-constructor) + function_ref( + Callable &&callable, + // This is not the copy-constructor. + enable_if_t, function_ref>::value> * = nullptr, + // Functor must be callable and return a suitable type. + enable_if_t< + std::is_void::value + || std::is_convertible()(std::declval()...)), + Ret>::value> * = nullptr) + : callback(callback_fn>), + callable(reinterpret_cast(&callable)) {} + + // NOLINTNEXTLINE(performance-unnecessary-value-param) + Ret operator()(Params... params) const { + return callback(callable, std::forward(params)...); + } + + explicit operator bool() const { return callback; } + + bool operator==(const function_ref &Other) const { + return callable == Other.callable; + } +}; +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index b681529325..d719597f0d 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -39,11 +39,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 11 +# define PYBIND11_INTERNALS_VERSION 12 #endif -#if PYBIND11_INTERNALS_VERSION < 11 -# error "PYBIND11_INTERNALS_VERSION 11 is the minimum for all platforms for pybind11v3." +#if PYBIND11_INTERNALS_VERSION < 12 +# error "PYBIND11_INTERNALS_VERSION 12 is the minimum for all platforms for pybind11 v3.1.0" #endif PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) @@ -191,7 +191,7 @@ inline bool same_type(const std::type_info &lhs, const std::type_info &rhs) { } struct type_hash { - size_t operator()(const std::type_index &t) const { + size_t operator()(const std::type_index &t) const noexcept { size_t hash = 5381; const char *ptr = t.name(); while (auto c = static_cast(*ptr++)) { @@ -202,7 +202,7 @@ struct type_hash { }; struct type_equal_to { - bool operator()(const std::type_index &lhs, const std::type_index &rhs) const { + bool operator()(const std::type_index &lhs, const std::type_index &rhs) const noexcept { return lhs.name() == rhs.name() || std::strcmp(lhs.name(), rhs.name()) == 0; } }; @@ -218,7 +218,7 @@ template using type_map = std::unordered_map; struct override_hash { - size_t operator()(const std::pair &v) const { + size_t operator()(const std::pair &v) const noexcept { size_t value = std::hash()(v.first); value ^= std::hash()(v.second) + 0x9e3779b9 + (value << 6) + (value >> 2); return value; diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 0f31262c47..76d998a3ba 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -13,6 +13,7 @@ #include "detail/dynamic_raw_ptr_cast_if_possible.h" #include "detail/exception_translation.h" #include "detail/function_record_pyobject.h" +#include "detail/function_ref.h" #include "detail/init.h" #include "detail/native_enum_data.h" #include "detail/using_smart_holder.h" @@ -386,6 +387,46 @@ class cpp_function : public function { return unique_function_record(new detail::function_record()); } +private: + // This is outlined from the dispatch lambda in initialize to save + // on code size. Crucially, we use function_ref to type-erase the + // actual function lambda so that we can get code reuse for + // functions with the same Return, Args, and Guard. + template + static handle call_impl(detail::function_call &call, detail::function_ref f) { + using namespace detail; + // Static assertion: function_ref must be trivially copyable to ensure safe pass-by-value. + // Lifetime safety: The function_ref is created from cap->f which lives in the capture + // object stored in the function record, and is only used synchronously within this + // function call. It is never stored beyond the scope of call_impl. + static_assert(std::is_trivially_copyable>::value, + "function_ref must be trivially copyable for safe pass-by-value usage"); + using cast_out + = make_caster::value, void_type, Return>>; + + ArgsConverter args_converter; + if (!args_converter.load_args(call)) { + return PYBIND11_TRY_NEXT_OVERLOAD; + } + + /* Override policy for rvalues -- usually to enforce rvp::move on an rvalue */ + return_value_policy policy + = return_value_policy_override::policy(call.func.policy); + + /* Perform the function call */ + handle result; + if (call.func.is_setter) { + (void) std::move(args_converter).template call(f); + result = none().release(); + } else { + result = cast_out::cast( + std::move(args_converter).template call(f), policy, call.parent); + } + + return result; + } + +protected: /// Special internal constructor for functors, lambda functions, etc. template void initialize(Func &&f, Return (*)(Args...), const Extra &...extra) { @@ -448,13 +489,6 @@ class cpp_function : public function { /* Dispatch code which converts function arguments and performs the actual function call */ rec->impl = [](function_call &call) -> handle { - cast_in args_converter; - - /* Try to cast the function arguments into the C++ domain */ - if (!args_converter.load_args(call)) { - return PYBIND11_TRY_NEXT_OVERLOAD; - } - /* Invoke call policy pre-call hook */ process_attributes::precall(call); @@ -463,24 +497,11 @@ class cpp_function : public function { : call.func.data[0]); auto *cap = const_cast(reinterpret_cast(data)); - /* Override policy for rvalues -- usually to enforce rvp::move on an rvalue */ - return_value_policy policy - = return_value_policy_override::policy(call.func.policy); - - /* Function scope guard -- defaults to the compile-to-nothing `void_type` */ - using Guard = extract_guard_t; - - /* Perform the function call */ - handle result; - if (call.func.is_setter) { - (void) std::move(args_converter).template call(cap->f); - result = none().release(); - } else { - result = cast_out::cast( - std::move(args_converter).template call(cap->f), - policy, - call.parent); - } + auto result = call_impl, + cast_in>(call, detail::function_ref(cap->f)); /* Invoke call policy post-call hook */ process_attributes::postcall(call, result); @@ -2245,7 +2266,7 @@ class class_ : public detail::generic_type { static void add_base(detail::type_record &) {} template - class_ &def(const char *name_, Func &&f, const Extra &...extra) { + PYBIND11_ALWAYS_INLINE class_ &def(const char *name_, Func &&f, const Extra &...extra) { cpp_function cf(method_adaptor(std::forward(f)), name(name_), is_method(*this), @@ -2830,38 +2851,13 @@ struct enum_base { pos_only()) if (is_convertible) { - PYBIND11_ENUM_OP_CONV_LHS("__eq__", !b.is_none() && a.equal(b)); - PYBIND11_ENUM_OP_CONV_LHS("__ne__", b.is_none() || !a.equal(b)); - if (is_arithmetic) { - PYBIND11_ENUM_OP_CONV("__lt__", a < b); - PYBIND11_ENUM_OP_CONV("__gt__", a > b); - PYBIND11_ENUM_OP_CONV("__le__", a <= b); - PYBIND11_ENUM_OP_CONV("__ge__", a >= b); - PYBIND11_ENUM_OP_CONV("__and__", a & b); - PYBIND11_ENUM_OP_CONV("__rand__", a & b); - PYBIND11_ENUM_OP_CONV("__or__", a | b); - PYBIND11_ENUM_OP_CONV("__ror__", a | b); - PYBIND11_ENUM_OP_CONV("__xor__", a ^ b); - PYBIND11_ENUM_OP_CONV("__rxor__", a ^ b); m_base.attr("__invert__") = cpp_function([](const object &arg) { return ~(int_(arg)); }, name("__invert__"), is_method(m_base), pos_only()); } - } else { - PYBIND11_ENUM_OP_STRICT("__eq__", int_(a).equal(int_(b)), return false); - PYBIND11_ENUM_OP_STRICT("__ne__", !int_(a).equal(int_(b)), return true); - - if (is_arithmetic) { -#define PYBIND11_THROW throw type_error("Expected an enumeration of matching type!"); - PYBIND11_ENUM_OP_STRICT("__lt__", int_(a) < int_(b), PYBIND11_THROW); - PYBIND11_ENUM_OP_STRICT("__gt__", int_(a) > int_(b), PYBIND11_THROW); - PYBIND11_ENUM_OP_STRICT("__le__", int_(a) <= int_(b), PYBIND11_THROW); - PYBIND11_ENUM_OP_STRICT("__ge__", int_(a) >= int_(b), PYBIND11_THROW); -#undef PYBIND11_THROW - } } #undef PYBIND11_ENUM_OP_CONV_LHS @@ -2977,6 +2973,69 @@ class enum_ : public class_ { def(init([](Scalar i) { return static_cast(i); }), arg("value")); def_property_readonly("value", [](Type value) { return (Scalar) value; }, pos_only()); +#define PYBIND11_ENUM_OP_SAME_TYPE(op, expr) \ + def(op, [](Type a, Type b) { return expr; }, pybind11::name(op), arg("other"), pos_only()) +#define PYBIND11_ENUM_OP_SAME_TYPE_RHS_MAY_BE_NONE(op, expr) \ + def(op, [](Type a, Type *b_ptr) { return expr; }, pybind11::name(op), arg("other"), pos_only()) +#define PYBIND11_ENUM_OP_SCALAR(op, op_expr) \ + def( \ + op, \ + [](Type a, Scalar b) { return static_cast(a) op_expr b; }, \ + pybind11::name(op), \ + arg("other"), \ + pos_only()) +#define PYBIND11_ENUM_OP_CONV_ARITHMETIC(op, op_expr) \ + /* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \ + PYBIND11_ENUM_OP_SAME_TYPE(op, static_cast(a) op_expr static_cast(b)); \ + PYBIND11_ENUM_OP_SCALAR(op, op_expr) +#define PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE(op, strict_behavior) \ + def( \ + op, \ + [](Type, const object &) { strict_behavior; }, \ + pybind11::name(op), \ + arg("other"), \ + pos_only()) +#define PYBIND11_ENUM_OP_STRICT_ARITHMETIC(op, op_expr, strict_behavior) \ + /* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \ + PYBIND11_ENUM_OP_SAME_TYPE(op, static_cast(a) op_expr static_cast(b)); \ + PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE(op, strict_behavior); + + PYBIND11_ENUM_OP_SAME_TYPE_RHS_MAY_BE_NONE("__eq__", b_ptr && a == *b_ptr); + PYBIND11_ENUM_OP_SAME_TYPE_RHS_MAY_BE_NONE("__ne__", !b_ptr || a != *b_ptr); + if (std::is_convertible::value) { + PYBIND11_ENUM_OP_SCALAR("__eq__", ==); + PYBIND11_ENUM_OP_SCALAR("__ne__", !=); + if (is_arithmetic) { + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__lt__", <); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__gt__", >); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__le__", <=); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__ge__", >=); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__and__", &); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__rand__", &); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__or__", |); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__ror__", |); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__xor__", ^); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__rxor__", ^); + } + } else if (is_arithmetic) { +#define PYBIND11_ENUM_OP_THROW_TYPE_ERROR \ + throw type_error("Expected an enumeration of matching type!"); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__lt__", <, PYBIND11_ENUM_OP_THROW_TYPE_ERROR); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__gt__", >, PYBIND11_ENUM_OP_THROW_TYPE_ERROR); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__le__", <=, PYBIND11_ENUM_OP_THROW_TYPE_ERROR); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__ge__", >=, PYBIND11_ENUM_OP_THROW_TYPE_ERROR); +#undef PYBIND11_ENUM_OP_THROW_TYPE_ERROR + } + PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE("__eq__", return false); + PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE("__ne__", return true); + +#undef PYBIND11_ENUM_OP_SAME_TYPE +#undef PYBIND11_ENUM_OP_SAME_TYPE_RHS_MAY_BE_NONE +#undef PYBIND11_ENUM_OP_SCALAR +#undef PYBIND11_ENUM_OP_CONV_ARITHMETIC +#undef PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE +#undef PYBIND11_ENUM_OP_STRICT_ARITHMETIC + def("__int__", [](Type value) { return (Scalar) value; }, pos_only()); def("__index__", [](Type value) { return (Scalar) value; }, pos_only()); attr("__setstate__") = cpp_function( diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 1539b171a2..d96e9afc1f 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -83,6 +83,7 @@ "include/pybind11/detail/descr.h", "include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h", "include/pybind11/detail/function_record_pyobject.h", + "include/pybind11/detail/function_ref.h", "include/pybind11/detail/holder_caster_foreign_helpers.h", "include/pybind11/detail/init.h", "include/pybind11/detail/internals.h", diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index 6cde727ad8..40803fbf8a 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -363,9 +363,34 @@ TEST_SUBMODULE(builtin_casters, m) { m.def("complex_cast", [](float x) { return "{}"_s.format(x); }); m.def("complex_cast", [](std::complex x) { return "({}, {})"_s.format(x.real(), x.imag()); }); + m.def( + "complex_cast_strict", + [](std::complex x) { return "({}, {})"_s.format(x.real(), x.imag()); }, + py::arg{}.noconvert()); + m.def("complex_convert", [](std::complex x) { return x; }); m.def("complex_noconvert", [](std::complex x) { return x; }, py::arg{}.noconvert()); + // test_overload_resolution_float_int + // Test that float overload registered before int overload gets selected when passing int + // This documents the breaking change: int can now match float in strict mode + m.def("overload_resolution_test", [](float x) { return "float: " + std::to_string(x); }); + m.def("overload_resolution_test", [](int x) { return "int: " + std::to_string(x); }); + + // Test with noconvert (strict mode) - this is the key breaking change + m.def( + "overload_resolution_strict", + [](float x) { return "float_strict: " + std::to_string(x); }, + py::arg{}.noconvert()); + m.def("overload_resolution_strict", [](int x) { return "int_strict: " + std::to_string(x); }); + + // Test complex overload resolution: complex registered before float/int + m.def("overload_resolution_complex", [](std::complex x) { + return "complex: (" + std::to_string(x.real()) + ", " + std::to_string(x.imag()) + ")"; + }); + m.def("overload_resolution_complex", [](float x) { return "float: " + std::to_string(x); }); + m.def("overload_resolution_complex", [](int x) { return "int: " + std::to_string(x); }); + // test int vs. long (Python 2) m.def("int_cast", []() { return 42; }); m.def("long_cast", []() { return (long) 42; }); diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 23c191cec2..b232c087e0 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -315,6 +315,7 @@ def cant_convert(v): # Before Python 3.8, `PyLong_AsLong` does not pick up on `obj.__index__`, # but pybind11 "backports" this behavior. assert convert(Index()) == 42 + assert isinstance(convert(Index()), int) assert noconvert(Index()) == 42 assert convert(IntAndIndex()) == 0 # Fishy; `int(DoubleThought)` == 42 assert noconvert(IntAndIndex()) == 0 @@ -323,6 +324,50 @@ def cant_convert(v): assert convert(RaisingValueErrorOnIndex()) == 42 requires_conversion(RaisingValueErrorOnIndex()) + class IndexReturnsFloat: + def __index__(self): + return 3.14 # noqa: PLE0305 Wrong: should return int + + class IntReturnsFloat: + def __int__(self): + return 3.14 # Wrong: should return int + + class IndexFloatIntInt: + def __index__(self): + return 3.14 # noqa: PLE0305 Wrong: should return int + + def __int__(self): + return 42 # Correct: returns int + + class IndexIntIntFloat: + def __index__(self): + return 42 # Correct: returns int + + def __int__(self): + return 3.14 # Wrong: should return int + + class IndexFloatIntFloat: + def __index__(self): + return 3.14 # noqa: PLE0305 Wrong: should return int + + def __int__(self): + return 2.71 # Wrong: should return int + + cant_convert(IndexReturnsFloat()) + requires_conversion(IndexReturnsFloat()) + + cant_convert(IntReturnsFloat()) + requires_conversion(IntReturnsFloat()) + + assert convert(IndexFloatIntInt()) == 42 # convert: __index__ fails, uses __int__ + requires_conversion(IndexFloatIntInt()) # noconvert: __index__ fails, no fallback + + assert convert(IndexIntIntFloat()) == 42 # convert: __index__ succeeds + assert noconvert(IndexIntIntFloat()) == 42 # noconvert: __index__ succeeds + + cant_convert(IndexFloatIntFloat()) # convert mode rejects (both fail) + requires_conversion(IndexFloatIntFloat()) # noconvert mode also rejects + def test_float_convert(doc): class Int: @@ -356,7 +401,7 @@ def cant_convert(v): assert pytest.approx(convert(Index())) == -7.0 assert isinstance(convert(Float()), float) assert pytest.approx(convert(3)) == 3.0 - requires_conversion(3) + assert pytest.approx(noconvert(3)) == 3.0 cant_convert(Int()) @@ -505,6 +550,11 @@ def __index__(self) -> int: assert m.complex_cast(Complex()) == "(5.0, 4.0)" assert m.complex_cast(2j) == "(0.0, 2.0)" + assert m.complex_cast_strict(1) == "(1.0, 0.0)" + assert m.complex_cast_strict(3.0) == "(3.0, 0.0)" + assert m.complex_cast_strict(complex(5, 4)) == "(5.0, 4.0)" + assert m.complex_cast_strict(2j) == "(0.0, 2.0)" + convert, noconvert = m.complex_convert, m.complex_noconvert def requires_conversion(v): @@ -529,14 +579,127 @@ def cant_convert(v): assert convert(Index()) == 1 assert isinstance(convert(Index()), complex) - requires_conversion(1) - requires_conversion(2.0) + assert noconvert(1) == 1.0 + assert noconvert(2.0) == 2.0 assert noconvert(1 + 5j) == 1.0 + 5.0j requires_conversion(Complex()) requires_conversion(Float()) requires_conversion(Index()) +def test_complex_index_handling(): + """ + Test __index__ handling in complex caster (added with PR #5879). + + This test verifies that custom __index__ objects (not PyLong) work correctly + with complex conversion. The behavior should be consistent across CPython, + PyPy, and GraalPy. + + - Custom __index__ objects work with convert (non-strict mode) + - Custom __index__ objects do NOT work with noconvert (strict mode) + - Regular int (PyLong) works with both convert and noconvert + """ + + class CustomIndex: + """Custom class with __index__ but not __int__ or __float__""" + + def __index__(self) -> int: + return 42 + + class CustomIndexNegative: + """Custom class with negative __index__""" + + def __index__(self) -> int: + return -17 + + convert, noconvert = m.complex_convert, m.complex_noconvert + + # Test that regular int (PyLong) works + assert convert(5) == 5.0 + 0j + assert noconvert(5) == 5.0 + 0j + + # Test that custom __index__ objects work with convert (non-strict mode) + # This exercises the PyPy-specific path in complex.h + assert convert(CustomIndex()) == 42.0 + 0j + assert convert(CustomIndexNegative()) == -17.0 + 0j + + # With noconvert (strict mode), custom __index__ objects are NOT accepted + # Strict mode only accepts complex, float, or int (PyLong), not custom __index__ objects + def requires_conversion(v): + pytest.raises(TypeError, noconvert, v) + + requires_conversion(CustomIndex()) + requires_conversion(CustomIndexNegative()) + + # Verify the result is actually a complex + result = convert(CustomIndex()) + assert isinstance(result, complex) + assert result.real == 42.0 + assert result.imag == 0.0 + + +def test_overload_resolution_float_int(): + """ + Test overload resolution behavior when int can match float (added with PR #5879). + + This test documents the breaking change in PR #5879: when a float overload is + registered before an int overload, passing a Python int will now match the float + overload (because int can be converted to float in strict mode per PEP 484). + + Before PR #5879: int(42) would match int overload (if both existed) + After PR #5879: int(42) matches float overload (if registered first) + + This is a breaking change because existing code that relied on int matching + int overloads may now match float overloads instead. + """ + # Test 1: float overload registered first, int second + # When passing int(42), pybind11 tries overloads in order: + # 1. float overload - can int(42) be converted? Yes (with PR #5879 changes) + # 2. Match! Use float overload (int overload never checked) + result = m.overload_resolution_test(42) + assert result == "float: 42.000000", ( + f"Expected int(42) to match float overload, got: {result}. " + "This documents the breaking change: int now matches float overloads." + ) + assert m.overload_resolution_test(42.0) == "float: 42.000000" + + # Test 2: With noconvert (strict mode) - this is the KEY breaking change + # Before PR #5879: int(42) would NOT match float overload with noconvert, would match int overload + # After PR #5879: int(42) DOES match float overload with noconvert (because int->float is now allowed) + result_strict = m.overload_resolution_strict(42) + assert result_strict == "float_strict: 42.000000", ( + f"Expected int(42) to match float overload with noconvert, got: {result_strict}. " + "This is the key breaking change: int now matches float even in strict mode." + ) + assert m.overload_resolution_strict(42.0) == "float_strict: 42.000000" + + # Test 3: complex overload registered first, then float, then int + # When passing int(5), pybind11 tries overloads in order: + # 1. complex overload - can int(5) be converted? Yes (with PR #5879 changes) + # 2. Match! Use complex overload + assert m.overload_resolution_complex(5) == "complex: (5.000000, 0.000000)" + assert m.overload_resolution_complex(5.0) == "complex: (5.000000, 0.000000)" + assert ( + m.overload_resolution_complex(complex(3, 4)) == "complex: (3.000000, 4.000000)" + ) + + # Verify that the overloads are registered in the expected order + # The docstring should show float overload before int overload + doc = m.overload_resolution_test.__doc__ + assert doc is not None + # Check that float overload appears before int overload in docstring + # The docstring uses "typing.SupportsFloat" and "typing.SupportsInt" + float_pos = doc.find("SupportsFloat") + int_pos = doc.find("SupportsInt") + assert float_pos != -1, f"Could not find 'SupportsFloat' in docstring: {doc}" + assert int_pos != -1, f"Could not find 'SupportsInt' in docstring: {doc}" + assert float_pos < int_pos, ( + f"Float overload should appear before int overload in docstring. " + f"Found 'SupportsFloat' at {float_pos}, 'SupportsInt' at {int_pos}. " + f"Docstring: {doc}" + ) + + def test_bool_caster(): """Test bool caster implicit conversions.""" convert, noconvert = m.bool_passthrough, m.bool_passthrough_noconvert diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index c0a57a7b86..327e41eb33 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -179,10 +179,11 @@ def gen_f(): # do some work async work = [1, 2, 3, 4] m.test_async_callback(gen_f(), work) - # wait until work is done - from time import sleep - - sleep(0.5) + # Wait for all detached worker threads to finish. + deadline = time.monotonic() + 5.0 + while len(res) < len(work) and time.monotonic() < deadline: + time.sleep(0.01) + assert len(res) == len(work), f"Timed out waiting for callbacks: res={res!r}" assert sum(res) == sum(x + 3 for x in work) diff --git a/tests/test_copy_move.py b/tests/test_copy_move.py index 3a3f293414..d843793350 100644 --- a/tests/test_copy_move.py +++ b/tests/test_copy_move.py @@ -70,12 +70,12 @@ def test_move_and_copy_loads(): assert c_m.copy_assignments + c_m.copy_constructions == 0 assert c_m.move_assignments == 6 - assert c_m.move_constructions == 9 + assert c_m.move_constructions == 21 assert c_mc.copy_assignments + c_mc.copy_constructions == 0 assert c_mc.move_assignments == 5 - assert c_mc.move_constructions == 8 + assert c_mc.move_constructions == 18 assert c_c.copy_assignments == 4 - assert c_c.copy_constructions == 6 + assert c_c.copy_constructions == 14 assert c_m.alive() + c_mc.alive() + c_c.alive() == 0 @@ -103,12 +103,12 @@ def test_move_and_copy_load_optional(): assert c_m.copy_assignments + c_m.copy_constructions == 0 assert c_m.move_assignments == 2 - assert c_m.move_constructions == 5 + assert c_m.move_constructions == 9 assert c_mc.copy_assignments + c_mc.copy_constructions == 0 assert c_mc.move_assignments == 2 - assert c_mc.move_constructions == 5 + assert c_mc.move_constructions == 9 assert c_c.copy_assignments == 2 - assert c_c.copy_constructions == 5 + assert c_c.copy_constructions == 9 assert c_m.alive() + c_mc.alive() + c_c.alive() == 0 diff --git a/tests/test_custom_type_casters.py b/tests/test_custom_type_casters.py index 6ed1c564f0..0680e50504 100644 --- a/tests/test_custom_type_casters.py +++ b/tests/test_custom_type_casters.py @@ -55,17 +55,7 @@ def test_noconvert_args(msg): assert m.floats_preferred(4) == 2.0 assert m.floats_only(4.0) == 2.0 - with pytest.raises(TypeError) as excinfo: - m.floats_only(4) - assert ( - msg(excinfo.value) - == """ - floats_only(): incompatible function arguments. The following argument types are supported: - 1. (f: float) -> float - - Invoked with: 4 - """ - ) + assert m.floats_only(4) == 2.0 assert m.ints_preferred(4) == 2 assert m.ints_preferred(True) == 0 diff --git a/tests/test_enum.py b/tests/test_enum.py index f295b01457..52083caa03 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -296,9 +296,19 @@ def test_generated_dunder_methods_pos_only(): ]: method = getattr(enum_type, binary_op, None) if method is not None: + # 1) The docs must start with the name of the op. assert ( re.match( - rf"^{binary_op}\(self: [\w\.]+, other: [\w\.]+, /\)", + rf"^{binary_op}\(", + method.__doc__, + ) + is not None + ) + # 2) The docs must contain the op's signature. This is a separate check + # and not anchored at the start because the op may be overloaded. + assert ( + re.search( + rf"{binary_op}\(self: [\w\.]+, other: [\w\.]+, /\)", method.__doc__, ) is not None diff --git a/tests/test_factory_constructors.cpp b/tests/test_factory_constructors.cpp index c96a3a31fb..33a484945a 100644 --- a/tests/test_factory_constructors.cpp +++ b/tests/test_factory_constructors.cpp @@ -405,11 +405,10 @@ TEST_SUBMODULE(factory_constructors, m) { pyNoisyAlloc.def(py::init([](double d, int) { return NoisyAlloc(d); })); // Old-style placement new init; requires preallocation ignoreOldStyleInitWarnings([&pyNoisyAlloc]() { - pyNoisyAlloc.def("__init__", - [](NoisyAlloc &a, double d, double) { new (&a) NoisyAlloc(d); }); + pyNoisyAlloc.def("__init__", [](NoisyAlloc &a, int i, double) { new (&a) NoisyAlloc(i); }); }); // Requires deallocation of previous overload preallocated value: - pyNoisyAlloc.def(py::init([](int i, double) { return new NoisyAlloc(i); })); + pyNoisyAlloc.def(py::init([](double d, double) { return new NoisyAlloc(d); })); // Regular again: requires yet another preallocation ignoreOldStyleInitWarnings([&pyNoisyAlloc]() { pyNoisyAlloc.def( diff --git a/tests/test_factory_constructors.py b/tests/test_factory_constructors.py index c6ae98c7fb..cdf16ec858 100644 --- a/tests/test_factory_constructors.py +++ b/tests/test_factory_constructors.py @@ -433,9 +433,10 @@ def test_reallocation_e(capture, msg): create_and_destroy(3.5, 4.5) assert msg(capture) == strip_comments( """ - noisy new # preallocation needed before invoking placement-new overload - noisy placement new # Placement new - NoisyAlloc(double 3.5) # construction + noisy new # preallocation needed before invoking factory pointer overload + noisy delete # deallocation of preallocated storage + noisy new # Factory pointer allocation + NoisyAlloc(double 3.5) # factory pointer construction --- ~NoisyAlloc() # Destructor noisy delete # operator delete @@ -450,9 +451,8 @@ def test_reallocation_f(capture, msg): assert msg(capture) == strip_comments( """ noisy new # preallocation needed before invoking placement-new overload - noisy delete # deallocation of preallocated storage - noisy new # Factory pointer allocation - NoisyAlloc(int 4) # factory pointer construction + noisy placement new # Placement new + NoisyAlloc(int 4) # construction --- ~NoisyAlloc() # Destructor noisy delete # operator delete diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index e324c8bdd4..f5fb02d121 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -240,33 +240,33 @@ TEST_SUBMODULE(methods_and_attributes, m) { #if defined(PYBIND11_OVERLOAD_CAST) .def("overloaded", py::overload_cast<>(&ExampleMandA::overloaded)) .def("overloaded", py::overload_cast(&ExampleMandA::overloaded)) + .def("overloaded", py::overload_cast(&ExampleMandA::overloaded)) .def("overloaded", py::overload_cast(&ExampleMandA::overloaded)) .def("overloaded", py::overload_cast(&ExampleMandA::overloaded)) - .def("overloaded", py::overload_cast(&ExampleMandA::overloaded)) .def("overloaded", py::overload_cast(&ExampleMandA::overloaded)) .def("overloaded_float", py::overload_cast(&ExampleMandA::overloaded)) .def("overloaded_const", py::overload_cast(&ExampleMandA::overloaded, py::const_)) + .def("overloaded_const", + py::overload_cast(&ExampleMandA::overloaded, py::const_)) .def("overloaded_const", py::overload_cast(&ExampleMandA::overloaded, py::const_)) .def("overloaded_const", py::overload_cast(&ExampleMandA::overloaded, py::const_)) - .def("overloaded_const", - py::overload_cast(&ExampleMandA::overloaded, py::const_)) .def("overloaded_const", py::overload_cast(&ExampleMandA::overloaded, py::const_)) #else // Use both the traditional static_cast method and the C++11 compatible overload_cast_ .def("overloaded", overload_cast_<>()(&ExampleMandA::overloaded)) .def("overloaded", overload_cast_()(&ExampleMandA::overloaded)) - .def("overloaded", overload_cast_()(&ExampleMandA::overloaded)) - .def("overloaded", static_cast(&ExampleMandA::overloaded)) - .def("overloaded", static_cast(&ExampleMandA::overloaded)) + .def("overloaded", overload_cast_()(&ExampleMandA::overloaded)) + .def("overloaded", static_cast(&ExampleMandA::overloaded)) + .def("overloaded", static_cast(&ExampleMandA::overloaded)) .def("overloaded", static_cast(&ExampleMandA::overloaded)) .def("overloaded_float", overload_cast_()(&ExampleMandA::overloaded)) .def("overloaded_const", overload_cast_()(&ExampleMandA::overloaded, py::const_)) - .def("overloaded_const", overload_cast_()(&ExampleMandA::overloaded, py::const_)) - .def("overloaded_const", static_cast(&ExampleMandA::overloaded)) - .def("overloaded_const", static_cast(&ExampleMandA::overloaded)) + .def("overloaded_const", overload_cast_()(&ExampleMandA::overloaded, py::const_)) + .def("overloaded_const", static_cast(&ExampleMandA::overloaded)) + .def("overloaded_const", static_cast(&ExampleMandA::overloaded)) .def("overloaded_const", static_cast(&ExampleMandA::overloaded)) #endif // test_no_mixed_overloads diff --git a/tests/test_stl.cpp b/tests/test_stl.cpp index 6084d517df..8bddbb1f38 100644 --- a/tests/test_stl.cpp +++ b/tests/test_stl.cpp @@ -528,7 +528,7 @@ TEST_SUBMODULE(stl, m) { m.def("load_variant", [](const variant &v) { return py::detail::visit_helper::call(visitor(), v); }); - m.def("load_variant_2pass", [](variant v) { + m.def("load_variant_2pass", [](variant v) { return py::detail::visit_helper::call(visitor(), v); }); m.def("cast_variant", []() {