diff options
author | Yannick Jadoul <yannick.jadoul@belgacom.net> | 2021-01-17 02:52:14 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-16 20:52:14 -0500 |
commit | 8449a8089c312e7138865e0af93b8d6ea971f83e (patch) | |
tree | 32842b57595cd0fadfbf42df019effdaea67c938 | |
parent | 0df11d857ce21a1f85aad1d18116cd4959f13f1d (diff) | |
download | pybind11-8449a8089c312e7138865e0af93b8d6ea971f83e.tar.gz |
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()
-rw-r--r-- | include/pybind11/cast.h | 10 | ||||
-rw-r--r-- | tests/test_builtin_casters.cpp | 4 | ||||
-rw-r--r-- | tests/test_builtin_casters.py | 62 |
3 files changed, 76 insertions, 0 deletions
diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index d2869264..ce85d997 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -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: diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index 79144594..16a78b1a 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -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); diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 39e8711d..0a469842 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) |