mirror of
https://github.com/pybind/pybind11.git
synced 2026-06-11 08:47:58 +00:00
Merge branch 'master' into henryiii-patch-3
This commit is contained in:
40
tests/env.py
40
tests/env.py
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
"""
|
||||
|
||||
@@ -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 &);
|
||||
|
||||
@@ -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)]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user