Try add tests

This commit is contained in:
Xuehai Pan
2026-01-24 12:59:38 +08:00
parent 436d812f76
commit ce9ca7f3aa
5 changed files with 141 additions and 54 deletions

View File

@@ -1381,11 +1381,22 @@ You can do that using ``py::custom_type_setup``:
.. code-block:: cpp
struct OwnsPythonObjects {
py::object value = py::none();
struct ContainerOwnsPythonObjects {
std::vector<py::object> list;
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(); }
};
py::class_<OwnsPythonObjects> cls(
m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) {
py::class_<ContainerOwnsPythonObjects> cls(
m, "ContainerOwnsPythonObjects", 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) {
@@ -1394,20 +1405,28 @@ You can do that using ``py::custom_type_setup``:
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);
.. versionadded:: 2.8

View File

@@ -29,3 +29,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

@@ -7,22 +7,64 @@
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, obj](py::handle weakref) -> void {
py::handle new_global_capsule = py::detail::get_internals_capsule();
if (!new_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, obj](py::handle weakref) -> void {
py::handle new_local_capsule = py::detail::get_local_internals_capsule();
if (!new_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", 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 +73,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

@@ -5,7 +5,7 @@ import weakref
import pytest
import env # noqa: F401
import env
from pybind11_tests import custom_type_setup as m
@@ -36,15 +36,27 @@ 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.PYPY or env.GRAALPY")
def test_py_cast_useable_on_shutdown():
env.check_script_success_in_subprocess(
"""
from pybind11_tests import custom_type_setup as m
obj = m.ContainerOwnsPythonObjects()
obj.append(obj)
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
@@ -269,36 +268,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 +295,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 +330,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 +351,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 +377,7 @@ def test_import_in_subinterpreter_before_main():
)
)
check_script_success_in_subprocess(
env.check_script_success_in_subprocess(
PREAMBLE_CODE
+ textwrap.dedent(
"""
@@ -434,7 +410,7 @@ def test_import_in_subinterpreter_before_main():
@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(
"""