Skip to content

Commit 0bb8ca2

Browse files
Always call PyNumber_Index when casting from Python to a C++ integral type, also pre-3.8 (#2801)
* Always call PyNumber_Index when casting from Python to a C++ integral type, also pre-3.8 * Fixed on PyPy * Simplify use of PyNumber_Index, following @rwgk's idea, and ignore warnings in >=3.8 * Reproduce mismatch between pre-3.8 and post-3.8 behavior on __index__ throwing TypeError * Fix tests on 3.6 <= Python < 3.8 * No, I don't have an uninitialized variable * Fix use of __index__ on Python 2 * Make types in test_int_convert more ~boring~ descriptive
1 parent 9ea39dc commit 0bb8ca2

File tree

2 files changed

+60
-30
lines changed

2 files changed

+60
-30
lines changed

include/pybind11/cast.h

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,14 +1040,31 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
10401040
return false;
10411041
} else if (PyFloat_Check(src.ptr())) {
10421042
return false;
1043-
} else if (!convert && !index_check(src.ptr()) && !PYBIND11_LONG_CHECK(src.ptr())) {
1043+
} else if (!convert && !PYBIND11_LONG_CHECK(src.ptr()) && !index_check(src.ptr())) {
10441044
return false;
1045-
} else if (std::is_unsigned<py_type>::value) {
1046-
py_value = as_unsigned<py_type>(src.ptr());
1047-
} else { // signed integer:
1048-
py_value = sizeof(T) <= sizeof(long)
1049-
? (py_type) PyLong_AsLong(src.ptr())
1050-
: (py_type) PYBIND11_LONG_AS_LONGLONG(src.ptr());
1045+
} else {
1046+
handle src_or_index = src;
1047+
#if PY_VERSION_HEX < 0x03080000
1048+
object index;
1049+
if (!PYBIND11_LONG_CHECK(src.ptr())) { // So: index_check(src.ptr())
1050+
index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
1051+
if (!index) {
1052+
PyErr_Clear();
1053+
if (!convert)
1054+
return false;
1055+
}
1056+
else {
1057+
src_or_index = index;
1058+
}
1059+
}
1060+
#endif
1061+
if (std::is_unsigned<py_type>::value) {
1062+
py_value = as_unsigned<py_type>(src_or_index.ptr());
1063+
} else { // signed integer:
1064+
py_value = sizeof(T) <= sizeof(long)
1065+
? (py_type) PyLong_AsLong(src_or_index.ptr())
1066+
: (py_type) PYBIND11_LONG_AS_LONGLONG(src_or_index.ptr());
1067+
}
10511068
}
10521069

10531070
// Python API reported an error
@@ -1056,15 +1073,8 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
10561073
// Check to see if the conversion is valid (integers should match exactly)
10571074
// Signed/unsigned checks happen elsewhere
10581075
if (py_err || (std::is_integral<T>::value && sizeof(py_type) != sizeof(T) && py_value != (py_type) (T) py_value)) {
1059-
bool type_error = py_err && PyErr_ExceptionMatches(
1060-
#if PY_VERSION_HEX < 0x03000000 && !defined(PYPY_VERSION)
1061-
PyExc_SystemError
1062-
#else
1063-
PyExc_TypeError
1064-
#endif
1065-
);
10661076
PyErr_Clear();
1067-
if (type_error && convert && PyNumber_Check(src.ptr())) {
1077+
if (py_err && convert && PyNumber_Check(src.ptr())) {
10681078
auto tmp = reinterpret_steal<object>(std::is_floating_point<T>::value
10691079
? PyNumber_Float(src.ptr())
10701080
: PyNumber_Long(src.ptr()));

tests/test_builtin_casters.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -252,22 +252,36 @@ def test_integer_casting():
252252

253253

254254
def test_int_convert():
255-
class DeepThought(object):
255+
class Int(object):
256256
def __int__(self):
257257
return 42
258258

259-
class ShallowThought(object):
259+
class NotInt(object):
260260
pass
261261

262-
class FuzzyThought(object):
262+
class Float(object):
263263
def __float__(self):
264264
return 41.99999
265265

266-
class IndexedThought(object):
266+
class Index(object):
267267
def __index__(self):
268268
return 42
269269

270-
class RaisingThought(object):
270+
class IntAndIndex(object):
271+
def __int__(self):
272+
return 42
273+
274+
def __index__(self):
275+
return 0
276+
277+
class RaisingTypeErrorOnIndex(object):
278+
def __index__(self):
279+
raise TypeError
280+
281+
def __int__(self):
282+
return 42
283+
284+
class RaisingValueErrorOnIndex(object):
271285
def __index__(self):
272286
raise ValueError
273287

@@ -276,7 +290,7 @@ def __int__(self):
276290

277291
convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert
278292

279-
def require_implicit(v):
293+
def requires_conversion(v):
280294
pytest.raises(TypeError, noconvert, v)
281295

282296
def cant_convert(v):
@@ -285,15 +299,21 @@ def cant_convert(v):
285299
assert convert(7) == 7
286300
assert noconvert(7) == 7
287301
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
302+
assert convert(Int()) == 42
303+
requires_conversion(Int())
304+
cant_convert(NotInt())
305+
cant_convert(Float())
306+
307+
# Before Python 3.8, `PyLong_AsLong` does not pick up on `obj.__index__`,
308+
# but pybind11 "backports" this behavior.
309+
assert convert(Index()) == 42
310+
assert noconvert(Index()) == 42
311+
assert convert(IntAndIndex()) == 0 # Fishy; `int(DoubleThought)` == 42
312+
assert noconvert(IntAndIndex()) == 0
313+
assert convert(RaisingTypeErrorOnIndex()) == 42
314+
requires_conversion(RaisingTypeErrorOnIndex())
315+
assert convert(RaisingValueErrorOnIndex()) == 42
316+
requires_conversion(RaisingValueErrorOnIndex())
297317

298318

299319
def test_numpy_int_convert():

0 commit comments

Comments
 (0)