From 515a3abb92c3d3a33df0a1c11c2bacf9ccb534aa Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 23 Oct 2025 09:02:34 -0700 Subject: [PATCH 01/45] init Signed-off-by: Michael Carlstrom --- docs/advanced/functions.rst | 1 + include/pybind11/cast.h | 6 +++++- tests/test_builtin_casters.py | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst index ff00c9c8ac..cc1676386b 100644 --- a/docs/advanced/functions.rst +++ b/docs/advanced/functions.rst @@ -455,6 +455,7 @@ object, such as: 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")); +TODO: Update this is no longer true 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``: diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 4708101d80..d197c54585 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -253,7 +253,11 @@ struct type_caster::value && !is_std_char_t #endif if (std::is_floating_point::value) { - if (convert || PyFloat_Check(src.ptr())) { + if (PyFloat_Check(src.ptr())) { + py_value = (py_type) PyFloat_AsDouble(src.ptr()); + } else if (PYBIND11_LONG_CHECK(src.ptr())) { + py_value = (py_type) PyLong_AsDouble(src.ptr()); + } else if (convert) { py_value = (py_type) PyFloat_AsDouble(src.ptr()); } else { return false; diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 7a2c6a4d8f..8376e7d0b6 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -322,6 +322,10 @@ def cant_convert(v): def test_float_convert(doc): + class Int: + def __int__(self): + return -5 + class Float: def __float__(self): return 41.45 @@ -333,8 +337,14 @@ def __float__(self): def requires_conversion(v): pytest.raises(TypeError, noconvert, v) + def cant_convert(v): + pytest.raises(TypeError, convert, v) + requires_conversion(Float()) assert pytest.approx(convert(Float())) == 41.45 + assert pytest.approx(convert(3)) == 3.0 + assert pytest.approx(noconvert(3)) == 3.0 + cant_convert(Int()) def test_numpy_int_convert(): From aa2693f96f23adc7c41f01047f1497f2291d97cd Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Thu, 23 Oct 2025 17:36:12 +0100 Subject: [PATCH 02/45] Add constexpr to is_floating_point check This is known at compile time so it can be constexpr --- include/pybind11/cast.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 4708101d80..5a0a57262f 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -252,8 +252,8 @@ struct type_caster::value && !is_std_char_t auto index_check = [](PyObject *o) { return hasattr(o, "__index__"); }; #endif - if (std::is_floating_point::value) { if (convert || PyFloat_Check(src.ptr())) { + if constexpr (std::is_floating_point::value) { py_value = (py_type) PyFloat_AsDouble(src.ptr()); } else { return false; From 7948b27d4dc77cfba27f3ceafd7b350627030bd7 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Thu, 23 Oct 2025 17:37:19 +0100 Subject: [PATCH 03/45] Allow noconvert float to accept int --- include/pybind11/cast.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 5a0a57262f..55e3dc4136 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -252,8 +252,8 @@ struct type_caster::value && !is_std_char_t auto index_check = [](PyObject *o) { return hasattr(o, "__index__"); }; #endif - if (convert || PyFloat_Check(src.ptr())) { if constexpr (std::is_floating_point::value) { + if (convert || PyFloat_Check(src.ptr()) || PYBIND11_LONG_CHECK(src.ptr())) { py_value = (py_type) PyFloat_AsDouble(src.ptr()); } else { return false; From 47746af1e7a16e5b9c451728b651dfe2a992b52f Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Thu, 23 Oct 2025 17:38:06 +0100 Subject: [PATCH 04/45] Update noconvert documentation --- docs/advanced/functions.rst | 39 ++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst index ff00c9c8ac..afe55196bf 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__`` to ``float`` or ``double``. +* Passing an argument that implements ``__int__`` to ``int``. +* Passing an argument that implements ``__complex__`` 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 implments ``__float__``. +``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 From 507d31c3d6f8a8ed8bf3f3ecf0df93989db7fe2e Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Thu, 23 Oct 2025 17:43:15 +0100 Subject: [PATCH 05/45] Allow noconvert complex to accept int and float --- include/pybind11/complex.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/complex.h b/include/pybind11/complex.h index 8a831c12ce..4518aa3070 100644 --- a/include/pybind11/complex.h +++ b/include/pybind11/complex.h @@ -51,7 +51,7 @@ 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; } Py_complex result = PyComplex_AsCComplex(src.ptr()); From 4ea8bcb2f4b7afc897c5e198f08b08c32049f41c Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 23 Oct 2025 10:11:04 -0700 Subject: [PATCH 06/45] Add complex strict test --- tests/test_builtin_casters.cpp | 1 + tests/test_builtin_casters.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index c516f8de7e..51f9fff459 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -363,6 +363,7 @@ 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()); // test int vs. long (Python 2) m.def("int_cast", []() { return (int) 42; }); diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 8376e7d0b6..8e6b388a4a 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -473,6 +473,11 @@ def test_complex_cast(): assert m.complex_cast(1) == "1.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)" + def test_bool_caster(): """Test bool caster implicit conversions.""" From 81e49f626e11c187e0870a97099db01bc45003f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:11:51 +0000 Subject: [PATCH 07/45] style: pre-commit fixes --- docs/advanced/functions.rst | 2 +- include/pybind11/complex.h | 4 +++- tests/test_builtin_casters.cpp | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst index afe55196bf..5ad336847e 100644 --- a/docs/advanced/functions.rst +++ b/docs/advanced/functions.rst @@ -473,7 +473,7 @@ object, such as: return f"MyFloat({self._value})" def __float__(self) -> float: return self._value - + >>> supports_float(MyFloat(4)) 2.0 >>> only_float(MyFloat(4)) diff --git a/include/pybind11/complex.h b/include/pybind11/complex.h index 4518aa3070..8fea1c0065 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()) || PyFloat_Check(src.ptr()) || PYBIND11_LONG_CHECK(src.ptr()))) { + if (!convert + && !(PyComplex_Check(src.ptr()) || PyFloat_Check(src.ptr()) + || PYBIND11_LONG_CHECK(src.ptr()))) { return false; } Py_complex result = PyComplex_AsCComplex(src.ptr()); diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index 51f9fff459..123db2986b 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -363,7 +363,10 @@ 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_cast_strict", + [](std::complex x) { return "({}, {})"_s.format(x.real(), x.imag()); }, + py::arg{}.noconvert()); // test int vs. long (Python 2) m.def("int_cast", []() { return (int) 42; }); From 0b94f212bf12b386bb1c521429e96b1d1eb7211f Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 23 Oct 2025 10:54:56 -0700 Subject: [PATCH 08/45] Update unit tests so int, becomes double. --- tests/test_custom_type_casters.py | 12 +----------- tests/test_factory_constructors.cpp | 4 ++-- tests/test_factory_constructors.py | 8 ++++---- tests/test_methods_and_attributes.cpp | 6 +++--- tests/test_stl.cpp | 2 +- 5 files changed, 11 insertions(+), 21 deletions(-) diff --git a/tests/test_custom_type_casters.py b/tests/test_custom_type_casters.py index bf31d3f374..ee97f96759 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_factory_constructors.cpp b/tests/test_factory_constructors.cpp index a387cd2e76..b97ff6ee0d 100644 --- a/tests/test_factory_constructors.cpp +++ b/tests/test_factory_constructors.cpp @@ -402,10 +402,10 @@ TEST_SUBMODULE(factory_constructors, m) { // Old-style placement new init; requires preallocation ignoreOldStyleInitWarnings([&pyNoisyAlloc]() { pyNoisyAlloc.def("__init__", - [](NoisyAlloc &a, double d, double) { new (&a) NoisyAlloc(d); }); + [](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 67f859b9ab..02c5724516 100644 --- a/tests/test_factory_constructors.py +++ b/tests/test_factory_constructors.py @@ -430,12 +430,12 @@ def test_reallocation_d(capture, msg): @pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") def test_reallocation_e(capture, msg): with capture: - create_and_destroy(3.5, 4.5) + create_and_destroy(4, 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 + NoisyAlloc(int 4) # construction --- ~NoisyAlloc() # Destructor noisy delete # operator delete @@ -446,13 +446,13 @@ def test_reallocation_e(capture, msg): @pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") def test_reallocation_f(capture, msg): with capture: - create_and_destroy(4, 0.5) + create_and_destroy(3.5, 0.5) 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 + NoisyAlloc(double 3.5) # factory pointer 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..14838b96b2 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -240,18 +240,18 @@ 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 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", []() { From e9bf4e5b11095d5f1c2097ec11f4f9195203e710 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:55:24 +0000 Subject: [PATCH 09/45] style: pre-commit fixes --- tests/test_factory_constructors.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_factory_constructors.cpp b/tests/test_factory_constructors.cpp index b97ff6ee0d..65a8e5cf15 100644 --- a/tests/test_factory_constructors.cpp +++ b/tests/test_factory_constructors.cpp @@ -401,8 +401,7 @@ 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, int i, double) { new (&a) NoisyAlloc(i); }); + pyNoisyAlloc.def("__init__", [](NoisyAlloc &a, int i, double) { new (&a) NoisyAlloc(i); }); }); // Requires deallocation of previous overload preallocated value: pyNoisyAlloc.def(py::init([](double d, double) { return new NoisyAlloc(d); })); From 21f8447daee82cd55809cbd0d59af47a23d6de95 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 23 Oct 2025 11:00:43 -0700 Subject: [PATCH 10/45] remove if (constexpr) Signed-off-by: Michael Carlstrom --- include/pybind11/cast.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 55e3dc4136..09c8f7068d 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -252,7 +252,7 @@ struct type_caster::value && !is_std_char_t auto index_check = [](PyObject *o) { return hasattr(o, "__index__"); }; #endif - if constexpr (std::is_floating_point::value) { + if (std::is_floating_point::value) { if (convert || PyFloat_Check(src.ptr()) || PYBIND11_LONG_CHECK(src.ptr())) { py_value = (py_type) PyFloat_AsDouble(src.ptr()); } else { From 58063183f3ff9bc16f017ed0554fd0908ef38e9b Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 23 Oct 2025 11:50:20 -0700 Subject: [PATCH 11/45] fix spelling error --- docs/advanced/functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst index 5ad336847e..78455546d0 100644 --- a/docs/advanced/functions.rst +++ b/docs/advanced/functions.rst @@ -455,7 +455,7 @@ object, such as: 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()); -``supports_float`` will accept any argument that implments ``__float__``. +``supports_float`` will accept any argument that implements ``__float__``. ``only_float`` will only accept a float or int argument. Anything else will fail with a ``TypeError``: .. note:: From a0fb6dc49588c8fa6b38555478f0f9cf6e1f4aa1 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 23 Oct 2025 12:24:01 -0700 Subject: [PATCH 12/45] bump order in #else --- tests/test_methods_and_attributes.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index 14838b96b2..567e0794fd 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -259,14 +259,14 @@ TEST_SUBMODULE(methods_and_attributes, m) { .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", 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", static_cast(&ExampleMandA::overloaded)) .def("overloaded_const", static_cast(&ExampleMandA::overloaded)) #endif // test_no_mixed_overloads From 2692820d20ac68fc46ed6f1f6a3ef70125ea8767 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 23 Oct 2025 17:24:50 -0700 Subject: [PATCH 13/45] Switch order in c++11 only section Signed-off-by: Michael Carlstrom --- tests/test_methods_and_attributes.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index 567e0794fd..f5fb02d121 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -258,15 +258,15 @@ TEST_SUBMODULE(methods_and_attributes, m) { // 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 From b12f5a89f165e60b51a4e707a5d9ca04842fdf36 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 23 Oct 2025 18:11:53 -0700 Subject: [PATCH 14/45] ci: trigger build From 2187a659a65b9ddbd2991c7a7a71f17fde199a32 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Thu, 23 Oct 2025 18:12:01 -0700 Subject: [PATCH 15/45] ci: trigger build From d42c8e8af752d20c569e6aba34a0e236353155c1 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 24 Oct 2025 09:16:57 +0100 Subject: [PATCH 16/45] Allow casting from float to int The int type caster allows anything that implements __int__ with explicit exception of the python float. I can't see any reason for this. This modifies the int casting behaviour to accept a float. If the argument is marked as noconvert() it will only accept int. --- include/pybind11/cast.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 09c8f7068d..a51b172f74 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -258,10 +258,7 @@ struct type_caster::value && !is_std_char_t } else { return false; } - } else if (PyFloat_Check(src.ptr()) - || (!convert && !PYBIND11_LONG_CHECK(src.ptr()) && !index_check(src.ptr()))) { - return false; - } else { + } else if (convert || PYBIND11_LONG_CHECK(src.ptr()) || index_check(src.ptr())) { handle src_or_index = src; // PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls. #if defined(PYPY_VERSION) @@ -284,6 +281,8 @@ struct type_caster::value && !is_std_char_t ? (py_type) PyLong_AsLong(src_or_index.ptr()) : (py_type) PYBIND11_LONG_AS_LONGLONG(src_or_index.ptr()); } + } else { + return false; } // Python API reported an error From 16bdff3359e5cc6ff224482d76ea3fb167b27c24 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 24 Oct 2025 11:29:11 -0700 Subject: [PATCH 17/45] tests for py::float into int --- tests/test_builtin_casters.py | 3 ++- tests/test_custom_type_casters.py | 12 +----------- tests/test_methods_and_attributes.py | 2 +- tests/test_stl.py | 12 ++++++------ 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 8e6b388a4a..25fe5b4a56 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -297,7 +297,8 @@ def cant_convert(v): assert convert(7) == 7 assert noconvert(7) == 7 - cant_convert(3.14159) + assert convert(3.14159) == 3 + requires_conversion(3.14159) # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) if sys.version_info < (3, 10) and env.CPYTHON: diff --git a/tests/test_custom_type_casters.py b/tests/test_custom_type_casters.py index ee97f96759..d8c8f8008f 100644 --- a/tests/test_custom_type_casters.py +++ b/tests/test_custom_type_casters.py @@ -59,17 +59,7 @@ def test_noconvert_args(msg): assert m.ints_preferred(4) == 2 assert m.ints_preferred(True) == 0 - with pytest.raises(TypeError) as excinfo: - m.ints_preferred(4.0) - assert ( - msg(excinfo.value) - == """ - ints_preferred(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt) -> int - - Invoked with: 4.0 - """ - ) + m.ints_preferred(4.0) == 2 assert m.ints_only(4) == 2 with pytest.raises(TypeError) as excinfo: diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 6a8d993cb6..968b2ba138 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -543,7 +543,7 @@ def test_overload_ordering(): ) with pytest.raises(TypeError) as err: - m.overload_order(1.1) + m.overload_order([]) assert "1. (arg0: typing.SupportsInt) -> int" in str(err.value) assert "2. (arg0: str) -> int" in str(err.value) diff --git a/tests/test_stl.py b/tests/test_stl.py index 4a57635e27..6ac31000ab 100644 --- a/tests/test_stl.py +++ b/tests/test_stl.py @@ -422,7 +422,7 @@ def test_missing_header_message(): ) with pytest.raises(TypeError) as excinfo: - cm.missing_header_arg([1.0, 2.0, 3.0]) + cm.missing_header_arg([1.0, "bar", 3.0]) assert expected_message in str(excinfo.value) with pytest.raises(TypeError) as excinfo: @@ -491,7 +491,7 @@ def test_pass_std_vector_pair_int(): def test_list_caster_fully_consumes_generator_object(): def gen_invalid(): - yield from [1, 2.0, 3] + yield from [1, "bar", 3] gen_obj = gen_invalid() with pytest.raises(TypeError): @@ -514,15 +514,15 @@ def test_pass_std_set_int(): def test_set_caster_dict_keys_failure(): - dict_keys = {1: None, 2.0: None, 3: None}.keys() + dict_keys = {1: None, "bar": None, 3: None}.keys() # The asserts does not really exercise anything in pybind11, but if one of # them fails in some future version of Python, the set_caster load # implementation may need to be revisited. - assert tuple(dict_keys) == (1, 2.0, 3) - assert tuple(dict_keys) == (1, 2.0, 3) + assert tuple(dict_keys) == (1, "bar", 3) + assert tuple(dict_keys) == (1, "bar", 3) with pytest.raises(TypeError): m.pass_std_set_int(dict_keys) - assert tuple(dict_keys) == (1, 2.0, 3) + assert tuple(dict_keys) == (1, "bar", 3) class FakePyMappingMissingItems: From 358266f27db6c334a9138293d597a0d8101c2a34 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 24 Oct 2025 11:51:34 -0700 Subject: [PATCH 18/45] Update complex_cast tests --- include/pybind11/complex.h | 2 +- tests/test_builtin_casters.cpp | 3 ++ tests/test_builtin_casters.py | 50 +++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/include/pybind11/complex.h b/include/pybind11/complex.h index 8fea1c0065..cc6ad81324 100644 --- a/include/pybind11/complex.h +++ b/include/pybind11/complex.h @@ -70,7 +70,7 @@ class type_caster> { return PyComplex_FromDoubles((double) src.real(), (double) src.imag()); } - PYBIND11_TYPE_CASTER(std::complex, const_name("complex")); + PYBIND11_TYPE_CASTER(std::complex, io_name("typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex", "complex")); }; PYBIND11_NAMESPACE_END(detail) PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index 123db2986b..4a3cda6a17 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -368,6 +368,9 @@ TEST_SUBMODULE(builtin_casters, m) { [](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 int vs. long (Python 2) m.def("int_cast", []() { return (int) 42; }); m.def("long_cast", []() { return (long) 42; }); diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 25fe5b4a56..1a560efd84 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -469,9 +469,32 @@ def test_reference_wrapper(): assert m.refwrap_call_iiw(IncType(10), m.refwrap_iiw) == [10, 10, 10, 10] -def test_complex_cast(): +def test_complex_cast(doc): """std::complex casts""" + + class Complex: + + def __complex__(self) -> complex: + return complex(5, 4) + + class Float: + + def __float__(self) -> float: + return 5.0 + + class Int: + + def __int__(self) -> int: + return 3 + + class Index: + + def __index__(self) -> int: + return 1 + assert m.complex_cast(1) == "1.0" + assert m.complex_cast(1.0) == "1.0" + 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)" @@ -479,6 +502,31 @@ def test_complex_cast(): 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): + pytest.raises(TypeError, noconvert, v) + + def cant_convert(v): + pytest.raises(TypeError, convert, v) + + assert doc(convert) == "complex_convert(arg0: typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex) -> complex" + assert doc(noconvert) == "complex_noconvert(arg0: complex) -> complex" + + assert convert(1) == 1.0 + assert convert(2.0) == 2.0 + assert convert(1 + 5j) == 1.0 + 5.0j + assert convert(Complex()) == 5.0 + 4j + assert convert(Float()) == 5.0 + cant_convert(Int()) + assert convert(Index()) == 1 + + 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_bool_caster(): """Test bool caster implicit conversions.""" From dadbf059c7311459f01832b0df084332661dc59c Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 24 Oct 2025 12:15:05 -0700 Subject: [PATCH 19/45] Add SupportsIndex to int and float --- include/pybind11/cast.h | 2 +- tests/test_builtin_casters.py | 12 +++++-- tests/test_callbacks.py | 4 +-- tests/test_class.py | 4 +-- tests/test_docstring_options.py | 6 ++-- tests/test_enum.py | 2 +- tests/test_factory_constructors.py | 8 ++--- tests/test_kwargs_and_defaults.py | 48 +++++++++++++------------- tests/test_methods_and_attributes.py | 14 ++++---- tests/test_numpy_dtypes.py | 2 +- tests/test_numpy_vectorize.py | 4 +-- tests/test_pytypes.py | 24 ++++++------- tests/test_stl.py | 22 ++++++------ tests/test_type_caster_pyobject_ptr.py | 2 +- 14 files changed, 80 insertions(+), 74 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index a51b172f74..dcb8140148 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -348,7 +348,7 @@ struct type_caster::value && !is_std_char_t PYBIND11_TYPE_CASTER(T, io_name::value>( - "typing.SupportsInt", "int", "typing.SupportsFloat", "float")); + "typing.SupportsInt | typing.SupportsIndex", "int", "typing.SupportsFloat | typing.SupportsIndex", "float")); }; template diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 1a560efd84..3c94afbf6c 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -286,7 +286,7 @@ def __int__(self): convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert - assert doc(convert) == "int_passthrough(arg0: typing.SupportsInt) -> int" + assert doc(convert) == "int_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" assert doc(noconvert) == "int_passthrough_noconvert(arg0: int) -> int" def requires_conversion(v): @@ -326,13 +326,17 @@ def test_float_convert(doc): class Int: def __int__(self): return -5 + + class Index: + def __index__(self) -> int: + return -7 class Float: def __float__(self): return 41.45 convert, noconvert = m.float_passthrough, m.float_passthrough_noconvert - assert doc(convert) == "float_passthrough(arg0: typing.SupportsFloat) -> float" + assert doc(convert) == "float_passthrough(arg0: typing.SupportsFloat | typing.SupportsIndex) -> float" assert doc(noconvert) == "float_passthrough_noconvert(arg0: float) -> float" def requires_conversion(v): @@ -342,7 +346,9 @@ def cant_convert(v): pytest.raises(TypeError, convert, v) requires_conversion(Float()) + requires_conversion(Index()) assert pytest.approx(convert(Float())) == 41.45 + assert pytest.approx(convert(Index())) == -7.0 assert pytest.approx(convert(3)) == 3.0 assert pytest.approx(noconvert(3)) == 3.0 cant_convert(Int()) @@ -392,7 +398,7 @@ def test_tuple(doc): assert ( doc(m.tuple_passthrough) == """ - tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt]) -> tuple[int, str, bool] + tuple_passthrough(arg0: tuple[bool, str, typing.SupportsInt | typing.SupportsIndex]) -> tuple[int, str, bool] Return a triple in reversed order """ diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index 09dadd94c3..c0a57a7b86 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -140,11 +140,11 @@ def test_cpp_function_roundtrip(): def test_function_signatures(doc): assert ( doc(m.test_callback3) - == "test_callback3(arg0: collections.abc.Callable[[typing.SupportsInt], int]) -> str" + == "test_callback3(arg0: collections.abc.Callable[[typing.SupportsInt | typing.SupportsIndex], int]) -> str" ) assert ( doc(m.test_callback4) - == "test_callback4() -> collections.abc.Callable[[typing.SupportsInt], int]" + == "test_callback4() -> collections.abc.Callable[[typing.SupportsInt | typing.SupportsIndex], int]" ) diff --git a/tests/test_class.py b/tests/test_class.py index 1e82930361..fae6a31899 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -163,13 +163,13 @@ def test_qualname(doc): assert ( doc(m.NestBase.Nested.fn) == """ - fn(self: m.class_.NestBase.Nested, arg0: typing.SupportsInt, arg1: m.class_.NestBase, arg2: m.class_.NestBase.Nested) -> None + fn(self: m.class_.NestBase.Nested, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: m.class_.NestBase, arg2: m.class_.NestBase.Nested) -> None """ ) assert ( doc(m.NestBase.Nested.fa) == """ - fa(self: m.class_.NestBase.Nested, a: typing.SupportsInt, b: m.class_.NestBase, c: m.class_.NestBase.Nested) -> None + fa(self: m.class_.NestBase.Nested, a: typing.SupportsInt | typing.SupportsIndex, b: m.class_.NestBase, c: m.class_.NestBase.Nested) -> None """ ) assert m.NestBase.__module__ == "pybind11_tests.class_" diff --git a/tests/test_docstring_options.py b/tests/test_docstring_options.py index f2a10480ca..802a1ec9e5 100644 --- a/tests/test_docstring_options.py +++ b/tests/test_docstring_options.py @@ -20,11 +20,11 @@ def test_docstring_options(): # options.enable_function_signatures() assert m.test_function3.__doc__.startswith( - "test_function3(a: typing.SupportsInt, b: typing.SupportsInt) -> None" + "test_function3(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None" ) assert m.test_function4.__doc__.startswith( - "test_function4(a: typing.SupportsInt, b: typing.SupportsInt) -> None" + "test_function4(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None" ) assert m.test_function4.__doc__.endswith("A custom docstring\n") @@ -37,7 +37,7 @@ def test_docstring_options(): # RAII destructor assert m.test_function7.__doc__.startswith( - "test_function7(a: typing.SupportsInt, b: typing.SupportsInt) -> None" + "test_function7(a: typing.SupportsInt | typing.SupportsIndex, b: typing.SupportsInt | typing.SupportsIndex) -> None" ) assert m.test_function7.__doc__.endswith("A custom docstring\n") diff --git a/tests/test_enum.py b/tests/test_enum.py index 99d4a88c8a..aede6524b4 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -328,7 +328,7 @@ def test_generated_dunder_methods_pos_only(): ) assert ( re.match( - r"^__setstate__\(self: [\w\.]+, state: [\w\.]+, /\)", + r"^__setstate__\(self: [\w\.]+, state: [\w\.\| ]+, /\)", enum_type.__setstate__.__doc__, ) is not None diff --git a/tests/test_factory_constructors.py b/tests/test_factory_constructors.py index 02c5724516..635d762bb3 100644 --- a/tests/test_factory_constructors.py +++ b/tests/test_factory_constructors.py @@ -78,10 +78,10 @@ def test_init_factory_signature(msg): msg(excinfo.value) == """ __init__(): incompatible constructor arguments. The following argument types are supported: - 1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt) + 1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt | typing.SupportsIndex) 2. m.factory_constructors.TestFactory1(arg0: str) 3. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.pointer_tag) - 4. m.factory_constructors.TestFactory1(arg0: object, arg1: typing.SupportsInt, arg2: object) + 4. m.factory_constructors.TestFactory1(arg0: object, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: object) Invoked with: 'invalid', 'constructor', 'arguments' """ @@ -93,13 +93,13 @@ def test_init_factory_signature(msg): __init__(*args, **kwargs) Overloaded function. - 1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt) -> None + 1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: typing.SupportsInt | typing.SupportsIndex) -> None 2. __init__(self: m.factory_constructors.TestFactory1, arg0: str) -> None 3. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.pointer_tag) -> None - 4. __init__(self: m.factory_constructors.TestFactory1, arg0: object, arg1: typing.SupportsInt, arg2: object) -> None + 4. __init__(self: m.factory_constructors.TestFactory1, arg0: object, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: object) -> None """ ) diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py index b62e4b7412..57345f128f 100644 --- a/tests/test_kwargs_and_defaults.py +++ b/tests/test_kwargs_and_defaults.py @@ -9,28 +9,28 @@ def test_function_signatures(doc): assert ( doc(m.kw_func0) - == "kw_func0(arg0: typing.SupportsInt, arg1: typing.SupportsInt) -> str" + == "kw_func0(arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> str" ) assert ( doc(m.kw_func1) - == "kw_func1(x: typing.SupportsInt, y: typing.SupportsInt) -> str" + == "kw_func1(x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsInt | typing.SupportsIndex) -> str" ) assert ( doc(m.kw_func2) - == "kw_func2(x: typing.SupportsInt = 100, y: typing.SupportsInt = 200) -> str" + == "kw_func2(x: typing.SupportsInt | typing.SupportsIndex = 100, y: typing.SupportsInt | typing.SupportsIndex = 200) -> str" ) assert doc(m.kw_func3) == "kw_func3(data: str = 'Hello world!') -> None" assert ( doc(m.kw_func4) - == "kw_func4(myList: collections.abc.Sequence[typing.SupportsInt] = [13, 17]) -> str" + == "kw_func4(myList: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex] = [13, 17]) -> str" ) assert ( doc(m.kw_func_udl) - == "kw_func_udl(x: typing.SupportsInt, y: typing.SupportsInt = 300) -> str" + == "kw_func_udl(x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsInt | typing.SupportsIndex = 300) -> str" ) assert ( doc(m.kw_func_udl_z) - == "kw_func_udl_z(x: typing.SupportsInt, y: typing.SupportsInt = 0) -> str" + == "kw_func_udl_z(x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsInt | typing.SupportsIndex = 0) -> str" ) assert doc(m.args_function) == "args_function(*args) -> tuple" assert ( @@ -42,11 +42,11 @@ def test_function_signatures(doc): ) assert ( doc(m.KWClass.foo0) - == "foo0(self: m.kwargs_and_defaults.KWClass, arg0: typing.SupportsInt, arg1: typing.SupportsFloat) -> None" + == "foo0(self: m.kwargs_and_defaults.KWClass, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex) -> None" ) assert ( doc(m.KWClass.foo1) - == "foo1(self: m.kwargs_and_defaults.KWClass, x: typing.SupportsInt, y: typing.SupportsFloat) -> None" + == "foo1(self: m.kwargs_and_defaults.KWClass, x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsFloat | typing.SupportsIndex) -> None" ) assert ( doc(m.kw_lb_func0) @@ -138,7 +138,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args(): incompatible function arguments. The following argument types are supported: - 1. (arg0: typing.SupportsInt, arg1: typing.SupportsFloat, *args) -> tuple + 1. (arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex, *args) -> tuple Invoked with: 1 """ @@ -149,7 +149,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args(): incompatible function arguments. The following argument types are supported: - 1. (arg0: typing.SupportsInt, arg1: typing.SupportsFloat, *args) -> tuple + 1. (arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex, *args) -> tuple Invoked with: """ @@ -183,7 +183,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt = 1, j: typing.SupportsFloat = 3.14159, *args, **kwargs) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, **kwargs) -> tuple Invoked with: 1; kwargs: i=1 """ @@ -194,7 +194,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt = 1, j: typing.SupportsFloat = 3.14159, *args, **kwargs) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, **kwargs) -> tuple Invoked with: 1, 2; kwargs: j=1 """ @@ -211,7 +211,7 @@ def test_mixed_args_and_kwargs(msg): msg(excinfo.value) == """ args_kwonly(): incompatible function arguments. The following argument types are supported: - 1. (i: typing.SupportsInt, j: typing.SupportsFloat, *args, z: typing.SupportsInt) -> tuple + 1. (i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsFloat | typing.SupportsIndex, *args, z: typing.SupportsInt | typing.SupportsIndex) -> tuple Invoked with: 2, 2.5, 22 """ @@ -233,12 +233,12 @@ def test_mixed_args_and_kwargs(msg): ) assert ( m.args_kwonly_kwargs.__doc__ - == "args_kwonly_kwargs(i: typing.SupportsInt, j: typing.SupportsFloat, *args, z: typing.SupportsInt, **kwargs) -> tuple\n" + == "args_kwonly_kwargs(i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsFloat | typing.SupportsIndex, *args, z: typing.SupportsInt | typing.SupportsIndex, **kwargs) -> tuple\n" ) assert ( m.args_kwonly_kwargs_defaults.__doc__ - == "args_kwonly_kwargs_defaults(i: typing.SupportsInt = 1, j: typing.SupportsFloat = 3.14159, *args, z: typing.SupportsInt = 42, **kwargs) -> tuple\n" + == "args_kwonly_kwargs_defaults(i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, z: typing.SupportsInt | typing.SupportsIndex = 42, **kwargs) -> tuple\n" ) assert m.args_kwonly_kwargs_defaults() == (1, 3.14159, (), 42, {}) assert m.args_kwonly_kwargs_defaults(2) == (2, 3.14159, (), 42, {}) @@ -294,11 +294,11 @@ def test_keyword_only_args(msg): x.method(i=1, j=2) assert ( m.first_arg_kw_only.__init__.__doc__ - == "__init__(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt = 0) -> None\n" + == "__init__(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt | typing.SupportsIndex = 0) -> None\n" ) assert ( m.first_arg_kw_only.method.__doc__ - == "method(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt = 1, j: typing.SupportsInt = 2) -> None\n" + == "method(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: typing.SupportsInt | typing.SupportsIndex = 1, j: typing.SupportsInt | typing.SupportsIndex = 2) -> None\n" ) @@ -344,7 +344,7 @@ def test_positional_only_args(): # Mix it with args and kwargs: assert ( m.args_kwonly_full_monty.__doc__ - == "args_kwonly_full_monty(arg0: typing.SupportsInt = 1, arg1: typing.SupportsInt = 2, /, j: typing.SupportsFloat = 3.14159, *args, z: typing.SupportsInt = 42, **kwargs) -> tuple\n" + == "args_kwonly_full_monty(arg0: typing.SupportsInt | typing.SupportsIndex = 1, arg1: typing.SupportsInt | typing.SupportsIndex = 2, /, j: typing.SupportsFloat | typing.SupportsIndex = 3.14159, *args, z: typing.SupportsInt | typing.SupportsIndex = 42, **kwargs) -> tuple\n" ) assert m.args_kwonly_full_monty() == (1, 2, 3.14159, (), 42, {}) assert m.args_kwonly_full_monty(8) == (8, 2, 3.14159, (), 42, {}) @@ -387,30 +387,30 @@ def test_positional_only_args(): # https://github.com/pybind/pybind11/pull/3402#issuecomment-963341987 assert ( m.first_arg_kw_only.pos_only.__doc__ - == "pos_only(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, /, i: typing.SupportsInt, j: typing.SupportsInt) -> None\n" + == "pos_only(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, /, i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex) -> None\n" ) def test_signatures(): assert ( m.kw_only_all.__doc__ - == "kw_only_all(*, i: typing.SupportsInt, j: typing.SupportsInt) -> tuple\n" + == "kw_only_all(*, i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" ) assert ( m.kw_only_mixed.__doc__ - == "kw_only_mixed(i: typing.SupportsInt, *, j: typing.SupportsInt) -> tuple\n" + == "kw_only_mixed(i: typing.SupportsInt | typing.SupportsIndex, *, j: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" ) assert ( m.pos_only_all.__doc__ - == "pos_only_all(i: typing.SupportsInt, j: typing.SupportsInt, /) -> tuple\n" + == "pos_only_all(i: typing.SupportsInt | typing.SupportsIndex, j: typing.SupportsInt | typing.SupportsIndex, /) -> tuple\n" ) assert ( m.pos_only_mix.__doc__ - == "pos_only_mix(i: typing.SupportsInt, /, j: typing.SupportsInt) -> tuple\n" + == "pos_only_mix(i: typing.SupportsInt | typing.SupportsIndex, /, j: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" ) assert ( m.pos_kw_only_mix.__doc__ - == "pos_kw_only_mix(i: typing.SupportsInt, /, j: typing.SupportsInt, *, k: typing.SupportsInt) -> tuple\n" + == "pos_kw_only_mix(i: typing.SupportsInt | typing.SupportsIndex, /, j: typing.SupportsInt | typing.SupportsIndex, *, k: typing.SupportsInt | typing.SupportsIndex) -> tuple\n" ) diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 968b2ba138..7adfe9a71d 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -251,7 +251,7 @@ def test_no_mixed_overloads(): "#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for more details" if not detailed_error_messages_enabled else "error while attempting to bind static method ExampleMandA.overload_mixed1" - "(arg0: typing.SupportsFloat) -> str" + "(arg0: typing.SupportsFloat | typing.SupportsIndex) -> str" ) ) @@ -264,7 +264,7 @@ def test_no_mixed_overloads(): "#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for more details" if not detailed_error_messages_enabled else "error while attempting to bind instance method ExampleMandA.overload_mixed2" - "(self: pybind11_tests.methods_and_attributes.ExampleMandA, arg0: typing.SupportsInt, arg1: typing.SupportsInt)" + "(self: pybind11_tests.methods_and_attributes.ExampleMandA, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex)" " -> str" ) ) @@ -491,7 +491,7 @@ def test_str_issue(msg): msg(excinfo.value) == """ __init__(): incompatible constructor arguments. The following argument types are supported: - 1. m.methods_and_attributes.StrIssue(arg0: typing.SupportsInt) + 1. m.methods_and_attributes.StrIssue(arg0: typing.SupportsInt | typing.SupportsIndex) 2. m.methods_and_attributes.StrIssue() Invoked with: 'no', 'such', 'constructor' @@ -534,21 +534,21 @@ def test_overload_ordering(): assert m.overload_order(0) == 4 assert ( - "1. overload_order(arg0: typing.SupportsInt) -> int" in m.overload_order.__doc__ + "1. overload_order(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in m.overload_order.__doc__ ) assert "2. overload_order(arg0: str) -> int" in m.overload_order.__doc__ assert "3. overload_order(arg0: str) -> int" in m.overload_order.__doc__ assert ( - "4. overload_order(arg0: typing.SupportsInt) -> int" in m.overload_order.__doc__ + "4. overload_order(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in m.overload_order.__doc__ ) with pytest.raises(TypeError) as err: m.overload_order([]) - assert "1. (arg0: typing.SupportsInt) -> int" in str(err.value) + assert "1. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str(err.value) assert "2. (arg0: str) -> int" in str(err.value) assert "3. (arg0: str) -> int" in str(err.value) - assert "4. (arg0: typing.SupportsInt) -> int" in str(err.value) + assert "4. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str(err.value) def test_rvalue_ref_param(): diff --git a/tests/test_numpy_dtypes.py b/tests/test_numpy_dtypes.py index 9f85742809..22814aba5a 100644 --- a/tests/test_numpy_dtypes.py +++ b/tests/test_numpy_dtypes.py @@ -367,7 +367,7 @@ def test_complex_array(): def test_signature(doc): assert ( doc(m.create_rec_nested) - == "create_rec_nested(arg0: typing.SupportsInt) -> numpy.typing.NDArray[NestedStruct]" + == "create_rec_nested(arg0: typing.SupportsInt | typing.SupportsIndex) -> numpy.typing.NDArray[NestedStruct]" ) diff --git a/tests/test_numpy_vectorize.py b/tests/test_numpy_vectorize.py index d405e68002..05f7c704f5 100644 --- a/tests/test_numpy_vectorize.py +++ b/tests/test_numpy_vectorize.py @@ -211,11 +211,11 @@ def test_passthrough_arguments(doc): "vec_passthrough(" + ", ".join( [ - "arg0: typing.SupportsFloat", + "arg0: typing.SupportsFloat | typing.SupportsIndex", "arg1: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", "arg2: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", "arg3: typing.Annotated[numpy.typing.ArrayLike, numpy.int32]", - "arg4: typing.SupportsInt", + "arg4: typing.SupportsInt | typing.SupportsIndex", "arg5: m.numpy_vectorize.NonPODClass", "arg6: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", ] diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index c1798f924c..8ac621867d 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -944,14 +944,14 @@ def test_tuple_variable_length_annotations(doc): def test_dict_annotations(doc): assert ( doc(m.annotate_dict_str_int) - == "annotate_dict_str_int(arg0: dict[str, typing.SupportsInt]) -> None" + == "annotate_dict_str_int(arg0: dict[str, typing.SupportsInt | typing.SupportsIndex]) -> None" ) def test_list_annotations(doc): assert ( doc(m.annotate_list_int) - == "annotate_list_int(arg0: list[typing.SupportsInt]) -> None" + == "annotate_list_int(arg0: list[typing.SupportsInt | typing.SupportsIndex]) -> None" ) @@ -969,7 +969,7 @@ def test_iterable_annotations(doc): def test_iterator_annotations(doc): assert ( doc(m.annotate_iterator_int) - == "annotate_iterator_int(arg0: collections.abc.Iterator[typing.SupportsInt]) -> None" + == "annotate_iterator_int(arg0: collections.abc.Iterator[typing.SupportsInt | typing.SupportsIndex]) -> None" ) @@ -989,7 +989,7 @@ def test_fn_return_only(doc): def test_type_annotation(doc): assert ( - doc(m.annotate_type) == "annotate_type(arg0: type[typing.SupportsInt]) -> type" + doc(m.annotate_type) == "annotate_type(arg0: type[typing.SupportsInt | typing.SupportsIndex]) -> type" ) @@ -1007,7 +1007,7 @@ def test_union_typing_only(doc): def test_union_object_annotations(doc): assert ( doc(m.annotate_union_to_object) - == "annotate_union_to_object(arg0: typing.SupportsInt | str) -> object" + == "annotate_union_to_object(arg0: typing.SupportsInt | typing.SupportsIndex | str) -> object" ) @@ -1044,7 +1044,7 @@ def test_never_annotation(doc, backport_typehints): def test_optional_object_annotations(doc): assert ( doc(m.annotate_optional_to_object) - == "annotate_optional_to_object(arg0: typing.SupportsInt | None) -> object" + == "annotate_optional_to_object(arg0: typing.SupportsInt | typing.SupportsIndex | None) -> object" ) @@ -1167,7 +1167,7 @@ def get_annotations_helper(o): def test_module_attribute_types() -> None: module_annotations = get_annotations_helper(m) - assert module_annotations["list_int"] == "list[typing.SupportsInt]" + assert module_annotations["list_int"] == "list[typing.SupportsInt | typing.SupportsIndex]" assert module_annotations["set_str"] == "set[str]" assert module_annotations["foo"] == "pybind11_tests.pytypes.foo" @@ -1190,7 +1190,7 @@ def test_get_annotations_compliance() -> None: module_annotations = get_annotations(m) - assert module_annotations["list_int"] == "list[typing.SupportsInt]" + assert module_annotations["list_int"] == "list[typing.SupportsInt | typing.SupportsIndex]" assert module_annotations["set_str"] == "set[str]" @@ -1204,10 +1204,10 @@ def test_class_attribute_types() -> None: instance_annotations = get_annotations_helper(m.Instance) assert empty_annotations is None - assert static_annotations["x"] == "typing.ClassVar[typing.SupportsFloat]" + assert static_annotations["x"] == "typing.ClassVar[typing.SupportsFloat | typing.SupportsIndex]" assert ( static_annotations["dict_str_int"] - == "typing.ClassVar[dict[str, typing.SupportsInt]]" + == "typing.ClassVar[dict[str, typing.SupportsInt | typing.SupportsIndex]]" ) assert m.Static.x == 1.0 @@ -1219,7 +1219,7 @@ def test_class_attribute_types() -> None: static.dict_str_int["hi"] = 3 assert m.Static().dict_str_int == {"hi": 3} - assert instance_annotations["y"] == "typing.SupportsFloat" + assert instance_annotations["y"] == "typing.SupportsFloat | typing.SupportsIndex" instance1 = m.Instance() instance1.y = 4.0 @@ -1236,7 +1236,7 @@ def test_class_attribute_types() -> None: def test_redeclaration_attr_with_type_hint() -> None: obj = m.Instance() m.attr_with_type_hint_float_x(obj) - assert get_annotations_helper(obj)["x"] == "typing.SupportsFloat" + assert get_annotations_helper(obj)["x"] == "typing.SupportsFloat | typing.SupportsIndex" with pytest.raises( RuntimeError, match=r'^__annotations__\["x"\] was set already\.$' ): diff --git a/tests/test_stl.py b/tests/test_stl.py index 6ac31000ab..fb42636771 100644 --- a/tests/test_stl.py +++ b/tests/test_stl.py @@ -22,7 +22,7 @@ def test_vector(doc): assert doc(m.cast_vector) == "cast_vector() -> list[int]" assert ( doc(m.load_vector) - == "load_vector(arg0: collections.abc.Sequence[typing.SupportsInt]) -> bool" + == "load_vector(arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> bool" ) # Test regression caused by 936: pointers to stl containers weren't castable @@ -51,7 +51,7 @@ def test_array(doc): ) assert ( doc(m.load_array) - == 'load_array(arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsInt], "FixedSize(2)"]) -> bool' + == 'load_array(arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"]) -> bool' ) @@ -72,7 +72,7 @@ def test_valarray(doc): assert doc(m.cast_valarray) == "cast_valarray() -> list[int]" assert ( doc(m.load_valarray) - == "load_valarray(arg0: collections.abc.Sequence[typing.SupportsInt]) -> bool" + == "load_valarray(arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> bool" ) @@ -234,7 +234,7 @@ def test_reference_sensitive_optional(doc): assert ( doc(m.double_or_zero_refsensitive) - == "double_or_zero_refsensitive(arg0: typing.SupportsInt | None) -> int" + == "double_or_zero_refsensitive(arg0: typing.SupportsInt | typing.SupportsIndex | None) -> int" ) assert m.half_or_none_refsensitive(0) is None @@ -352,7 +352,7 @@ def test_variant(doc): assert ( doc(m.load_variant) - == "load_variant(arg0: typing.SupportsInt | str | typing.SupportsFloat | None) -> str" + == "load_variant(arg0: typing.SupportsInt | typing.SupportsIndex | str | typing.SupportsFloat | typing.SupportsIndex | None) -> str" ) @@ -368,7 +368,7 @@ def test_variant_monostate(doc): assert ( doc(m.load_monostate_variant) - == "load_monostate_variant(arg0: None | typing.SupportsInt | str) -> str" + == "load_monostate_variant(arg0: None | typing.SupportsInt | typing.SupportsIndex | str) -> str" ) @@ -388,7 +388,7 @@ def test_stl_pass_by_pointer(msg): msg(excinfo.value) == """ stl_pass_by_pointer(): incompatible function arguments. The following argument types are supported: - 1. (v: collections.abc.Sequence[typing.SupportsInt] = None) -> list[int] + 1. (v: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex] = None) -> list[int] Invoked with: """ @@ -400,7 +400,7 @@ def test_stl_pass_by_pointer(msg): msg(excinfo.value) == """ stl_pass_by_pointer(): incompatible function arguments. The following argument types are supported: - 1. (v: collections.abc.Sequence[typing.SupportsInt] = None) -> list[int] + 1. (v: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex] = None) -> list[int] Invoked with: None """ @@ -615,7 +615,7 @@ class FormalSequenceLike(BareSequenceLike, Sequence): # convert mode assert ( doc(m.roundtrip_std_vector_int) - == "roundtrip_std_vector_int(arg0: collections.abc.Sequence[typing.SupportsInt]) -> list[int]" + == "roundtrip_std_vector_int(arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> list[int]" ) assert m.roundtrip_std_vector_int([1, 2, 3]) == [1, 2, 3] assert m.roundtrip_std_vector_int((1, 2, 3)) == [1, 2, 3] @@ -668,7 +668,7 @@ class FormalMappingLike(BareMappingLike, Mapping): # convert mode assert ( doc(m.roundtrip_std_map_str_int) - == "roundtrip_std_map_str_int(arg0: collections.abc.Mapping[str, typing.SupportsInt]) -> dict[str, int]" + == "roundtrip_std_map_str_int(arg0: collections.abc.Mapping[str, typing.SupportsInt | typing.SupportsIndex]) -> dict[str, int]" ) assert m.roundtrip_std_map_str_int(a1b2c3) == a1b2c3 assert m.roundtrip_std_map_str_int(FormalMappingLike(**a1b2c3)) == a1b2c3 @@ -714,7 +714,7 @@ class FormalSetLike(BareSetLike, Set): # convert mode assert ( doc(m.roundtrip_std_set_int) - == "roundtrip_std_set_int(arg0: collections.abc.Set[typing.SupportsInt]) -> set[int]" + == "roundtrip_std_set_int(arg0: collections.abc.Set[typing.SupportsInt | typing.SupportsIndex]) -> set[int]" ) assert m.roundtrip_std_set_int({1, 2, 3}) == {1, 2, 3} assert m.roundtrip_std_set_int(FormalSetLike(1, 2, 3)) == {1, 2, 3} diff --git a/tests/test_type_caster_pyobject_ptr.py b/tests/test_type_caster_pyobject_ptr.py index 5df8ca0196..c69c9440fe 100644 --- a/tests/test_type_caster_pyobject_ptr.py +++ b/tests/test_type_caster_pyobject_ptr.py @@ -103,7 +103,7 @@ def test_return_list_pyobject_ptr_reference(): def test_type_caster_name_via_incompatible_function_arguments_type_error(): with pytest.raises( - TypeError, match=r"1\. \(arg0: object, arg1: typing.SupportsInt\) -> None" + TypeError, match=r"1\. \(arg0: object, arg1: typing.SupportsInt | typing.SupportsIndex\) -> None" ): m.pass_pyobject_ptr_and_int(ValueHolder(101), ValueHolder(202)) From 248b12e0373205a31b090707e0af79bff710d644 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 19:15:39 +0000 Subject: [PATCH 20/45] style: pre-commit fixes --- include/pybind11/cast.h | 9 ++++++--- include/pybind11/complex.h | 5 ++++- tests/test_builtin_casters.cpp | 4 ++-- tests/test_builtin_casters.py | 28 ++++++++++++++++---------- tests/test_methods_and_attributes.py | 14 +++++++++---- tests/test_pytypes.py | 23 ++++++++++++++++----- tests/test_type_caster_pyobject_ptr.py | 3 ++- 7 files changed, 59 insertions(+), 27 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index dcb8140148..9282eb068f 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -346,9 +346,12 @@ struct type_caster::value && !is_std_char_t return PyLong_FromUnsignedLongLong((unsigned long long) src); } - PYBIND11_TYPE_CASTER(T, - io_name::value>( - "typing.SupportsInt | typing.SupportsIndex", "int", "typing.SupportsFloat | typing.SupportsIndex", "float")); + PYBIND11_TYPE_CASTER( + T, + io_name::value>("typing.SupportsInt | typing.SupportsIndex", + "int", + "typing.SupportsFloat | typing.SupportsIndex", + "float")); }; template diff --git a/include/pybind11/complex.h b/include/pybind11/complex.h index cc6ad81324..9efad5edce 100644 --- a/include/pybind11/complex.h +++ b/include/pybind11/complex.h @@ -70,7 +70,10 @@ class type_caster> { return PyComplex_FromDoubles((double) src.real(), (double) src.imag()); } - PYBIND11_TYPE_CASTER(std::complex, io_name("typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex", "complex")); + PYBIND11_TYPE_CASTER( + std::complex, + io_name("typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex", + "complex")); }; PYBIND11_NAMESPACE_END(detail) PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index 4a3cda6a17..7e6b97d14d 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -368,8 +368,8 @@ TEST_SUBMODULE(builtin_casters, m) { [](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()); + m.def("complex_convert", [](std::complex x) { return x; }); + m.def("complex_noconvert", [](std::complex x) { return x; }, py::arg{}.noconvert()); // test int vs. long (Python 2) m.def("int_cast", []() { return (int) 42; }); diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 3c94afbf6c..e078c73259 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -286,7 +286,10 @@ def __int__(self): convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert - assert doc(convert) == "int_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" + assert ( + doc(convert) + == "int_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" + ) assert doc(noconvert) == "int_passthrough_noconvert(arg0: int) -> int" def requires_conversion(v): @@ -326,7 +329,7 @@ def test_float_convert(doc): class Int: def __int__(self): return -5 - + class Index: def __index__(self) -> int: return -7 @@ -336,7 +339,10 @@ def __float__(self): return 41.45 convert, noconvert = m.float_passthrough, m.float_passthrough_noconvert - assert doc(convert) == "float_passthrough(arg0: typing.SupportsFloat | typing.SupportsIndex) -> float" + assert ( + doc(convert) + == "float_passthrough(arg0: typing.SupportsFloat | typing.SupportsIndex) -> float" + ) assert doc(noconvert) == "float_passthrough_noconvert(arg0: float) -> float" def requires_conversion(v): @@ -479,22 +485,18 @@ def test_complex_cast(doc): """std::complex casts""" class Complex: - def __complex__(self) -> complex: return complex(5, 4) - - class Float: + class Float: def __float__(self) -> float: return 5.0 - - class Int: + class Int: def __int__(self) -> int: return 3 - - class Index: + class Index: def __index__(self) -> int: return 1 @@ -516,7 +518,10 @@ def requires_conversion(v): def cant_convert(v): pytest.raises(TypeError, convert, v) - assert doc(convert) == "complex_convert(arg0: typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex) -> complex" + assert ( + doc(convert) + == "complex_convert(arg0: typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex) -> complex" + ) assert doc(noconvert) == "complex_noconvert(arg0: complex) -> complex" assert convert(1) == 1.0 @@ -534,6 +539,7 @@ def cant_convert(v): requires_conversion(Float()) requires_conversion(Index()) + def test_bool_caster(): """Test bool caster implicit conversions.""" convert, noconvert = m.bool_passthrough, m.bool_passthrough_noconvert diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 7adfe9a71d..647580de9b 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -534,21 +534,27 @@ def test_overload_ordering(): assert m.overload_order(0) == 4 assert ( - "1. overload_order(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in m.overload_order.__doc__ + "1. overload_order(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" + in m.overload_order.__doc__ ) assert "2. overload_order(arg0: str) -> int" in m.overload_order.__doc__ assert "3. overload_order(arg0: str) -> int" in m.overload_order.__doc__ assert ( - "4. overload_order(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in m.overload_order.__doc__ + "4. overload_order(arg0: typing.SupportsInt | typing.SupportsIndex) -> int" + in m.overload_order.__doc__ ) with pytest.raises(TypeError) as err: m.overload_order([]) - assert "1. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str(err.value) + assert "1. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str( + err.value + ) assert "2. (arg0: str) -> int" in str(err.value) assert "3. (arg0: str) -> int" in str(err.value) - assert "4. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str(err.value) + assert "4. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str( + err.value + ) def test_rvalue_ref_param(): diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 8ac621867d..09fc5f37ee 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -989,7 +989,8 @@ def test_fn_return_only(doc): def test_type_annotation(doc): assert ( - doc(m.annotate_type) == "annotate_type(arg0: type[typing.SupportsInt | typing.SupportsIndex]) -> type" + doc(m.annotate_type) + == "annotate_type(arg0: type[typing.SupportsInt | typing.SupportsIndex]) -> type" ) @@ -1167,7 +1168,10 @@ def get_annotations_helper(o): def test_module_attribute_types() -> None: module_annotations = get_annotations_helper(m) - assert module_annotations["list_int"] == "list[typing.SupportsInt | typing.SupportsIndex]" + assert ( + module_annotations["list_int"] + == "list[typing.SupportsInt | typing.SupportsIndex]" + ) assert module_annotations["set_str"] == "set[str]" assert module_annotations["foo"] == "pybind11_tests.pytypes.foo" @@ -1190,7 +1194,10 @@ def test_get_annotations_compliance() -> None: module_annotations = get_annotations(m) - assert module_annotations["list_int"] == "list[typing.SupportsInt | typing.SupportsIndex]" + assert ( + module_annotations["list_int"] + == "list[typing.SupportsInt | typing.SupportsIndex]" + ) assert module_annotations["set_str"] == "set[str]" @@ -1204,7 +1211,10 @@ def test_class_attribute_types() -> None: instance_annotations = get_annotations_helper(m.Instance) assert empty_annotations is None - assert static_annotations["x"] == "typing.ClassVar[typing.SupportsFloat | typing.SupportsIndex]" + assert ( + static_annotations["x"] + == "typing.ClassVar[typing.SupportsFloat | typing.SupportsIndex]" + ) assert ( static_annotations["dict_str_int"] == "typing.ClassVar[dict[str, typing.SupportsInt | typing.SupportsIndex]]" @@ -1236,7 +1246,10 @@ def test_class_attribute_types() -> None: def test_redeclaration_attr_with_type_hint() -> None: obj = m.Instance() m.attr_with_type_hint_float_x(obj) - assert get_annotations_helper(obj)["x"] == "typing.SupportsFloat | typing.SupportsIndex" + assert ( + get_annotations_helper(obj)["x"] + == "typing.SupportsFloat | typing.SupportsIndex" + ) with pytest.raises( RuntimeError, match=r'^__annotations__\["x"\] was set already\.$' ): diff --git a/tests/test_type_caster_pyobject_ptr.py b/tests/test_type_caster_pyobject_ptr.py index c69c9440fe..3c608ce923 100644 --- a/tests/test_type_caster_pyobject_ptr.py +++ b/tests/test_type_caster_pyobject_ptr.py @@ -103,7 +103,8 @@ def test_return_list_pyobject_ptr_reference(): def test_type_caster_name_via_incompatible_function_arguments_type_error(): with pytest.raises( - TypeError, match=r"1\. \(arg0: object, arg1: typing.SupportsInt | typing.SupportsIndex\) -> None" + TypeError, + match=r"1\. \(arg0: object, arg1: typing.SupportsInt | typing.SupportsIndex\) -> None", ): m.pass_pyobject_ptr_and_int(ValueHolder(101), ValueHolder(202)) From 2163f507fc4e7d70224693385f862666abe7d318 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 24 Oct 2025 12:25:19 -0700 Subject: [PATCH 21/45] fix assert --- tests/test_custom_type_casters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_custom_type_casters.py b/tests/test_custom_type_casters.py index d8c8f8008f..76b3a8aa95 100644 --- a/tests/test_custom_type_casters.py +++ b/tests/test_custom_type_casters.py @@ -59,7 +59,7 @@ def test_noconvert_args(msg): assert m.ints_preferred(4) == 2 assert m.ints_preferred(True) == 0 - m.ints_preferred(4.0) == 2 + assert m.ints_preferred(4.0) == 2 assert m.ints_only(4) == 2 with pytest.raises(TypeError) as excinfo: From c13f21e824777b72fc2c91e195bb925e6fd86587 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 24 Oct 2025 12:30:32 -0700 Subject: [PATCH 22/45] Update docs to mention other conversions --- docs/advanced/functions.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst index 78455546d0..c9092cdabc 100644 --- a/docs/advanced/functions.rst +++ b/docs/advanced/functions.rst @@ -437,9 +437,9 @@ Certain argument types may support conversion from one type to another. Some examples of conversions are: * :ref:`implicit_conversions` declared using ``py::implicitly_convertible()`` -* Passing an argument that implements ``__float__`` to ``float`` or ``double``. -* Passing an argument that implements ``__int__`` to ``int``. -* Passing an argument that implements ``__complex__`` to ``std::complex``. +* 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 @@ -455,7 +455,7 @@ object, such as: 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()); -``supports_float`` will accept any argument that implements ``__float__``. +``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:: From f4ed7b7d5a119d12a2929f94b3fdb99733909186 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 24 Oct 2025 13:18:48 -0700 Subject: [PATCH 23/45] fix pypy __index__ problems --- include/pybind11/cast.h | 12 ++---------- include/pybind11/complex.h | 16 +++++++++++++++- include/pybind11/detail/common.h | 7 +++++++ tests/test_builtin_casters.py | 4 ++++ 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 9282eb068f..3639c4f840 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -244,26 +244,18 @@ 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()) || PYBIND11_LONG_CHECK(src.ptr())) { py_value = (py_type) PyFloat_AsDouble(src.ptr()); } else { return false; } - } else if (convert || PYBIND11_LONG_CHECK(src.ptr()) || index_check(src.ptr())) { + } else if (convert || PYBIND11_LONG_CHECK(src.ptr()) || PYBIND11_INDEX_CHECK(src.ptr())) { 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 (!PYBIND11_LONG_CHECK(src.ptr())) { // So: PYBIND11_INDEX_CHECK(src.ptr()) index = reinterpret_steal(PyNumber_Index(src.ptr())); if (!index) { PyErr_Clear(); diff --git a/include/pybind11/complex.h b/include/pybind11/complex.h index 9efad5edce..03febde701 100644 --- a/include/pybind11/complex.h +++ b/include/pybind11/complex.h @@ -56,7 +56,21 @@ class type_caster> { || PYBIND11_LONG_CHECK(src.ptr()))) { return false; } - Py_complex result = PyComplex_AsCComplex(src.ptr()); + handle src_or_index = src; +#if defined(PYPY_VERSION) + object index; + if (PYBIND11_INDEX_CHECK(src.ptr())) { + index = reinterpret_steal(PyNumber_Index(src.ptr())); + if (!index) { + PyErr_Clear(); + if (!convert) + return false; + } else { + src_or_index = index; + } + } +#endif + Py_complex result = PyComplex_AsCComplex(src_or_index.ptr()); if (result.real == -1.0 && PyErr_Occurred()) { PyErr_Clear(); return false; diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 16952c5829..792e515e6a 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -322,6 +322,13 @@ #define PYBIND11_BYTES_AS_STRING PyBytes_AsString #define PYBIND11_BYTES_SIZE PyBytes_Size #define PYBIND11_LONG_CHECK(o) PyLong_Check(o) +// In PyPy 7.3.3, `PyIndex_Check` is implemented by calling `__index__`, +// while CPython only considers the existence of `nb_index`/`__index__`. +#if !defined(PYPY_VERSION) +# define PYBIND11_INDEX_CHECK(o) PyIndex_Check(o) +#else +# define PYBIND11_INDEX_CHECK(o) hasattr(o, "__index__") +#endif #define PYBIND11_LONG_AS_LONGLONG(o) PyLong_AsLongLong(o) #define PYBIND11_LONG_FROM_SIGNED(o) PyLong_FromSsize_t((ssize_t) (o)) #define PYBIND11_LONG_FROM_UNSIGNED(o) PyLong_FromSize_t((size_t) (o)) diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index e078c73259..44a8214246 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -316,6 +316,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 @@ -355,6 +356,7 @@ def cant_convert(v): requires_conversion(Index()) assert pytest.approx(convert(Float())) == 41.45 assert pytest.approx(convert(Index())) == -7.0 + assert isinstance(convert(Float()), float) assert pytest.approx(convert(3)) == 3.0 assert pytest.approx(noconvert(3)) == 3.0 cant_convert(Int()) @@ -529,8 +531,10 @@ def cant_convert(v): assert convert(1 + 5j) == 1.0 + 5.0j assert convert(Complex()) == 5.0 + 4j assert convert(Float()) == 5.0 + assert isinstance(convert(Float()), complex) cant_convert(Int()) assert convert(Index()) == 1 + assert isinstance(convert(Index()), complex) assert noconvert(1) == 1.0 assert noconvert(2.0) == 2.0 From fc815ec1080160da8ea07a47a3226beaf3ff051d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 20:19:25 +0000 Subject: [PATCH 24/45] style: pre-commit fixes --- include/pybind11/detail/common.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 792e515e6a..07c0943006 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -325,9 +325,9 @@ // In PyPy 7.3.3, `PyIndex_Check` is implemented by calling `__index__`, // while CPython only considers the existence of `nb_index`/`__index__`. #if !defined(PYPY_VERSION) -# define PYBIND11_INDEX_CHECK(o) PyIndex_Check(o) +# define PYBIND11_INDEX_CHECK(o) PyIndex_Check(o) #else -# define PYBIND11_INDEX_CHECK(o) hasattr(o, "__index__") +# define PYBIND11_INDEX_CHECK(o) hasattr(o, "__index__") #endif #define PYBIND11_LONG_AS_LONGLONG(o) PyLong_AsLongLong(o) #define PYBIND11_LONG_FROM_SIGNED(o) PyLong_FromSsize_t((ssize_t) (o)) From 388366f2d936158692ef151e50983ceeccbc8328 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 26 Oct 2025 13:52:18 -0700 Subject: [PATCH 25/45] extract out PyLong_AsLong __index__ deprecation Signed-off-by: Michael Carlstrom --- tests/conftest.py | 20 +++++++++++++++++++- tests/env.py | 18 ------------------ tests/test_builtin_casters.py | 33 ++++++++++----------------------- 3 files changed, 29 insertions(+), 42 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 39de4e1381..7e7f2912c4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,10 +17,12 @@ import textwrap import traceback import weakref -from typing import Callable +from typing import Callable, SupportsIndex, TypeVar import pytest +import env + # Early diagnostic for failed imports try: import pybind11_tests @@ -311,3 +313,19 @@ def backport(sanatized_string: SanitizedString) -> SanitizedString: return sanatized_string return backport + +_EXPECTED_T = TypeVar("_EXPECTED_T") + +# The implicit conversion from np.float32 is undesirable but currently accepted. +# TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) +# TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) +# https://github.com/pybind/pybind11/issues/3408 +@pytest.fixture +def avoid_PyLong_AsLong_deprecation() -> Callable[[Callable[[SupportsIndex], _EXPECTED_T], SupportsIndex, _EXPECTED_T], bool]: + def check(convert: Callable[[SupportsIndex], _EXPECTED_T], value: SupportsIndex, expected: _EXPECTED_T) -> bool: + if sys.version_info < (3, 10) and env.CPYTHON: + with pytest.deprecated_call(): + return convert(value) == expected + else: + return convert(value) == expected + return check \ No newline at end of file diff --git a/tests/env.py b/tests/env.py index 1773925918..ccb1fd30b6 100644 --- a/tests/env.py +++ b/tests/env.py @@ -4,8 +4,6 @@ import sys import sysconfig -import pytest - ANDROID = sys.platform.startswith("android") LINUX = sys.platform.startswith("linux") MACOS = sys.platform.startswith("darwin") @@ -29,19 +27,3 @@ or GRAALPY or (CPYTHON and PY_GIL_DISABLED and (3, 13) <= sys.version_info < (3, 14)) ) - - -def deprecated_call(): - """ - pytest.deprecated_call() seems broken in pytest<3.9.x; concretely, it - doesn't work on CPython 3.8.0 with pytest==3.3.2 on Ubuntu 18.04 (#2922). - - This is a narrowed reimplementation of the following PR :( - https://github.com/pytest-dev/pytest/pull/4104 - """ - # TODO: Remove this when testing requires pytest>=3.9. - pieces = pytest.__version__.split(".") - pytest_major_minor = (int(pieces[0]), int(pieces[1])) - if pytest_major_minor < (3, 9): - return pytest.warns((DeprecationWarning, PendingDeprecationWarning)) - return pytest.deprecated_call() diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 44a8214246..c24f5e328f 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -247,7 +247,7 @@ def test_integer_casting(): assert "incompatible function arguments" in str(excinfo.value) -def test_int_convert(doc): +def test_int_convert(doc, avoid_PyLong_AsLong_deprecation): class Int: def __int__(self): return 42 @@ -300,15 +300,9 @@ def cant_convert(v): assert convert(7) == 7 assert noconvert(7) == 7 - assert convert(3.14159) == 3 + assert avoid_PyLong_AsLong_deprecation(convert, 3.14159, 3) requires_conversion(3.14159) - # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) - # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) - if sys.version_info < (3, 10) and env.CPYTHON: - with env.deprecated_call(): - assert convert(Int()) == 42 - else: - assert convert(Int()) == 42 + assert avoid_PyLong_AsLong_deprecation(convert, Int(), 42) requires_conversion(Int()) cant_convert(NotInt()) cant_convert(Float()) @@ -326,7 +320,7 @@ def cant_convert(v): requires_conversion(RaisingValueErrorOnIndex()) -def test_float_convert(doc): +def test_float_convert(doc, avoid_PyLong_AsLong_deprecation): class Int: def __int__(self): return -5 @@ -357,12 +351,12 @@ def cant_convert(v): assert pytest.approx(convert(Float())) == 41.45 assert pytest.approx(convert(Index())) == -7.0 assert isinstance(convert(Float()), float) - assert pytest.approx(convert(3)) == 3.0 + assert avoid_PyLong_AsLong_deprecation(convert, 3, 3.0) assert pytest.approx(noconvert(3)) == 3.0 cant_convert(Int()) -def test_numpy_int_convert(): +def test_numpy_int_convert(avoid_PyLong_AsLong_deprecation): np = pytest.importorskip("numpy") convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert @@ -375,14 +369,7 @@ def require_implicit(v): assert noconvert(np.intc(42)) == 42 # The implicit conversion from np.float32 is undesirable but currently accepted. - # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) - # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) - # https://github.com/pybind/pybind11/issues/3408 - if (3, 8) <= sys.version_info < (3, 10) and env.CPYTHON: - with env.deprecated_call(): - assert convert(np.float32(3.14159)) == 3 - else: - assert convert(np.float32(3.14159)) == 3 + assert avoid_PyLong_AsLong_deprecation(convert, np.float32(3.14159), 3) require_implicit(np.float32(3.14159)) @@ -483,7 +470,7 @@ def test_reference_wrapper(): assert m.refwrap_call_iiw(IncType(10), m.refwrap_iiw) == [10, 10, 10, 10] -def test_complex_cast(doc): +def test_complex_cast(doc, avoid_PyLong_AsLong_deprecation): """std::complex casts""" class Complex: @@ -526,8 +513,8 @@ def cant_convert(v): ) assert doc(noconvert) == "complex_noconvert(arg0: complex) -> complex" - assert convert(1) == 1.0 - assert convert(2.0) == 2.0 + assert avoid_PyLong_AsLong_deprecation(convert, 1, 1.0) + assert avoid_PyLong_AsLong_deprecation(convert, 2.0, 2.0) assert convert(1 + 5j) == 1.0 + 5.0j assert convert(Complex()) == 5.0 + 4j assert convert(Float()) == 5.0 From a95605202060792dd30fca487a8071e8bf4ec581 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:52:57 +0000 Subject: [PATCH 26/45] style: pre-commit fixes --- tests/conftest.py | 15 ++++++++++++--- tests/test_builtin_casters.py | 3 --- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7e7f2912c4..1a245e7e1e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -314,18 +314,27 @@ def backport(sanatized_string: SanitizedString) -> SanitizedString: return backport + _EXPECTED_T = TypeVar("_EXPECTED_T") + # The implicit conversion from np.float32 is undesirable but currently accepted. # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) # https://github.com/pybind/pybind11/issues/3408 @pytest.fixture -def avoid_PyLong_AsLong_deprecation() -> Callable[[Callable[[SupportsIndex], _EXPECTED_T], SupportsIndex, _EXPECTED_T], bool]: - def check(convert: Callable[[SupportsIndex], _EXPECTED_T], value: SupportsIndex, expected: _EXPECTED_T) -> bool: +def avoid_PyLong_AsLong_deprecation() -> Callable[ + [Callable[[SupportsIndex], _EXPECTED_T], SupportsIndex, _EXPECTED_T], bool +]: + def check( + convert: Callable[[SupportsIndex], _EXPECTED_T], + value: SupportsIndex, + expected: _EXPECTED_T, + ) -> bool: if sys.version_info < (3, 10) and env.CPYTHON: with pytest.deprecated_call(): return convert(value) == expected else: return convert(value) == expected - return check \ No newline at end of file + + return check diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index c24f5e328f..568bd51715 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -1,10 +1,7 @@ from __future__ import annotations -import sys - import pytest -import env from pybind11_tests import IncType, UserType from pybind11_tests import builtin_casters as m From 962c9fad40d6426a1babd0be9ed28eb560da9a17 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 26 Oct 2025 14:28:41 -0700 Subject: [PATCH 27/45] Add back env.deprecated_call Signed-off-by: Michael Carlstrom --- tests/conftest.py | 2 +- tests/env.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1a245e7e1e..727a678b2a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -332,7 +332,7 @@ def check( expected: _EXPECTED_T, ) -> bool: if sys.version_info < (3, 10) and env.CPYTHON: - with pytest.deprecated_call(): + with env.deprecated_call(): return convert(value) == expected else: return convert(value) == expected diff --git a/tests/env.py b/tests/env.py index ccb1fd30b6..64cea7f73a 100644 --- a/tests/env.py +++ b/tests/env.py @@ -3,6 +3,9 @@ import platform import sys import sysconfig +from typing import Any + +import pytest ANDROID = sys.platform.startswith("android") LINUX = sys.platform.startswith("linux") @@ -27,3 +30,19 @@ or GRAALPY or (CPYTHON and PY_GIL_DISABLED and (3, 13) <= sys.version_info < (3, 14)) ) + + +def deprecated_call() -> Any: + """ + pytest.deprecated_call() seems broken in pytest<3.9.x; concretely, it + doesn't work on CPython 3.8.0 with pytest==3.3.2 on Ubuntu 18.04 (#2922). + + This is a narrowed reimplementation of the following PR :( + https://github.com/pytest-dev/pytest/pull/4104 + """ + # TODO: Remove this when testing requires pytest>=3.9. + pieces = pytest.__version__.split(".") + pytest_major_minor = (int(pieces[0]), int(pieces[1])) + if pytest_major_minor < (3, 9): + return pytest.warns((DeprecationWarning, PendingDeprecationWarning)) + return pytest.deprecated_call() From 7b1bc727a89af9bcc103c6f6e102fb18d23a55f2 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 26 Oct 2025 14:34:20 -0700 Subject: [PATCH 28/45] remove note Signed-off-by: Michael Carlstrom --- tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 727a678b2a..17cdbf295f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -318,7 +318,6 @@ def backport(sanatized_string: SanitizedString) -> SanitizedString: _EXPECTED_T = TypeVar("_EXPECTED_T") -# The implicit conversion from np.float32 is undesirable but currently accepted. # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) # https://github.com/pybind/pybind11/issues/3408 From edbcbf24bd6ce2290013df8215d7b430494d504c Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 26 Oct 2025 14:40:41 -0700 Subject: [PATCH 29/45] remove untrue comment Signed-off-by: Michael Carlstrom --- tests/test_builtin_casters.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 568bd51715..cc5151e63e 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -365,7 +365,6 @@ def require_implicit(v): assert convert(np.intc(42)) == 42 assert noconvert(np.intc(42)) == 42 - # The implicit conversion from np.float32 is undesirable but currently accepted. assert avoid_PyLong_AsLong_deprecation(convert, np.float32(3.14159), 3) require_implicit(np.float32(3.14159)) From a3a4a5e2b1923a318cd23c7a3366d6d1fa0dae36 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 26 Oct 2025 14:54:28 -0700 Subject: [PATCH 30/45] fix noconvert_args Signed-off-by: Michael Carlstrom --- tests/test_custom_type_casters.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_custom_type_casters.py b/tests/test_custom_type_casters.py index 76b3a8aa95..f25252058f 100644 --- a/tests/test_custom_type_casters.py +++ b/tests/test_custom_type_casters.py @@ -6,7 +6,7 @@ from pybind11_tests import custom_type_casters as m -def test_noconvert_args(msg): +def test_noconvert_args(msg, avoid_PyLong_AsLong_deprecation): a = m.ArgInspector() assert ( msg(a.f("hi")) @@ -59,6 +59,7 @@ def test_noconvert_args(msg): assert m.ints_preferred(4) == 2 assert m.ints_preferred(True) == 0 + assert avoid_PyLong_AsLong_deprecation(m.ints_preferred, 4.0, 2) assert m.ints_preferred(4.0) == 2 assert m.ints_only(4) == 2 From d5dab1486adf88f05a44d8127d975bd5efa5b03b Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 26 Oct 2025 15:03:49 -0700 Subject: [PATCH 31/45] resolve error Signed-off-by: Michael Carlstrom --- tests/test_builtin_casters.py | 10 +++++----- tests/test_custom_type_casters.py | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index cc5151e63e..7776705cf1 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -317,7 +317,7 @@ def cant_convert(v): requires_conversion(RaisingValueErrorOnIndex()) -def test_float_convert(doc, avoid_PyLong_AsLong_deprecation): +def test_float_convert(doc): class Int: def __int__(self): return -5 @@ -348,7 +348,7 @@ def cant_convert(v): assert pytest.approx(convert(Float())) == 41.45 assert pytest.approx(convert(Index())) == -7.0 assert isinstance(convert(Float()), float) - assert avoid_PyLong_AsLong_deprecation(convert, 3, 3.0) + assert pytest.approx(convert(3)) == 3.0 assert pytest.approx(noconvert(3)) == 3.0 cant_convert(Int()) @@ -466,7 +466,7 @@ def test_reference_wrapper(): assert m.refwrap_call_iiw(IncType(10), m.refwrap_iiw) == [10, 10, 10, 10] -def test_complex_cast(doc, avoid_PyLong_AsLong_deprecation): +def test_complex_cast(doc): """std::complex casts""" class Complex: @@ -509,8 +509,8 @@ def cant_convert(v): ) assert doc(noconvert) == "complex_noconvert(arg0: complex) -> complex" - assert avoid_PyLong_AsLong_deprecation(convert, 1, 1.0) - assert avoid_PyLong_AsLong_deprecation(convert, 2.0, 2.0) + assert convert(1) == 1.0 + assert convert(2.0) == 2.0 assert convert(1 + 5j) == 1.0 + 5.0j assert convert(Complex()) == 5.0 + 4j assert convert(Float()) == 5.0 diff --git a/tests/test_custom_type_casters.py b/tests/test_custom_type_casters.py index f25252058f..ad1dca20e6 100644 --- a/tests/test_custom_type_casters.py +++ b/tests/test_custom_type_casters.py @@ -60,7 +60,6 @@ def test_noconvert_args(msg, avoid_PyLong_AsLong_deprecation): assert m.ints_preferred(4) == 2 assert m.ints_preferred(True) == 0 assert avoid_PyLong_AsLong_deprecation(m.ints_preferred, 4.0, 2) - assert m.ints_preferred(4.0) == 2 assert m.ints_only(4) == 2 with pytest.raises(TypeError) as excinfo: From 83ade19887aa02c90564817362a473421bbe4652 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 28 Oct 2025 19:24:25 -0700 Subject: [PATCH 32/45] Add comment Signed-off-by: Michael Carlstrom --- include/pybind11/complex.h | 2 ++ tests/conftest.py | 2 +- tests/env.py | 19 ------------------- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/include/pybind11/complex.h b/include/pybind11/complex.h index 03febde701..8f285d778f 100644 --- a/include/pybind11/complex.h +++ b/include/pybind11/complex.h @@ -57,6 +57,8 @@ class type_caster> { return false; } handle src_or_index = src; + // PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls. + // The same logic is used in numeric_caster for ints and floats #if defined(PYPY_VERSION) object index; if (PYBIND11_INDEX_CHECK(src.ptr())) { diff --git a/tests/conftest.py b/tests/conftest.py index 17cdbf295f..b96055fc10 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -331,7 +331,7 @@ def check( expected: _EXPECTED_T, ) -> bool: if sys.version_info < (3, 10) and env.CPYTHON: - with env.deprecated_call(): + with pytest.deprecated_call(): return convert(value) == expected else: return convert(value) == expected diff --git a/tests/env.py b/tests/env.py index 64cea7f73a..ccb1fd30b6 100644 --- a/tests/env.py +++ b/tests/env.py @@ -3,9 +3,6 @@ import platform import sys import sysconfig -from typing import Any - -import pytest ANDROID = sys.platform.startswith("android") LINUX = sys.platform.startswith("linux") @@ -30,19 +27,3 @@ or GRAALPY or (CPYTHON and PY_GIL_DISABLED and (3, 13) <= sys.version_info < (3, 14)) ) - - -def deprecated_call() -> Any: - """ - pytest.deprecated_call() seems broken in pytest<3.9.x; concretely, it - doesn't work on CPython 3.8.0 with pytest==3.3.2 on Ubuntu 18.04 (#2922). - - This is a narrowed reimplementation of the following PR :( - https://github.com/pytest-dev/pytest/pull/4104 - """ - # TODO: Remove this when testing requires pytest>=3.9. - pieces = pytest.__version__.split(".") - pytest_major_minor = (int(pieces[0]), int(pieces[1])) - if pytest_major_minor < (3, 9): - return pytest.warns((DeprecationWarning, PendingDeprecationWarning)) - return pytest.deprecated_call() From a2df4eb3174953461cd80d7de1c1f70e0403a96a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 8 Nov 2025 11:18:17 -0800 Subject: [PATCH 33/45] [skip ci] tests: Add overload resolution test for float/int breaking change Add test_overload_resolution_float_int() to explicitly test the breaking change where int arguments now match float overloads when registered first. The existing tests verify conversion behavior (int -> float, int/float -> complex) but do not test overload resolution when both float and int overloads exist. This test fills that gap by: - Testing that float overload registered before int overload matches int(42) - Testing strict mode (noconvert) overload resolution breaking change - Testing complex overload resolution with int/float/complex overloads - Documenting the breaking change explicitly This complements existing tests which verify 'can it convert?' by testing 'which overload wins when multiple can convert?' --- tests/test_builtin_casters.cpp | 20 +++++++++++ tests/test_builtin_casters.py | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index 7e6b97d14d..92afecd980 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -371,6 +371,26 @@ TEST_SUBMODULE(builtin_casters, m) { 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 (int) 42; }); m.def("long_cast", []() { return (long) 42; }); diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 7776705cf1..afb8100333 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -527,6 +527,68 @@ def cant_convert(v): requires_conversion(Index()) +def test_overload_resolution_float_int(): + """ + Test overload resolution behavior when int can match float. + + This test documents the breaking change: 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 this PR: int(42) would match int overload (if both existed) + After this PR: 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 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: int(42) would NOT match float overload with noconvert, would match int overload + # After PR: 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 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 From 754e2d0315c71e5d46ced2da21f493bfff59eb5f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 8 Nov 2025 12:20:13 -0800 Subject: [PATCH 34/45] Add test to verify that custom __index__ objects (not PyLong) work correctly with complex conversion. These should be consistent across CPython, PyPy, and GraalPy. --- tests/test_builtin_casters.py | 51 +++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index afb8100333..f9b7abebb8 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -527,6 +527,57 @@ def cant_convert(v): requires_conversion(Index()) +def test_complex_index_handling(): + """ + Test __index__ handling in complex caster. + + 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. From 41ff8766311601d55adb4c5f8ee1dcb63c1a7532 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 14:18:14 -0800 Subject: [PATCH 35/45] Improve comment clarity for PyPy __index__ handling Replace cryptic 'So: PYBIND11_INDEX_CHECK(src.ptr())' comment with clearer explanation of the logic: - Explains that we need to call PyNumber_Index explicitly on PyPy for non-PyLong objects - Clarifies the relationship to the outer condition: when convert is false, we only reach this point if PYBIND11_INDEX_CHECK passed above This makes the code more maintainable and easier to understand during review. --- include/pybind11/cast.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 3639c4f840..566be8df68 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -255,7 +255,9 @@ struct type_caster::value && !is_std_char_t // 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: PYBIND11_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(); From 0a00147a13511647e3bfce70b81532ffaee527e3 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 14:34:43 -0800 Subject: [PATCH 36/45] Undo inconsequential change to regex in test_enum.py During merge, HEAD's regex pattern was kept, but master's version is preferred. The order of ` ` and `\|` in the character class is arbitrary. Keep master's order (already fixed in PR #5891; sorry I missed looking back here when working on 5891). --- tests/test_enum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_enum.py b/tests/test_enum.py index aede6524b4..f295b01457 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -328,7 +328,7 @@ def test_generated_dunder_methods_pos_only(): ) assert ( re.match( - r"^__setstate__\(self: [\w\.]+, state: [\w\.\| ]+, /\)", + r"^__setstate__\(self: [\w\.]+, state: [\w\. \|]+, /\)", enum_type.__setstate__.__doc__, ) is not None From c7e959e1b577ddf7c3bb6172141b62e4c3cb655b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 15:38:34 -0800 Subject: [PATCH 37/45] test_methods_and_attributes.py: Restore existing `m.overload_order(1.1)` call and clearly explain the behavior change. --- tests/test_methods_and_attributes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 647580de9b..aae4e577fb 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -544,6 +544,11 @@ def test_overload_ordering(): in m.overload_order.__doc__ ) + # PR 5879 enabled conversions from Python `float` to C++ `int` in convert mode + # (i.e. if `arg(...).noconvert()` is NOT specified). This matches Python's + # behavior where `int(1.1)` succeeds, making pybind11 more consistent with + # Python's type system. + assert m.overload_order(1.1) == 4 with pytest.raises(TypeError) as err: m.overload_order([]) From e4023cd39fff42d43fd87184a5c6697bef5000be Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 16:04:23 -0800 Subject: [PATCH 38/45] =?UTF-8?q?Reject=20float=20=E2=86=92=20int=20conver?= =?UTF-8?q?sion=20even=20in=20convert=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enabling implicit float → int conversion in convert mode causes silent truncation (e.g., 1.9 → 1). This is dangerous because: 1. It's implicit - users don't expect truncation when calling functions 2. It's silent - no warning or error 3. It can hide bugs - precision loss is hard to detect This change restores the explicit rejection of PyFloat_Check for integer casters, even in convert mode. This is more in line with Python's behavior where int(1.9) must be explicit. Note that the int → float conversion in noconvert mode is preserved, as that's a safe widening conversion. --- include/pybind11/cast.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 566be8df68..a2753bda06 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -250,6 +250,11 @@ struct type_caster::value && !is_std_char_t } else { return false; } + } else if (PyFloat_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). + return false; } else if (convert || PYBIND11_LONG_CHECK(src.ptr()) || PYBIND11_INDEX_CHECK(src.ptr())) { handle src_or_index = src; // PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls. From cc981a887b0d795b7a26a3258a07d4a5663427d8 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 16:24:23 -0800 Subject: [PATCH 39/45] =?UTF-8?q?Revert=20test=20changes=20that=20sidestep?= =?UTF-8?q?ped=20implicit=20float=E2=86=92int=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts all test modifications that were made to accommodate implicit float→int conversion in convert mode. With the production code change that explicitly rejects float→int conversion even in convert mode, these test workarounds are no longer needed. Changes reverted: - test_builtin_casters.py: Restored cant_convert(3.14159) and np.float32 conversion with deprecated_call wrapper - test_custom_type_casters.py: Restored TypeError expectation for m.ints_preferred(4.0) - test_methods_and_attributes.py: Restored TypeError expectation for m.overload_order(1.1) - test_stl.py: Restored float literals (2.0) that were replaced with strings to avoid conversion - test_factory_constructors.py: Restored original constructor calls that were modified to avoid float→int conversion Also removes the unused avoid_PyLong_AsLong_deprecation fixture and related TypeVar imports, as all uses were removed. --- tests/conftest.py | 28 +--------------------------- tests/test_builtin_casters.py | 28 ++++++++++++++++++++++------ tests/test_custom_type_casters.py | 14 ++++++++++++-- tests/test_factory_constructors.py | 8 ++++---- tests/test_methods_and_attributes.py | 7 +------ tests/test_stl.py | 12 ++++++------ 6 files changed, 46 insertions(+), 51 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b96055fc10..39de4e1381 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,12 +17,10 @@ import textwrap import traceback import weakref -from typing import Callable, SupportsIndex, TypeVar +from typing import Callable import pytest -import env - # Early diagnostic for failed imports try: import pybind11_tests @@ -313,27 +311,3 @@ def backport(sanatized_string: SanitizedString) -> SanitizedString: return sanatized_string return backport - - -_EXPECTED_T = TypeVar("_EXPECTED_T") - - -# TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) -# TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) -# https://github.com/pybind/pybind11/issues/3408 -@pytest.fixture -def avoid_PyLong_AsLong_deprecation() -> Callable[ - [Callable[[SupportsIndex], _EXPECTED_T], SupportsIndex, _EXPECTED_T], bool -]: - def check( - convert: Callable[[SupportsIndex], _EXPECTED_T], - value: SupportsIndex, - expected: _EXPECTED_T, - ) -> bool: - if sys.version_info < (3, 10) and env.CPYTHON: - with pytest.deprecated_call(): - return convert(value) == expected - else: - return convert(value) == expected - - return check diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index f9b7abebb8..0168e73ac6 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -1,7 +1,10 @@ from __future__ import annotations +import sys + import pytest +import env from pybind11_tests import IncType, UserType from pybind11_tests import builtin_casters as m @@ -244,7 +247,7 @@ def test_integer_casting(): assert "incompatible function arguments" in str(excinfo.value) -def test_int_convert(doc, avoid_PyLong_AsLong_deprecation): +def test_int_convert(doc): class Int: def __int__(self): return 42 @@ -297,9 +300,14 @@ def cant_convert(v): assert convert(7) == 7 assert noconvert(7) == 7 - assert avoid_PyLong_AsLong_deprecation(convert, 3.14159, 3) - requires_conversion(3.14159) - assert avoid_PyLong_AsLong_deprecation(convert, Int(), 42) + cant_convert(3.14159) + # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) + # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) + if sys.version_info < (3, 10) and env.CPYTHON: + with env.deprecated_call(): + assert convert(Int()) == 42 + else: + assert convert(Int()) == 42 requires_conversion(Int()) cant_convert(NotInt()) cant_convert(Float()) @@ -353,7 +361,7 @@ def cant_convert(v): cant_convert(Int()) -def test_numpy_int_convert(avoid_PyLong_AsLong_deprecation): +def test_numpy_int_convert(): np = pytest.importorskip("numpy") convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert @@ -365,7 +373,15 @@ def require_implicit(v): assert convert(np.intc(42)) == 42 assert noconvert(np.intc(42)) == 42 - assert avoid_PyLong_AsLong_deprecation(convert, np.float32(3.14159), 3) + # The implicit conversion from np.float32 is undesirable but currently accepted. + # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) + # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) + # https://github.com/pybind/pybind11/issues/3408 + if (3, 8) <= sys.version_info < (3, 10) and env.CPYTHON: + with env.deprecated_call(): + assert convert(np.float32(3.14159)) == 3 + else: + assert convert(np.float32(3.14159)) == 3 require_implicit(np.float32(3.14159)) diff --git a/tests/test_custom_type_casters.py b/tests/test_custom_type_casters.py index ad1dca20e6..0680e50504 100644 --- a/tests/test_custom_type_casters.py +++ b/tests/test_custom_type_casters.py @@ -6,7 +6,7 @@ from pybind11_tests import custom_type_casters as m -def test_noconvert_args(msg, avoid_PyLong_AsLong_deprecation): +def test_noconvert_args(msg): a = m.ArgInspector() assert ( msg(a.f("hi")) @@ -59,7 +59,17 @@ def test_noconvert_args(msg, avoid_PyLong_AsLong_deprecation): assert m.ints_preferred(4) == 2 assert m.ints_preferred(True) == 0 - assert avoid_PyLong_AsLong_deprecation(m.ints_preferred, 4.0, 2) + with pytest.raises(TypeError) as excinfo: + m.ints_preferred(4.0) + assert ( + msg(excinfo.value) + == """ + ints_preferred(): incompatible function arguments. The following argument types are supported: + 1. (i: typing.SupportsInt | typing.SupportsIndex) -> int + + Invoked with: 4.0 + """ + ) assert m.ints_only(4) == 2 with pytest.raises(TypeError) as excinfo: diff --git a/tests/test_factory_constructors.py b/tests/test_factory_constructors.py index 635d762bb3..c6ae98c7fb 100644 --- a/tests/test_factory_constructors.py +++ b/tests/test_factory_constructors.py @@ -430,12 +430,12 @@ def test_reallocation_d(capture, msg): @pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") def test_reallocation_e(capture, msg): with capture: - create_and_destroy(4, 4.5) + 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(int 4) # construction + NoisyAlloc(double 3.5) # construction --- ~NoisyAlloc() # Destructor noisy delete # operator delete @@ -446,13 +446,13 @@ def test_reallocation_e(capture, msg): @pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") def test_reallocation_f(capture, msg): with capture: - create_and_destroy(3.5, 0.5) + create_and_destroy(4, 0.5) 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(double 3.5) # factory pointer construction + NoisyAlloc(int 4) # factory pointer construction --- ~NoisyAlloc() # Destructor noisy delete # operator delete diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index aae4e577fb..553d5bfc1b 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -544,13 +544,8 @@ def test_overload_ordering(): in m.overload_order.__doc__ ) - # PR 5879 enabled conversions from Python `float` to C++ `int` in convert mode - # (i.e. if `arg(...).noconvert()` is NOT specified). This matches Python's - # behavior where `int(1.1)` succeeds, making pybind11 more consistent with - # Python's type system. - assert m.overload_order(1.1) == 4 with pytest.raises(TypeError) as err: - m.overload_order([]) + m.overload_order(1.1) assert "1. (arg0: typing.SupportsInt | typing.SupportsIndex) -> int" in str( err.value diff --git a/tests/test_stl.py b/tests/test_stl.py index fb42636771..b04f55c9f8 100644 --- a/tests/test_stl.py +++ b/tests/test_stl.py @@ -422,7 +422,7 @@ def test_missing_header_message(): ) with pytest.raises(TypeError) as excinfo: - cm.missing_header_arg([1.0, "bar", 3.0]) + cm.missing_header_arg([1.0, 2.0, 3.0]) assert expected_message in str(excinfo.value) with pytest.raises(TypeError) as excinfo: @@ -491,7 +491,7 @@ def test_pass_std_vector_pair_int(): def test_list_caster_fully_consumes_generator_object(): def gen_invalid(): - yield from [1, "bar", 3] + yield from [1, 2.0, 3] gen_obj = gen_invalid() with pytest.raises(TypeError): @@ -514,15 +514,15 @@ def test_pass_std_set_int(): def test_set_caster_dict_keys_failure(): - dict_keys = {1: None, "bar": None, 3: None}.keys() + dict_keys = {1: None, 2.0: None, 3: None}.keys() # The asserts does not really exercise anything in pybind11, but if one of # them fails in some future version of Python, the set_caster load # implementation may need to be revisited. - assert tuple(dict_keys) == (1, "bar", 3) - assert tuple(dict_keys) == (1, "bar", 3) + assert tuple(dict_keys) == (1, 2.0, 3) + assert tuple(dict_keys) == (1, 2.0, 3) with pytest.raises(TypeError): m.pass_std_set_int(dict_keys) - assert tuple(dict_keys) == (1, "bar", 3) + assert tuple(dict_keys) == (1, 2.0, 3) class FakePyMappingMissingItems: From cc411c6e172d1cca63b7dd305a6b404a3b2a0d64 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 16:30:35 -0800 Subject: [PATCH 40/45] Replace env.deprecated_call() with pytest.deprecated_call() The env.deprecated_call() function was removed, but two test cases still reference it. Replace with pytest.deprecated_call(), which is the standard pytest context manager for handling deprecation warnings. Since we already require pytest>=6 (see tests/requirements.txt), the compatibility function is obsolete and pytest.deprecated_call() is available. --- tests/test_builtin_casters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 0168e73ac6..389c744c3d 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -304,7 +304,7 @@ def cant_convert(v): # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar) # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) if sys.version_info < (3, 10) and env.CPYTHON: - with env.deprecated_call(): + with pytest.deprecated_call(): assert convert(Int()) == 42 else: assert convert(Int()) == 42 @@ -378,7 +378,7 @@ def require_implicit(v): # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7) # https://github.com/pybind/pybind11/issues/3408 if (3, 8) <= sys.version_info < (3, 10) and env.CPYTHON: - with env.deprecated_call(): + with pytest.deprecated_call(): assert convert(np.float32(3.14159)) == 3 else: assert convert(np.float32(3.14159)) == 3 From ac957e386dc2b02e7bf66e1423df5a7e6651b385 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 16:59:38 -0800 Subject: [PATCH 41/45] Update test expectations for swapped NoisyAlloc overloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR 5879 swapped the order of NoisyAlloc constructor overloads: - (int i, double) is now placement new (comes first) - (double d, double) is now factory pointer (comes second) This swap is necessary because pybind11 tries overloads in order until one matches. With int → float conversion now allowed: - create_and_destroy(4, 0.5): Without the swap, (double d, double) would match first (since int → double conversion is allowed), bypassing the more specific (int i, double) overload. With the swap, (int i, double) matches first (exact match), which is correct. - create_and_destroy(3.5, 4.5): (int i, double) fails (float → int is rejected), then (double d, double) matches, which is correct. The swap ensures exact int matches are preferred over double matches when an int is provided, which is the expected overload resolution behavior. Update the test expectations to match the new overload resolution order. --- tests/test_factory_constructors.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 From 32fc2e897d1e724d02297dbc95be901b821c02f9 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 19:33:58 -0800 Subject: [PATCH 42/45] Resolve clang-tidy error: /__w/pybind11/pybind11/include/pybind11/cast.h:253:46: error: repeated branch body in conditional chain [bugprone-branch-clone,-warnings-as-errors] 253 | } else if (PyFloat_Check(src.ptr())) { | ^ /__w/pybind11/pybind11/include/pybind11/cast.h:258:10: note: end of the original 258 | } else if (convert || PYBIND11_LONG_CHECK(src.ptr()) || PYBIND11_INDEX_CHECK(src.ptr())) { | ^ /__w/pybind11/pybind11/include/pybind11/cast.h:283:16: note: clone 1 starts here 283 | } else { | ^ --- include/pybind11/cast.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index a2753bda06..48508b81dc 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -250,12 +250,15 @@ struct type_caster::value && !is_std_char_t } else { return false; } - } else if (PyFloat_Check(src.ptr())) { + } else if (PyFloat_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 if (convert || PYBIND11_LONG_CHECK(src.ptr()) || PYBIND11_INDEX_CHECK(src.ptr())) { + } else { handle src_or_index = src; // PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls. #if defined(PYPY_VERSION) @@ -280,8 +283,6 @@ struct type_caster::value && !is_std_char_t ? (py_type) PyLong_AsLong(src_or_index.ptr()) : (py_type) PYBIND11_LONG_AS_LONGLONG(src_or_index.ptr()); } - } else { - return false; } // Python API reported an error From f1d81588b9b3dfb78111a23cd1853841394a29f2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 12 Nov 2025 00:47:20 -0800 Subject: [PATCH 43/45] Add test coverage for __index__ and __int__ edge cases: incorrectly returning float These tests ensure that: - Invalid return types (floats) are properly rejected - The fallback from __index__ to __int__ works correctly in convert mode - noconvert mode correctly prevents fallback when __index__ fails --- tests/test_builtin_casters.py | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 389c744c3d..4d1e59e934 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -324,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: From 0f1f8ea9a1b1e3f229d5113062493856973fa623 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 12 Nov 2025 08:32:40 -0800 Subject: [PATCH 44/45] Minor comment-only changes: add PR number, for easy future reference --- tests/test_builtin_casters.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 4d1e59e934..b232c087e0 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -589,7 +589,7 @@ def cant_convert(v): def test_complex_index_handling(): """ - Test __index__ handling in complex caster. + 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, @@ -640,21 +640,21 @@ def requires_conversion(v): def test_overload_resolution_float_int(): """ - Test overload resolution behavior when int can match float. + Test overload resolution behavior when int can match float (added with PR #5879). - This test documents the breaking change: 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). + 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 this PR: int(42) would match int overload (if both existed) - After this PR: int(42) matches float overload (if registered first) + 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 changes) + # 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", ( @@ -664,8 +664,8 @@ def test_overload_resolution_float_int(): assert m.overload_resolution_test(42.0) == "float: 42.000000" # Test 2: With noconvert (strict mode) - this is the KEY breaking change - # Before PR: int(42) would NOT match float overload with noconvert, would match int overload - # After PR: int(42) DOES match float overload with noconvert (because int->float is now allowed) + # 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}. " @@ -675,7 +675,7 @@ def test_overload_resolution_float_int(): # 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 changes) + # 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)" From 816298a5e8c65551033d073ef237e93bb86ddd11 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 12 Nov 2025 08:40:24 -0800 Subject: [PATCH 45/45] Ensure we are not leaking a Python error is something is wrong elsewhere (e.g. UB, or bug in Python beta testing). See also: https://github.com/pybind/pybind11/pull/5879#issuecomment-3521099331 --- include/pybind11/cast.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 48508b81dc..d9478806c0 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -285,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