diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index c428e3f13a..f6448c9ce7 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1020,6 +1020,14 @@ struct type_caster::value && !is_std_char_t if (!src) 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())) py_value = (py_type) PyFloat_AsDouble(src.ptr()); @@ -1027,6 +1035,8 @@ struct type_caster::value && !is_std_char_t return false; } else if (PyFloat_Check(src.ptr())) { return false; + } else if (!convert && !index_check(src.ptr()) && !PYBIND11_LONG_CHECK(src.ptr())) { + return false; } else if (std::is_unsigned::value) { py_value = as_unsigned(src.ptr()); } else { // signed integer: diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index acc9f8fb36..84c06a889b 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -98,6 +98,10 @@ TEST_SUBMODULE(builtin_casters, m) { m.def("i64_str", [](std::int64_t v) { return std::to_string(v); }); m.def("u64_str", [](std::uint64_t v) { return std::to_string(v); }); + // test_int_convert + m.def("int_passthrough", [](int arg) { return arg; }); + m.def("int_passthrough_noconvert", [](int arg) { return arg; }, py::arg{}.noconvert()); + // test_tuple m.def("pair_passthrough", [](std::pair input) { return std::make_pair(input.second, input.first); diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index bd7996b620..1d48a2f596 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -251,6 +251,68 @@ def test_integer_casting(): assert "incompatible function arguments" in str(excinfo.value) +def test_int_convert(): + class DeepThought(object): + def __int__(self): + return 42 + + class ShallowThought(object): + pass + + class FuzzyThought(object): + def __float__(self): + return 41.99999 + + class IndexedThought(object): + def __index__(self): + return 42 + + class RaisingThought(object): + def __index__(self): + raise ValueError + + def __int__(self): + return 42 + + convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert + + def require_implicit(v): + pytest.raises(TypeError, noconvert, v) + + def cant_convert(v): + pytest.raises(TypeError, convert, v) + + assert convert(7) == 7 + assert noconvert(7) == 7 + cant_convert(3.14159) + assert convert(DeepThought()) == 42 + require_implicit(DeepThought()) + cant_convert(ShallowThought()) + cant_convert(FuzzyThought()) + if env.PY >= (3, 8): + # Before Python 3.8, `int(obj)` does not pick up on `obj.__index__` + assert convert(IndexedThought()) == 42 + assert noconvert(IndexedThought()) == 42 + cant_convert(RaisingThought()) # no fall-back to `__int__`if `__index__` raises + + +def test_numpy_int_convert(): + np = pytest.importorskip("numpy") + + convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert + + def require_implicit(v): + pytest.raises(TypeError, noconvert, v) + + # `np.intc` is an alias that corresponds to a C++ `int` + assert convert(np.intc(42)) == 42 + assert noconvert(np.intc(42)) == 42 + + # The implicit conversion from np.float32 is undesirable but currently accepted. + assert convert(np.float32(3.14159)) == 3 + require_implicit(np.float32(3.14159)) + + def test_tuple(doc): """std::pair <-> tuple & std::tuple <-> tuple""" assert m.pair_passthrough((True, "test")) == ("test", True)