Skip to content
12 changes: 5 additions & 7 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -259,22 +259,20 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
return false;
} else {
handle src_or_index = src;
// PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls.
#if defined(PYPY_VERSION)
object index;
// If 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())) {
// PyLong_AsUnsignedLong[Long] never calls __index__, so call PyNumber_Index here.
if (std::is_unsigned<py_type>::value && !PYBIND11_LONG_CHECK(src.ptr())
&& PYBIND11_INDEX_CHECK(src.ptr())) {
index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
if (!index) {
PyErr_Clear();
if (!convert)
if (!convert) {
return false;
}
} else {
src_or_index = index;
}
}
#endif
if (std::is_unsigned<py_type>::value) {
py_value = as_unsigned<py_type>(src_or_index.ptr());
} else { // signed integer:
Expand Down
9 changes: 5 additions & 4 deletions include/pybind11/complex.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ class type_caster<std::complex<T>> {
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)
// Older PyPy versions need explicit __index__ conversion before PyComplex_AsCComplex().
// Preserve complex values and __complex__ precedence.
#if defined(PYPY_VERSION_NUM) && PYPY_VERSION_NUM < 0x08000000
object index;
if (PYBIND11_INDEX_CHECK(src.ptr())) {
if (!PyComplex_Check(src.ptr()) && !hasattr(type::handle_of(src), "__complex__")
&& PYBIND11_INDEX_CHECK(src.ptr())) {
index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
if (!index) {
PyErr_Clear();
Expand Down
2 changes: 2 additions & 0 deletions tests/test_builtin_casters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ TEST_SUBMODULE(builtin_casters, m) {
// test_int_convert
m.def("int_passthrough", [](int arg) { return arg; });
m.def("int_passthrough_noconvert", [](int arg) { return arg; }, py::arg{}.noconvert());
m.def("uint_passthrough", [](unsigned arg) { return arg; });
m.def("uint_passthrough_noconvert", [](unsigned arg) { return arg; }, py::arg{}.noconvert());

// test_float_convert
m.def("float_passthrough", [](float arg) { return arg; });
Expand Down
59 changes: 51 additions & 8 deletions tests/test_builtin_casters.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ def test_integer_casting():
assert "incompatible function arguments" in str(excinfo.value)


def test_int_convert(doc):
@pytest.mark.parametrize("prefix", ["int", "uint"])
def test_int_convert(doc, prefix):
class Int:
def __int__(self):
return 42
Expand Down Expand Up @@ -284,13 +285,14 @@ def __index__(self):
def __int__(self):
return 42

convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert
convert = getattr(m, f"{prefix}_passthrough")
noconvert = getattr(m, f"{prefix}_passthrough_noconvert")

assert (
doc(convert)
== "int_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int"
== f"{prefix}_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int"
)
assert doc(noconvert) == "int_passthrough_noconvert(arg0: int) -> int"
assert doc(noconvert) == f"{prefix}_passthrough_noconvert(arg0: int) -> int"

def requires_conversion(v):
pytest.raises(TypeError, noconvert, v)
Expand All @@ -302,7 +304,7 @@ def cant_convert(v):
assert noconvert(7) == 7
cant_convert(3.14159)
# TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar)
if sys.version_info < (3, 10) and env.CPYTHON:
if sys.version_info < (3, 10) and env.CPYTHON and prefix == "int":
with pytest.deprecated_call():
assert convert(Int()) == 42
else:
Expand Down Expand Up @@ -402,10 +404,12 @@ def cant_convert(v):
cant_convert(Int())


def test_numpy_int_convert():
@pytest.mark.parametrize("prefix", ["int", "uint"])
def test_numpy_int_convert(prefix):
np = pytest.importorskip("numpy")

convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert
convert = getattr(m, f"{prefix}_passthrough")
noconvert = getattr(m, f"{prefix}_passthrough_noconvert")

def require_implicit(v):
pytest.raises(TypeError, noconvert, v)
Expand All @@ -417,7 +421,7 @@ def require_implicit(v):
# The implicit conversion from np.float32 is undesirable but currently accepted.
# TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar)
# https://github.com/pybind/pybind11/issues/3408
if sys.version_info < (3, 10) and env.CPYTHON:
if sys.version_info < (3, 10) and env.CPYTHON and prefix == "int":
with pytest.deprecated_call():
assert convert(np.float32(3.14159)) == 3
else:
Expand Down Expand Up @@ -634,6 +638,45 @@ def requires_conversion(v):
assert result.imag == 0.0


@pytest.mark.parametrize("base", [object, int, float])
def test_complex_method_precedence(base):
class ComplexAndIndex(base):
def __complex__(self):
return 3.0 + 4.0j

def __index__(self):
return 42

value = ComplexAndIndex()
assert m.complex_convert(value) == 3.0 + 4.0j
if base is object:
with pytest.raises(TypeError):
m.complex_noconvert(value)
else:
assert m.complex_noconvert(value) == 3.0 + 4.0j


@pytest.mark.parametrize("function", [m.complex_convert, m.complex_noconvert])
def test_complex_subclass_with_index(function):
class ComplexWithIndex(complex):
def __index__(self):
return 42

assert function(ComplexWithIndex(3.0, 4.0)) == 3.0 + 4.0j


def test_complex_method_error_with_index():
class ComplexAndIndex:
def __complex__(self):
raise ValueError("complex conversion failed")

def __index__(self):
return 42

with pytest.raises(TypeError):
m.complex_convert(ComplexAndIndex())


def test_overload_resolution_float_int():
"""
Test overload resolution behavior when int can match float (added with PR #5879).
Expand Down
6 changes: 6 additions & 0 deletions tests/test_enum.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ TEST_SUBMODULE(enums, m) {
.value("ESecondMode", ClassWithUnscopedEnum::ESecondMode)
.export_values();

// test_scalar_comparison_with_index_object
enum UnscopedInt32Enum : std::int32_t { I32Three = 3 };
py::enum_<UnscopedInt32Enum>(m, "UnscopedInt32Enum").value("Three", I32Three);
enum UnscopedUInt32Enum : std::uint32_t { U32Three = 3 };
py::enum_<UnscopedUInt32Enum>(m, "UnscopedUInt32Enum").value("Three", U32Three);

// test_enum_to_int
m.def("test_enum_to_int", [](int) {});
m.def("test_enum_to_uint", [](uint32_t) {});
Expand Down
20 changes: 20 additions & 0 deletions tests/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,26 @@ def test_enum_to_int():
m.test_enum_to_long_long(m.ScopedBoolEnum.TRUE)


@pytest.mark.parametrize("enum_name", ["UnscopedInt32Enum", "UnscopedUInt32Enum"])
def test_scalar_comparison_with_index_object(enum_name):
# An unsigned underlying type must compare like a signed one against objects
# that only implement __index__ (e.g. numpy integers).
class Index:
def __index__(self):
return 3

three = getattr(m, enum_name).Three
assert three == 3
assert three == Index()
assert not three != Index()
assert three != 4
np = pytest.importorskip("numpy")
assert three == np.int32(3)
assert three == np.uint64(3)
assert not three != np.int64(3)
assert three != np.int32(4)


def test_duplicate_enum_name():
with pytest.raises(ValueError) as excinfo:
m.register_bad_enum()
Expand Down
Loading