fix: only allow integer type_caster to call __int__ method when conversion is allowed; always call __index__ (#2698)

* Only allow integer type_caster to call __int__ or __index__ method when conversion is allowed

* Remove tests for __index__ as this seems to only be used to convert to int in 3.8+

* Take both `int` and `long` types into account for Python 2

* Add test_numpy_int_convert to assert tests currently fail, even though np.intc has an __index__ method

* Also consider __index__ as noconvert to a C++ integer

* New-style classes for Python 2.7; sigh

* Add some tests on types with custom __index__ method

* Ignore some tests in Python <3.8

* Update comment about conversion from np.float32 to C++ int

* Workaround difference between CPython and PyPy's different PyIndex_Check (unnoticed because we currently don't have PyPy >= 3.8)

* Avoid ICC segfault with py::arg()
This commit is contained in:
Yannick Jadoul 2021-01-17 02:52:14 +01:00 committed by GitHub
parent 0df11d857c
commit 8449a8089c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 0 deletions

View File

@ -1025,6 +1025,14 @@ public:
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<T>::value) {
if (convert || PyFloat_Check(src.ptr()))
py_value = (py_type) PyFloat_AsDouble(src.ptr());
@ -1032,6 +1040,8 @@ public:
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<py_type>::value) {
py_value = as_unsigned<py_type>(src.ptr());
} else { // signed integer:

View File

@ -141,6 +141,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<bool, std::string> input) {
return std::make_pair(input.second, input.first);

View File

@ -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)