diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 1d857a0ed5..f6622511ca 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -259,22 +259,20 @@ struct type_caster::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::value && !PYBIND11_LONG_CHECK(src.ptr()) + && PYBIND11_INDEX_CHECK(src.ptr())) { index = reinterpret_steal(PyNumber_Index(src.ptr())); if (!index) { PyErr_Clear(); - if (!convert) + if (!convert) { return false; + } } else { src_or_index = index; } } -#endif if (std::is_unsigned::value) { py_value = as_unsigned(src_or_index.ptr()); } else { // signed integer: diff --git a/include/pybind11/complex.h b/include/pybind11/complex.h index 9d0812f09f..ec02ba3ae4 100644 --- a/include/pybind11/complex.h +++ b/include/pybind11/complex.h @@ -58,7 +58,6 @@ class type_caster> { } 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/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index 40803fbf8a..b7599615e3 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -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; }); diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index a08ac410b6..29ab6070ee 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -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 @@ -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) @@ -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: @@ -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) @@ -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: diff --git a/tests/test_enum.cpp b/tests/test_enum.cpp index 2a7fa0fda0..186bc922cb 100644 --- a/tests/test_enum.cpp +++ b/tests/test_enum.cpp @@ -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_(m, "UnscopedInt32Enum").value("Three", I32Three); + enum UnscopedUInt32Enum : std::uint32_t { U32Three = 3 }; + py::enum_(m, "UnscopedUInt32Enum").value("Three", U32Three); + // test_enum_to_int m.def("test_enum_to_int", [](int) {}); m.def("test_enum_to_uint", [](uint32_t) {}); diff --git a/tests/test_enum.py b/tests/test_enum.py index 81170c91dd..451807365c 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -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()