Merge branch 'master' into henryiii-patch-3

This commit is contained in:
Henry Schreiner
2026-02-10 00:28:02 -05:00
committed by GitHub
18 changed files with 453 additions and 144 deletions

View File

@@ -11,6 +11,18 @@ MACOS = sys.platform.startswith("darwin")
WIN = sys.platform.startswith("win32") or sys.platform.startswith("cygwin")
FREEBSD = sys.platform.startswith("freebsd")
MUSLLINUX = False
MANYLINUX = False
if LINUX:
def _is_musl() -> bool:
libc, _ = platform.libc_ver()
return libc == "musl" or (libc != "glibc" and libc != "")
MUSLLINUX = _is_musl()
MANYLINUX = not MUSLLINUX
del _is_musl
CPYTHON = platform.python_implementation() == "CPython"
PYPY = platform.python_implementation() == "PyPy"
GRAALPY = sys.implementation.name == "graalpy"
@@ -29,3 +41,31 @@ TYPES_ARE_IMMORTAL = (
or GRAALPY
or (CPYTHON and PY_GIL_DISABLED and (3, 13) <= sys.version_info < (3, 14))
)
def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None:
"""Runs the given code in a subprocess."""
import os
import subprocess
import sys
import textwrap
code = textwrap.dedent(code).strip()
try:
for _ in range(rerun): # run flakily failing test multiple times
subprocess.check_output(
[sys.executable, "-c", code],
cwd=os.getcwd(),
stderr=subprocess.STDOUT,
text=True,
)
except subprocess.CalledProcessError as ex:
raise RuntimeError(
f"Subprocess failed with exit code {ex.returncode}.\n\n"
f"Code:\n"
f"```python\n"
f"{code}\n"
f"```\n\n"
f"Output:\n"
f"{ex.output}"
) from None

View File

@@ -10,7 +10,7 @@ numpy~=1.22.2; platform_python_implementation=="CPython" and python_version=="3.
numpy~=1.26.0; platform_python_implementation=="CPython" and python_version>="3.11" and python_version<"3.13" and platform_machine!="ARM64"
numpy>=2.3.0; platform_python_implementation=="CPython" and python_version>="3.11" and platform_machine=="ARM64"
numpy~=2.2.0; platform_python_implementation=="CPython" and python_version=="3.13" and platform_machine!="ARM64"
numpy==2.4.0; platform_python_implementation=="CPython" and python_version>="3.14"
numpy>=2.4.0; platform_python_implementation=="CPython" and python_version>="3.14"
pytest>=6
pytest-timeout
scipy~=1.5.4; platform_python_implementation=="CPython" and python_version<"3.10"

View File

@@ -7,22 +7,67 @@
BSD-style license that can be found in the LICENSE file.
*/
#include <pybind11/detail/internals.h>
#include <pybind11/pybind11.h>
#include "pybind11_tests.h"
#include <vector>
namespace py = pybind11;
namespace {
struct ContainerOwnsPythonObjects {
std::vector<py::object> list;
struct OwnsPythonObjects {
py::object value = py::none();
void append(const py::object &obj) { list.emplace_back(obj); }
py::object at(py::ssize_t index) const {
if (index >= size() || index < 0) {
throw py::index_error("Index out of range");
}
return list.at(py::size_t(index));
}
py::ssize_t size() const { return py::ssize_t_cast(list.size()); }
void clear() { list.clear(); }
};
void add_gc_checkers_with_weakrefs(const py::object &obj) {
py::handle global_capsule = py::detail::get_internals_capsule();
if (!global_capsule) {
throw std::runtime_error("No global internals capsule found");
}
(void) py::weakref(obj, py::cpp_function([global_capsule](py::handle weakref) -> void {
py::handle current_global_capsule = py::detail::get_internals_capsule();
if (!current_global_capsule.is(global_capsule)) {
throw std::runtime_error(
"Global internals capsule was destroyed prematurely");
}
weakref.dec_ref();
}))
.release();
py::handle local_capsule = py::detail::get_local_internals_capsule();
if (!local_capsule) {
throw std::runtime_error("No local internals capsule found");
}
(void) py::weakref(
obj, py::cpp_function([local_capsule](py::handle weakref) -> void {
py::handle current_local_capsule = py::detail::get_local_internals_capsule();
if (!current_local_capsule.is(local_capsule)) {
throw std::runtime_error("Local internals capsule was destroyed prematurely");
}
weakref.dec_ref();
}))
.release();
}
} // namespace
TEST_SUBMODULE(custom_type_setup, m) {
py::class_<OwnsPythonObjects> cls(
m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) {
py::class_<ContainerOwnsPythonObjects> cls(
m,
"ContainerOwnsPythonObjects",
// Please review/update docs/advanced/classes.rst after making changes here.
py::custom_type_setup([](PyHeapTypeObject *heap_type) {
auto *type = &heap_type->ht_type;
type->tp_flags |= Py_TPFLAGS_HAVE_GC;
type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) {
@@ -31,19 +76,29 @@ TEST_SUBMODULE(custom_type_setup, m) {
Py_VISIT(Py_TYPE(self_base));
#endif
if (py::detail::is_holder_constructed(self_base)) {
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
Py_VISIT(self.value.ptr());
auto &self = py::cast<ContainerOwnsPythonObjects &>(py::handle(self_base));
for (auto &item : self.list) {
Py_VISIT(item.ptr());
}
}
return 0;
};
type->tp_clear = [](PyObject *self_base) {
if (py::detail::is_holder_constructed(self_base)) {
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
self.value = py::none();
auto &self = py::cast<ContainerOwnsPythonObjects &>(py::handle(self_base));
for (auto &item : self.list) {
Py_CLEAR(item.ptr());
}
self.list.clear();
}
return 0;
};
}));
cls.def(py::init<>());
cls.def_readwrite("value", &OwnsPythonObjects::value);
cls.def("append", &ContainerOwnsPythonObjects::append);
cls.def("at", &ContainerOwnsPythonObjects::at);
cls.def("size", &ContainerOwnsPythonObjects::size);
cls.def("clear", &ContainerOwnsPythonObjects::clear);
m.def("add_gc_checkers_with_weakrefs", &add_gc_checkers_with_weakrefs);
}

View File

@@ -1,11 +1,14 @@
from __future__ import annotations
import gc
import os
import sys
import weakref
import pytest
import env # noqa: F401
import env
import pybind11_tests
from pybind11_tests import custom_type_setup as m
@@ -36,15 +39,46 @@ def gc_tester():
# PyPy does not seem to reliably garbage collect.
@pytest.mark.skipif("env.PYPY or env.GRAALPY")
def test_self_cycle(gc_tester):
obj = m.OwnsPythonObjects()
obj.value = obj
obj = m.ContainerOwnsPythonObjects()
obj.append(obj)
gc_tester(obj)
# PyPy does not seem to reliably garbage collect.
@pytest.mark.skipif("env.PYPY or env.GRAALPY")
def test_indirect_cycle(gc_tester):
obj = m.OwnsPythonObjects()
obj_list = [obj]
obj.value = obj_list
obj = m.ContainerOwnsPythonObjects()
obj.append([obj])
gc_tester(obj)
@pytest.mark.skipif(
env.IOS or sys.platform.startswith("emscripten"),
reason="Requires subprocess support",
)
@pytest.mark.skipif("env.PYPY or env.GRAALPY")
def test_py_cast_useable_on_shutdown():
"""Test that py::cast works during interpreter shutdown.
See PR #5972 and https://github.com/pybind/pybind11/pull/5958#discussion_r2717645230.
"""
env.check_script_success_in_subprocess(
f"""
import sys
sys.path.insert(0, {os.path.dirname(env.__file__)!r})
sys.path.insert(0, {os.path.dirname(pybind11_tests.__file__)!r})
from pybind11_tests import custom_type_setup as m
# Create a self-referential cycle that will be collected during shutdown.
# The tp_traverse and tp_clear callbacks call py::cast, which requires
# internals to still be valid.
obj = m.ContainerOwnsPythonObjects()
obj.append(obj)
# Add weakref callbacks that verify the capsule is still alive when the
# pybind11 object is garbage collected during shutdown.
m.add_gc_checkers_with_weakrefs(obj)
"""
)

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import contextlib
import os
import pickle
import subprocess
import sys
import textwrap
@@ -219,6 +218,7 @@ PREAMBLE_CODE = textwrap.dedent(
def test():
import sys
sys.path.insert(0, {os.path.dirname(env.__file__)!r})
sys.path.insert(0, {os.path.dirname(pybind11_tests.__file__)!r})
import collections
@@ -269,36 +269,13 @@ def test_import_module_with_singleton_per_interpreter():
interp.exec(code)
def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None:
"""Runs the given code in a subprocess."""
code = textwrap.dedent(code).strip()
try:
for _ in range(rerun): # run flakily failing test multiple times
subprocess.check_output(
[sys.executable, "-c", code],
cwd=os.getcwd(),
stderr=subprocess.STDOUT,
text=True,
)
except subprocess.CalledProcessError as ex:
raise RuntimeError(
f"Subprocess failed with exit code {ex.returncode}.\n\n"
f"Code:\n"
f"```python\n"
f"{code}\n"
f"```\n\n"
f"Output:\n"
f"{ex.output}"
) from None
@pytest.mark.skipif(
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
)
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
def test_import_in_subinterpreter_after_main():
"""Tests that importing a module in a subinterpreter after the main interpreter works correctly"""
check_script_success_in_subprocess(
env.check_script_success_in_subprocess(
PREAMBLE_CODE
+ textwrap.dedent(
"""
@@ -319,7 +296,7 @@ def test_import_in_subinterpreter_after_main():
)
)
check_script_success_in_subprocess(
env.check_script_success_in_subprocess(
PREAMBLE_CODE
+ textwrap.dedent(
"""
@@ -354,7 +331,7 @@ def test_import_in_subinterpreter_after_main():
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
def test_import_in_subinterpreter_before_main():
"""Tests that importing a module in a subinterpreter before the main interpreter works correctly"""
check_script_success_in_subprocess(
env.check_script_success_in_subprocess(
PREAMBLE_CODE
+ textwrap.dedent(
"""
@@ -375,7 +352,7 @@ def test_import_in_subinterpreter_before_main():
)
)
check_script_success_in_subprocess(
env.check_script_success_in_subprocess(
PREAMBLE_CODE
+ textwrap.dedent(
"""
@@ -401,7 +378,7 @@ def test_import_in_subinterpreter_before_main():
)
)
check_script_success_in_subprocess(
env.check_script_success_in_subprocess(
PREAMBLE_CODE
+ textwrap.dedent(
"""
@@ -431,10 +408,15 @@ def test_import_in_subinterpreter_before_main():
@pytest.mark.skipif(
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
)
@pytest.mark.xfail(
env.MUSLLINUX,
reason="Flaky on musllinux, see also: https://github.com/pybind/pybind11/pull/5972#discussion_r2755283335",
strict=False,
)
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
def test_import_in_subinterpreter_concurrently():
"""Tests that importing a module in multiple subinterpreters concurrently works correctly"""
check_script_success_in_subprocess(
env.check_script_success_in_subprocess(
PREAMBLE_CODE
+ textwrap.dedent(
"""

View File

@@ -14,6 +14,7 @@
#include <cstdint>
#include <utility>
#include <vector>
// Size / dtype checks.
struct DtypeCheck {
@@ -246,6 +247,22 @@ TEST_SUBMODULE(numpy_array, sm) {
sm.def("nbytes", [](const arr &a) { return a.nbytes(); });
sm.def("owndata", [](const arr &a) { return a.owndata(); });
#ifdef PYBIND11_HAS_SPAN
// test_shape_strides_span
sm.def("shape_span", [](const arr &a) {
auto span = a.shape_span();
return std::vector<py::ssize_t>(span.begin(), span.end());
});
sm.def("strides_span", [](const arr &a) {
auto span = a.strides_span();
return std::vector<py::ssize_t>(span.begin(), span.end());
});
// Test that spans can be used to construct new arrays
sm.def("array_from_spans", [](const arr &a) {
return py::array(a.dtype(), a.shape_span(), a.strides_span(), a.data(), a);
});
#endif
// test_index_offset
def_index_fn(index_at, const arr &);
def_index_fn(index_at_t, const arr_t &);

View File

@@ -68,6 +68,45 @@ def test_array_attributes():
assert not m.owndata(a)
@pytest.mark.skipif(not hasattr(m, "shape_span"), reason="std::span not available")
def test_shape_strides_span():
# Test 0-dimensional array (scalar)
a = np.array(42, "f8")
assert m.ndim(a) == 0
assert m.shape_span(a) == []
assert m.strides_span(a) == []
# Test 1-dimensional array
a = np.array([1, 2, 3, 4], "u2")
assert m.ndim(a) == 1
assert m.shape_span(a) == [4]
assert m.strides_span(a) == [2]
# Test 2-dimensional array
a = np.array([[1, 2, 3], [4, 5, 6]], "u2").view()
a.flags.writeable = False
assert m.ndim(a) == 2
assert m.shape_span(a) == [2, 3]
assert m.strides_span(a) == [6, 2]
# Test 3-dimensional array
a = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], "i4")
assert m.ndim(a) == 3
assert m.shape_span(a) == [2, 2, 2]
# Verify spans match regular shape/strides
assert list(m.shape_span(a)) == list(m.shape(a))
assert list(m.strides_span(a)) == list(m.strides(a))
# Test that spans can be used to construct new arrays
original = np.array([[1, 2, 3], [4, 5, 6]], "f4")
new_array = m.array_from_spans(original)
assert new_array.shape == original.shape
assert new_array.strides == original.strides
assert new_array.dtype == original.dtype
# Verify data is shared (since we pass the same data pointer)
np.testing.assert_array_equal(new_array, original)
@pytest.mark.parametrize(
("args", "ret"), [([], 0), ([0], 0), ([1], 3), ([0, 1], 1), ([1, 2], 5)]
)

View File

@@ -501,15 +501,21 @@ TEST_CASE("Per-Subinterpreter GIL") {
// wait for something to set sync to our thread number
// we are holding our subinterpreter's GIL
while (sync != num)
std::this_thread::sleep_for(std::chrono::microseconds(1));
{
py::gil_scoped_release nogil;
while (sync != num)
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
// now change it so the next thread can move on
++sync;
// but keep holding the GIL until after the next thread moves on as well
while (sync == num + 1)
std::this_thread::sleep_for(std::chrono::microseconds(1));
{
py::gil_scoped_release nogil;
while (sync == num + 1)
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
// one last check before quitting the thread, the internals should be different
auto sub_int