Skip to content

Commit 8449a80

Browse files
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()
1 parent 0df11d8 commit 8449a80

File tree

3 files changed

+76
-0
lines changed

3 files changed

+76
-0
lines changed

include/pybind11/cast.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,13 +1025,23 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
10251025
if (!src)
10261026
return false;
10271027

1028+
#if !defined(PYPY_VERSION)
1029+
auto index_check = [](PyObject *o) { return PyIndex_Check(o); };
1030+
#else
1031+
// In PyPy 7.3.3, `PyIndex_Check` is implemented by calling `__index__`,
1032+
// while CPython only considers the existence of `nb_index`/`__index__`.
1033+
auto index_check = [](PyObject *o) { return hasattr(o, "__index__"); };
1034+
#endif
1035+
10281036
if (std::is_floating_point<T>::value) {
10291037
if (convert || PyFloat_Check(src.ptr()))
10301038
py_value = (py_type) PyFloat_AsDouble(src.ptr());
10311039
else
10321040
return false;
10331041
} else if (PyFloat_Check(src.ptr())) {
10341042
return false;
1043+
} else if (!convert && !index_check(src.ptr()) && !PYBIND11_LONG_CHECK(src.ptr())) {
1044+
return false;
10351045
} else if (std::is_unsigned<py_type>::value) {
10361046
py_value = as_unsigned<py_type>(src.ptr());
10371047
} else { // signed integer:

tests/test_builtin_casters.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ TEST_SUBMODULE(builtin_casters, m) {
141141
m.def("i64_str", [](std::int64_t v) { return std::to_string(v); });
142142
m.def("u64_str", [](std::uint64_t v) { return std::to_string(v); });
143143

144+
// test_int_convert
145+
m.def("int_passthrough", [](int arg) { return arg; });
146+
m.def("int_passthrough_noconvert", [](int arg) { return arg; }, py::arg{}.noconvert());
147+
144148
// test_tuple
145149
m.def("pair_passthrough", [](std::pair<bool, std::string> input) {
146150
return std::make_pair(input.second, input.first);

tests/test_builtin_casters.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,68 @@ def test_integer_casting():
251251
assert "incompatible function arguments" in str(excinfo.value)
252252

253253

254+
def test_int_convert():
255+
class DeepThought(object):
256+
def __int__(self):
257+
return 42
258+
259+
class ShallowThought(object):
260+
pass
261+
262+
class FuzzyThought(object):
263+
def __float__(self):
264+
return 41.99999
265+
266+
class IndexedThought(object):
267+
def __index__(self):
268+
return 42
269+
270+
class RaisingThought(object):
271+
def __index__(self):
272+
raise ValueError
273+
274+
def __int__(self):
275+
return 42
276+
277+
convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert
278+
279+
def require_implicit(v):
280+
pytest.raises(TypeError, noconvert, v)
281+
282+
def cant_convert(v):
283+
pytest.raises(TypeError, convert, v)
284+
285+
assert convert(7) == 7
286+
assert noconvert(7) == 7
287+
cant_convert(3.14159)
288+
assert convert(DeepThought()) == 42
289+
require_implicit(DeepThought())
290+
cant_convert(ShallowThought())
291+
cant_convert(FuzzyThought())
292+
if env.PY >= (3, 8):
293+
# Before Python 3.8, `int(obj)` does not pick up on `obj.__index__`
294+
assert convert(IndexedThought()) == 42
295+
assert noconvert(IndexedThought()) == 42
296+
cant_convert(RaisingThought()) # no fall-back to `__int__`if `__index__` raises
297+
298+
299+
def test_numpy_int_convert():
300+
np = pytest.importorskip("numpy")
301+
302+
convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert
303+
304+
def require_implicit(v):
305+
pytest.raises(TypeError, noconvert, v)
306+
307+
# `np.intc` is an alias that corresponds to a C++ `int`
308+
assert convert(np.intc(42)) == 42
309+
assert noconvert(np.intc(42)) == 42
310+
311+
# The implicit conversion from np.float32 is undesirable but currently accepted.
312+
assert convert(np.float32(3.14159)) == 3
313+
require_implicit(np.float32(3.14159))
314+
315+
254316
def test_tuple(doc):
255317
"""std::pair <-> tuple & std::tuple <-> tuple"""
256318
assert m.pair_passthrough((True, "test")) == ("test", True)

0 commit comments

Comments
 (0)