Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
515a3ab
init
InvincibleRMC Oct 23, 2025
aa2693f
Add constexpr to is_floating_point check
gentlegiantJGC Oct 23, 2025
7948b27
Allow noconvert float to accept int
gentlegiantJGC Oct 23, 2025
47746af
Update noconvert documentation
gentlegiantJGC Oct 23, 2025
507d31c
Allow noconvert complex to accept int and float
gentlegiantJGC Oct 23, 2025
f4115fe
Merge remote-tracking branch 'collab/fix-pep-484' into expand-float-s…
InvincibleRMC Oct 23, 2025
4ea8bcb
Add complex strict test
InvincibleRMC Oct 23, 2025
81e49f6
style: pre-commit fixes
pre-commit-ci[bot] Oct 23, 2025
0b94f21
Update unit tests so int, becomes double.
InvincibleRMC Oct 23, 2025
e9bf4e5
style: pre-commit fixes
pre-commit-ci[bot] Oct 23, 2025
21f8447
remove if (constexpr)
InvincibleRMC Oct 23, 2025
5806318
fix spelling error
InvincibleRMC Oct 23, 2025
a0fb6dc
bump order in #else
InvincibleRMC Oct 23, 2025
2692820
Switch order in c++11 only section
InvincibleRMC Oct 24, 2025
b12f5a8
ci: trigger build
InvincibleRMC Oct 24, 2025
2187a65
ci: trigger build
InvincibleRMC Oct 24, 2025
d42c8e8
Allow casting from float to int
gentlegiantJGC Oct 24, 2025
16bdff3
tests for py::float into int
InvincibleRMC Oct 24, 2025
358266f
Update complex_cast tests
InvincibleRMC Oct 24, 2025
dadbf05
Add SupportsIndex to int and float
InvincibleRMC Oct 24, 2025
248b12e
style: pre-commit fixes
pre-commit-ci[bot] Oct 24, 2025
2163f50
fix assert
InvincibleRMC Oct 24, 2025
c13f21e
Update docs to mention other conversions
InvincibleRMC Oct 24, 2025
f4ed7b7
fix pypy __index__ problems
InvincibleRMC Oct 24, 2025
fc815ec
style: pre-commit fixes
pre-commit-ci[bot] Oct 24, 2025
388366f
extract out PyLong_AsLong __index__ deprecation
InvincibleRMC Oct 26, 2025
a956052
style: pre-commit fixes
pre-commit-ci[bot] Oct 26, 2025
962c9fa
Add back env.deprecated_call
InvincibleRMC Oct 26, 2025
7b1bc72
remove note
InvincibleRMC Oct 26, 2025
edbcbf2
remove untrue comment
InvincibleRMC Oct 26, 2025
a3a4a5e
fix noconvert_args
InvincibleRMC Oct 26, 2025
d5dab14
resolve error
InvincibleRMC Oct 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions docs/advanced/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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<A,B>()``
* Calling a method accepting a double with an integer argument
* Calling a ``std::complex<float>`` argument with a non-complex python type
(for example, with a float). (Requires the optional ``pybind11/complex.h``
header).
* Passing an argument that implements ``__float__`` or ``__index__`` to ``float`` or ``double``.
* Passing an argument that implements ``__int__`` or ``__index__`` to ``int``.
* Passing an argument that implements ``__complex__``, ``__float__``, or ``__index__`` to ``std::complex<float>``.
(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).
Expand All @@ -452,24 +452,37 @@ object, such as:

.. code-block:: cpp

m.def("floats_only", [](double f) { return 0.5 * f; }, py::arg("f").noconvert());
m.def("floats_preferred", [](double f) { return 0.5 * f; }, py::arg("f"));
m.def("supports_float", [](double f) { return 0.5 * f; }, py::arg("f"));
m.def("only_float", [](double f) { return 0.5 * f; }, py::arg("f").noconvert());

Attempting the call the second function (the one without ``.noconvert()``) with
an integer will succeed, but attempting to call the ``.noconvert()`` version
will fail with a ``TypeError``:
``supports_float`` will accept any argument that implements ``__float__`` or ``__index__``.
``only_float`` will only accept a float or int argument. Anything else will fail with a ``TypeError``:

.. note::

The noconvert behaviour of float, double and complex has changed to match PEP 484.
A float/double argument marked noconvert will accept float or int.
A std::complex<float> 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 "<stdin>", line 1, in <module>
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
Expand Down
28 changes: 11 additions & 17 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -244,29 +244,18 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::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<T>::value) {
if (convert || PyFloat_Check(src.ptr())) {
if (convert || PyFloat_Check(src.ptr()) || PYBIND11_LONG_CHECK(src.ptr())) {
py_value = (py_type) PyFloat_AsDouble(src.ptr());
} else {
return false;
}
} else if (PyFloat_Check(src.ptr())
|| (!convert && !PYBIND11_LONG_CHECK(src.ptr()) && !index_check(src.ptr()))) {
return false;
} else {
} 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<object>(PyNumber_Index(src.ptr()));
if (!index) {
PyErr_Clear();
Expand All @@ -284,6 +273,8 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::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
Expand Down Expand Up @@ -347,9 +338,12 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
return PyLong_FromUnsignedLongLong((unsigned long long) src);
}

PYBIND11_TYPE_CASTER(T,
io_name<std::is_integral<T>::value>(
"typing.SupportsInt", "int", "typing.SupportsFloat", "float"));
PYBIND11_TYPE_CASTER(
T,
io_name<std::is_integral<T>::value>("typing.SupportsInt | typing.SupportsIndex",
"int",
"typing.SupportsFloat | typing.SupportsIndex",
"float"));
};

template <typename T>
Expand Down
25 changes: 22 additions & 3 deletions include/pybind11/complex.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,26 @@ class type_caster<std::complex<T>> {
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());
handle src_or_index = src;
#if defined(PYPY_VERSION)
object index;
if (PYBIND11_INDEX_CHECK(src.ptr())) {
index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
if (!index) {
PyErr_Clear();
if (!convert)
return false;
} else {
src_or_index = index;
}
}
#endif
Comment on lines +60 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why you are doing this?
Doesn't PyComplex_AsCComplex handle that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this I was getting failures on old pypy versions see https://github.com/pybind/pybind11/actions/runs/18790097220/job/53618039827.

See this issue for more info pypy/pypy#3383

So I copied the logic from the numeric caster. Ref

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I think I understand. Can you add a comment in the code explaining it?

Py_complex result = PyComplex_AsCComplex(src_or_index.ptr());
if (result.real == -1.0 && PyErr_Occurred()) {
PyErr_Clear();
return false;
Expand All @@ -68,7 +84,10 @@ class type_caster<std::complex<T>> {
return PyComplex_FromDoubles((double) src.real(), (double) src.imag());
}

PYBIND11_TYPE_CASTER(std::complex<T>, const_name("complex"));
PYBIND11_TYPE_CASTER(
std::complex<T>,
io_name("typing.SupportsComplex | typing.SupportsFloat | typing.SupportsIndex",
"complex"));
};
PYBIND11_NAMESPACE_END(detail)
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
7 changes: 7 additions & 0 deletions include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
28 changes: 27 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -311,3 +313,27 @@ 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 env.deprecated_call():
return convert(value) == expected
else:
return convert(value) == expected

return check
3 changes: 2 additions & 1 deletion tests/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import platform
import sys
import sysconfig
from typing import Any

import pytest

Expand Down Expand Up @@ -31,7 +32,7 @@
)


def deprecated_call():
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).
Expand Down
7 changes: 7 additions & 0 deletions tests/test_builtin_casters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,13 @@ TEST_SUBMODULE(builtin_casters, m) {
m.def("complex_cast", [](float x) { return "{}"_s.format(x); });
m.def("complex_cast",
[](std::complex<float> x) { return "({}, {})"_s.format(x.real(), x.imag()); });
m.def(
"complex_cast_strict",
[](std::complex<float> x) { return "({}, {})"_s.format(x.real(), x.imag()); },
py::arg{}.noconvert());

m.def("complex_convert", [](std::complex<float> x) { return x; });
m.def("complex_noconvert", [](std::complex<float> x) { return x; }, py::arg{}.noconvert());

// test int vs. long (Python 2)
m.def("int_cast", []() { return (int) 42; });
Expand Down
Loading
Loading