Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,17 @@ jobs:
- runs-on: ubuntu-latest
python-version: '3.14t'
cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_TEST_SMART_HOLDER=ON
# Keep one PyPy 7 job while that runtime remains supported.
- runs-on: ubuntu-latest
python-version: 'pypy-3.11-v7.3.23'
cmake-args: -DCMAKE_CXX_STANDARD=17
- runs-on: ubuntu-latest
python-version: 'pypy-3.11-v8.0.1'
cmake-args: -DCMAKE_CXX_STANDARD=17
# Keep 8.0.0 to test the deallocation and memoryview workarounds (PR #6197).
- runs-on: ubuntu-latest
python-version: 'pypy-3.12-v8.0.0'
cmake-args: -DCMAKE_CXX_STANDARD=20
- runs-on: ubuntu-latest
python-version: 'graalpy-25.0'
cmake-args: -DCMAKE_CXX_STANDARD=20
Expand Down Expand Up @@ -138,7 +146,7 @@ jobs:
python-version: '3.15'
cmake-args: -DCMAKE_CXX_STANDARD=17
- runs-on: macos-latest
python-version: 'pypy-3.11-v7.3.23'
python-version: 'pypy-3.12-v8.0.1'
- runs-on: macos-latest
python-version: 'graalpy-25.0'
- runs-on: macos-latest
Expand Down Expand Up @@ -172,6 +180,9 @@ jobs:
- runs-on: windows-latest
python-version: 'pypy3.11'
cmake-args: -DCMAKE_CXX_STANDARD=20
- runs-on: windows-latest
python-version: 'pypy3.12'
cmake-args: -DCMAKE_CXX_STANDARD=17
# The setup-python action currently doesn't have graalpy for windows
# See https://github.com/actions/setup-python/pull/880

Expand Down
12 changes: 5 additions & 7 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -259,22 +259,20 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_t
return false;
} else {
handle src_or_index = src;
// PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls.
#if defined(PYPY_VERSION)
object index;
// If not a PyLong, we need to call PyNumber_Index explicitly on PyPy.
// When convert is false, we only reach here if PYBIND11_INDEX_CHECK passed above.
if (!PYBIND11_LONG_CHECK(src.ptr())) {
// PyLong_AsUnsignedLong[Long] never calls __index__, so call PyNumber_Index here.
if (std::is_unsigned<py_type>::value && !PYBIND11_LONG_CHECK(src.ptr())
&& PYBIND11_INDEX_CHECK(src.ptr())) {
index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
if (!index) {
PyErr_Clear();
if (!convert)
if (!convert) {
return false;
}
} else {
src_or_index = index;
}
}
#endif
if (std::is_unsigned<py_type>::value) {
py_value = as_unsigned<py_type>(src_or_index.ptr());
} else { // signed integer:
Expand Down
9 changes: 5 additions & 4 deletions include/pybind11/complex.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ class type_caster<std::complex<T>> {
return false;
}
handle src_or_index = src;
// PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls.
// The same logic is used in numeric_caster for ints and floats
#if defined(PYPY_VERSION)
// Older PyPy versions need explicit __index__ conversion before PyComplex_AsCComplex().
// Preserve complex values and __complex__ precedence.
#if defined(PYPY_VERSION_NUM) && PYPY_VERSION_NUM < 0x08000000
object index;
if (PYBIND11_INDEX_CHECK(src.ptr())) {
if (!PyComplex_Check(src.ptr()) && !hasattr(type::handle_of(src), "__complex__")
&& PYBIND11_INDEX_CHECK(src.ptr())) {
index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
if (!index) {
PyErr_Clear();
Expand Down
20 changes: 17 additions & 3 deletions include/pybind11/detail/class-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,20 @@ PYBIND11_INLINE void clear_patients(PyObject *self) {
}
}

PYBIND11_INLINE PyObject **instance_dict_ptr(PyObject *self) {
#if defined(PYPY_VERSION_NUM) && (PYPY_VERSION_NUM & 0xFFFF0000) == 0x08000000
// PyPy 8.0.0's _PyObject_GetDictPtr() can abort when called from tp_dealloc.
// The bug was fixed in 8.0.1, but all 8.0.x releases share an extension ABI,
// so an extension built against a fixed release can still run on 8.0.0.
// Keep this workaround for the entire 8.0.x series.
auto offset = Py_TYPE(self)->tp_dictoffset;
return offset > 0 ? reinterpret_cast<PyObject **>(reinterpret_cast<char *>(self) + offset)
: nullptr;
#else
return _PyObject_GetDictPtr(self);
#endif
}

PYBIND11_INLINE void clear_instance(PyObject *self) {
auto *instance = reinterpret_cast<detail::instance *>(self);

Expand Down Expand Up @@ -436,7 +450,7 @@ PYBIND11_INLINE void clear_instance(PyObject *self) {
PyObject_ClearWeakRefs(self);
}

PyObject **dict_ptr = _PyObject_GetDictPtr(self);
PyObject **dict_ptr = instance_dict_ptr(self);
if (dict_ptr) {
Py_CLEAR(*dict_ptr);
}
Expand Down Expand Up @@ -525,7 +539,7 @@ extern "C" PYBIND11_INLINE int pybind11_traverse(PyObject *self, visitproc visit
return ret;
}
#else
PyObject *&dict = *_PyObject_GetDictPtr(self);
PyObject *&dict = *instance_dict_ptr(self);
Py_VISIT(dict);
#endif
// https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse
Expand All @@ -537,7 +551,7 @@ extern "C" PYBIND11_INLINE int pybind11_clear(PyObject *self) {
#if PY_VERSION_HEX >= 0x030D0000
PyObject_ClearManagedDict(self);
#else
PyObject *&dict = *_PyObject_GetDictPtr(self);
PyObject *&dict = *instance_dict_ptr(self);
Py_CLEAR(dict);
#endif
return 0;
Expand Down
3 changes: 3 additions & 0 deletions include/pybind11/detail/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ void add_patient(PyObject *nurse, PyObject *patient);

void clear_patients(PyObject *self);

/// Returns the address of the instance `__dict__` slot, or nullptr if there is none.
PyObject **instance_dict_ptr(PyObject *self);

/// Clears all internal data from the instance and removes it from registered instances in
/// preparation for deallocation.
void clear_instance(PyObject *self);
Expand Down
13 changes: 13 additions & 0 deletions include/pybind11/pytypes-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,21 @@ PYBIND11_INLINE memoryview memoryview::from_buffer(void *ptr,
view.itemsize = itemsize;
view.format = const_cast<char *>(format);
view.ndim = static_cast<int>(ndim);
#if defined(PYPY_VERSION)
// PyPy (at least up to 8.0.0) only copies shape and strides that point to its inline arrays.
if (ndim > PyBUF_MAX_NDIM) {
pybind11_fail("memoryview: too many dimensions");
}
for (size_t i = 0; i < ndim; ++i) {
view._shape[i] = (*shape)[i];
view._strides[i] = (*strides)[i];
}
view.shape = view._shape;
view.strides = view._strides;
#else
view.shape = shape->data();
view.strides = strides->data();
#endif
view.suboffsets = nullptr;
view.internal = nullptr;
PyObject *obj = PyMemoryView_FromBuffer(&view);
Expand Down
2 changes: 2 additions & 0 deletions tests/test_builtin_casters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ TEST_SUBMODULE(builtin_casters, m) {
// test_int_convert
m.def("int_passthrough", [](int arg) { return arg; });
m.def("int_passthrough_noconvert", [](int arg) { return arg; }, py::arg{}.noconvert());
m.def("uint_passthrough", [](unsigned arg) { return arg; });
m.def("uint_passthrough_noconvert", [](unsigned arg) { return arg; }, py::arg{}.noconvert());

// test_float_convert
m.def("float_passthrough", [](float arg) { return arg; });
Expand Down
59 changes: 51 additions & 8 deletions tests/test_builtin_casters.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ def test_integer_casting():
assert "incompatible function arguments" in str(excinfo.value)


def test_int_convert(doc):
@pytest.mark.parametrize("prefix", ["int", "uint"])
def test_int_convert(doc, prefix):
class Int:
def __int__(self):
return 42
Expand Down Expand Up @@ -284,13 +285,14 @@ def __index__(self):
def __int__(self):
return 42

convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert
convert = getattr(m, f"{prefix}_passthrough")
noconvert = getattr(m, f"{prefix}_passthrough_noconvert")

assert (
doc(convert)
== "int_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int"
== f"{prefix}_passthrough(arg0: typing.SupportsInt | typing.SupportsIndex) -> int"
)
assert doc(noconvert) == "int_passthrough_noconvert(arg0: int) -> int"
assert doc(noconvert) == f"{prefix}_passthrough_noconvert(arg0: int) -> int"

def requires_conversion(v):
pytest.raises(TypeError, noconvert, v)
Expand All @@ -302,7 +304,7 @@ def cant_convert(v):
assert noconvert(7) == 7
cant_convert(3.14159)
# TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar)
if sys.version_info < (3, 10) and env.CPYTHON:
if sys.version_info < (3, 10) and env.CPYTHON and prefix == "int":
with pytest.deprecated_call():
assert convert(Int()) == 42
else:
Expand Down Expand Up @@ -402,10 +404,12 @@ def cant_convert(v):
cant_convert(Int())


def test_numpy_int_convert():
@pytest.mark.parametrize("prefix", ["int", "uint"])
def test_numpy_int_convert(prefix):
np = pytest.importorskip("numpy")

convert, noconvert = m.int_passthrough, m.int_passthrough_noconvert
convert = getattr(m, f"{prefix}_passthrough")
noconvert = getattr(m, f"{prefix}_passthrough_noconvert")

def require_implicit(v):
pytest.raises(TypeError, noconvert, v)
Expand All @@ -417,7 +421,7 @@ def require_implicit(v):
# The implicit conversion from np.float32 is undesirable but currently accepted.
# TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar)
# https://github.com/pybind/pybind11/issues/3408
if sys.version_info < (3, 10) and env.CPYTHON:
if sys.version_info < (3, 10) and env.CPYTHON and prefix == "int":
with pytest.deprecated_call():
assert convert(np.float32(3.14159)) == 3
else:
Expand Down Expand Up @@ -634,6 +638,45 @@ def requires_conversion(v):
assert result.imag == 0.0


@pytest.mark.parametrize("base", [object, int, float])
def test_complex_method_precedence(base):
class ComplexAndIndex(base):
def __complex__(self):
return 3.0 + 4.0j

def __index__(self):
return 42

value = ComplexAndIndex()
assert m.complex_convert(value) == 3.0 + 4.0j
if base is object:
with pytest.raises(TypeError):
m.complex_noconvert(value)
else:
assert m.complex_noconvert(value) == 3.0 + 4.0j


@pytest.mark.parametrize("function", [m.complex_convert, m.complex_noconvert])
def test_complex_subclass_with_index(function):
class ComplexWithIndex(complex):
def __index__(self):
return 42

assert function(ComplexWithIndex(3.0, 4.0)) == 3.0 + 4.0j


def test_complex_method_error_with_index():
class ComplexAndIndex:
def __complex__(self):
raise ValueError("complex conversion failed")

def __index__(self):
return 42

with pytest.raises(TypeError):
m.complex_convert(ComplexAndIndex())


def test_overload_resolution_float_int():
"""
Test overload resolution behavior when int can match float (added with PR #5879).
Expand Down
2 changes: 2 additions & 0 deletions tests/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,8 @@ class PyNonFinalFinalChild(m.IsNonFinalFinal):
def test_exception_rvalue_abort():
with pytest.raises(RuntimeError):
m.PyPrintDestructor().throw_something()
# The destructor calls Python; on PyPy, a later GC at an arbitrary point can abort
pytest.gc_collect()


# https://github.com/pybind/pybind11/issues/1568
Expand Down
6 changes: 6 additions & 0 deletions tests/test_enum.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ TEST_SUBMODULE(enums, m) {
.value("ESecondMode", ClassWithUnscopedEnum::ESecondMode)
.export_values();

// test_scalar_comparison_with_index_object
enum UnscopedInt32Enum : std::int32_t { I32Three = 3 };
py::enum_<UnscopedInt32Enum>(m, "UnscopedInt32Enum").value("Three", I32Three);
enum UnscopedUInt32Enum : std::uint32_t { U32Three = 3 };
py::enum_<UnscopedUInt32Enum>(m, "UnscopedUInt32Enum").value("Three", U32Three);

// test_enum_to_int
m.def("test_enum_to_int", [](int) {});
m.def("test_enum_to_uint", [](uint32_t) {});
Expand Down
20 changes: 20 additions & 0 deletions tests/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,26 @@ def test_enum_to_int():
m.test_enum_to_long_long(m.ScopedBoolEnum.TRUE)


@pytest.mark.parametrize("enum_name", ["UnscopedInt32Enum", "UnscopedUInt32Enum"])
def test_scalar_comparison_with_index_object(enum_name):
# An unsigned underlying type must compare like a signed one against objects
# that only implement __index__ (e.g. numpy integers).
class Index:
def __index__(self):
return 3

three = getattr(m, enum_name).Three
assert three == 3
assert three == Index()
assert not three != Index()
assert three != 4
np = pytest.importorskip("numpy")
assert three == np.int32(3)
assert three == np.uint64(3)
assert not three != np.int64(3)
assert three != np.int32(4)


def test_duplicate_enum_name():
with pytest.raises(ValueError) as excinfo:
m.register_bad_enum()
Expand Down
6 changes: 5 additions & 1 deletion tests/test_multiple_inheritance.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,13 @@ def test_mi_static_properties():
def test_mi_dynamic_attributes():
"""Mixing bases with and without dynamic attribute support"""

for d in (m.VanillaDictMix1(), m.VanillaDictMix2()):
for cls in (m.VanillaDictMix1, m.VanillaDictMix2):
d = cls()
d.dynamic = 1
assert d.dynamic == 1
del d
# The dict must be cleared without crashing (PyPy 3.12)
pytest.gc_collect()


def test_mi_unaligned_base():
Expand Down
1 change: 1 addition & 0 deletions tools/check_inl_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"PYBIND11_HAS_STRING_VIEW",
"PYBIND11_PRECOMPILED",
"PYPY_VERSION",
"PYPY_VERSION_NUM",
"PY_MAJOR_VERSION",
"PY_MINOR_VERSION",
"PY_VERSION_HEX",
Expand Down
Loading