Skip to content

Commit ce9ca7f

Browse files
committed
Try add tests
1 parent 436d812 commit ce9ca7f

File tree

5 files changed

+141
-54
lines changed

5 files changed

+141
-54
lines changed

docs/advanced/classes.rst

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,11 +1381,22 @@ You can do that using ``py::custom_type_setup``:
13811381

13821382
.. code-block:: cpp
13831383
1384-
struct OwnsPythonObjects {
1385-
py::object value = py::none();
1384+
struct ContainerOwnsPythonObjects {
1385+
std::vector<py::object> list;
1386+
1387+
void append(const py::object &obj) { list.emplace_back(obj); }
1388+
py::object at(py::ssize_t index) const {
1389+
if (index >= size() || index < 0) {
1390+
throw py::index_error("Index out of range");
1391+
}
1392+
return list.at(py::size_t(index));
1393+
}
1394+
py::ssize_t size() const { return py::ssize_t_cast(list.size()); }
1395+
void clear() { list.clear(); }
13861396
};
1387-
py::class_<OwnsPythonObjects> cls(
1388-
m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) {
1397+
1398+
py::class_<ContainerOwnsPythonObjects> cls(
1399+
m, "ContainerOwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) {
13891400
auto *type = &heap_type->ht_type;
13901401
type->tp_flags |= Py_TPFLAGS_HAVE_GC;
13911402
type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) {
@@ -1394,20 +1405,28 @@ You can do that using ``py::custom_type_setup``:
13941405
Py_VISIT(Py_TYPE(self_base));
13951406
#endif
13961407
if (py::detail::is_holder_constructed(self_base)) {
1397-
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
1398-
Py_VISIT(self.value.ptr());
1408+
auto &self = py::cast<ContainerOwnsPythonObjects &>(py::handle(self_base));
1409+
for (auto &item : self.list) {
1410+
Py_VISIT(item.ptr());
1411+
}
13991412
}
14001413
return 0;
14011414
};
14021415
type->tp_clear = [](PyObject *self_base) {
14031416
if (py::detail::is_holder_constructed(self_base)) {
1404-
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
1405-
self.value = py::none();
1417+
auto &self = py::cast<ContainerOwnsPythonObjects &>(py::handle(self_base));
1418+
for (auto &item : self.list) {
1419+
Py_CLEAR(item.ptr());
1420+
}
1421+
self.list.clear();
14061422
}
14071423
return 0;
14081424
};
14091425
}));
14101426
cls.def(py::init<>());
1411-
cls.def_readwrite("value", &OwnsPythonObjects::value);
1427+
cls.def("append", &ContainerOwnsPythonObjects::append);
1428+
cls.def("at", &ContainerOwnsPythonObjects::at);
1429+
cls.def("size", &ContainerOwnsPythonObjects::size);
1430+
cls.def("clear", &ContainerOwnsPythonObjects::clear);
14121431
14131432
.. versionadded:: 2.8

tests/env.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,31 @@
2929
or GRAALPY
3030
or (CPYTHON and PY_GIL_DISABLED and (3, 13) <= sys.version_info < (3, 14))
3131
)
32+
33+
34+
def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None:
35+
"""Runs the given code in a subprocess."""
36+
import os
37+
import subprocess
38+
import sys
39+
import textwrap
40+
41+
code = textwrap.dedent(code).strip()
42+
try:
43+
for _ in range(rerun): # run flakily failing test multiple times
44+
subprocess.check_output(
45+
[sys.executable, "-c", code],
46+
cwd=os.getcwd(),
47+
stderr=subprocess.STDOUT,
48+
text=True,
49+
)
50+
except subprocess.CalledProcessError as ex:
51+
raise RuntimeError(
52+
f"Subprocess failed with exit code {ex.returncode}.\n\n"
53+
f"Code:\n"
54+
f"```python\n"
55+
f"{code}\n"
56+
f"```\n\n"
57+
f"Output:\n"
58+
f"{ex.output}"
59+
) from None

tests/test_custom_type_setup.cpp

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,64 @@
77
BSD-style license that can be found in the LICENSE file.
88
*/
99

10+
#include <pybind11/detail/internals.h>
1011
#include <pybind11/pybind11.h>
1112

1213
#include "pybind11_tests.h"
1314

15+
#include <vector>
16+
1417
namespace py = pybind11;
1518

1619
namespace {
20+
struct ContainerOwnsPythonObjects {
21+
std::vector<py::object> list;
1722

18-
struct OwnsPythonObjects {
19-
py::object value = py::none();
23+
void append(const py::object &obj) { list.emplace_back(obj); }
24+
py::object at(py::ssize_t index) const {
25+
if (index >= size() || index < 0) {
26+
throw py::index_error("Index out of range");
27+
}
28+
return list.at(py::size_t(index));
29+
}
30+
py::ssize_t size() const { return py::ssize_t_cast(list.size()); }
31+
void clear() { list.clear(); }
2032
};
33+
34+
void add_gc_checkers_with_weakrefs(const py::object &obj) {
35+
py::handle global_capsule = py::detail::get_internals_capsule();
36+
if (!global_capsule) {
37+
throw std::runtime_error("No global internals capsule found");
38+
}
39+
(void) py::weakref(obj, py::cpp_function([global_capsule, obj](py::handle weakref) -> void {
40+
py::handle new_global_capsule = py::detail::get_internals_capsule();
41+
if (!new_global_capsule.is(global_capsule)) {
42+
throw std::runtime_error(
43+
"Global internals capsule was destroyed prematurely");
44+
}
45+
weakref.dec_ref();
46+
}))
47+
.release();
48+
49+
py::handle local_capsule = py::detail::get_local_internals_capsule();
50+
if (!local_capsule) {
51+
throw std::runtime_error("No local internals capsule found");
52+
}
53+
(void) py::weakref(
54+
obj, py::cpp_function([local_capsule, obj](py::handle weakref) -> void {
55+
py::handle new_local_capsule = py::detail::get_local_internals_capsule();
56+
if (!new_local_capsule.is(local_capsule)) {
57+
throw std::runtime_error("Local internals capsule was destroyed prematurely");
58+
}
59+
weakref.dec_ref();
60+
}))
61+
.release();
62+
}
2163
} // namespace
2264

2365
TEST_SUBMODULE(custom_type_setup, m) {
24-
py::class_<OwnsPythonObjects> cls(
25-
m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) {
66+
py::class_<ContainerOwnsPythonObjects> cls(
67+
m, "ContainerOwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) {
2668
auto *type = &heap_type->ht_type;
2769
type->tp_flags |= Py_TPFLAGS_HAVE_GC;
2870
type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) {
@@ -31,19 +73,29 @@ TEST_SUBMODULE(custom_type_setup, m) {
3173
Py_VISIT(Py_TYPE(self_base));
3274
#endif
3375
if (py::detail::is_holder_constructed(self_base)) {
34-
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
35-
Py_VISIT(self.value.ptr());
76+
auto &self = py::cast<ContainerOwnsPythonObjects &>(py::handle(self_base));
77+
for (auto &item : self.list) {
78+
Py_VISIT(item.ptr());
79+
}
3680
}
3781
return 0;
3882
};
3983
type->tp_clear = [](PyObject *self_base) {
4084
if (py::detail::is_holder_constructed(self_base)) {
41-
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
42-
self.value = py::none();
85+
auto &self = py::cast<ContainerOwnsPythonObjects &>(py::handle(self_base));
86+
for (auto &item : self.list) {
87+
Py_CLEAR(item.ptr());
88+
}
89+
self.list.clear();
4390
}
4491
return 0;
4592
};
4693
}));
4794
cls.def(py::init<>());
48-
cls.def_readwrite("value", &OwnsPythonObjects::value);
95+
cls.def("append", &ContainerOwnsPythonObjects::append);
96+
cls.def("at", &ContainerOwnsPythonObjects::at);
97+
cls.def("size", &ContainerOwnsPythonObjects::size);
98+
cls.def("clear", &ContainerOwnsPythonObjects::clear);
99+
100+
m.def("add_gc_checkers_with_weakrefs", &add_gc_checkers_with_weakrefs);
49101
}

tests/test_custom_type_setup.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import pytest
77

8-
import env # noqa: F401
8+
import env
99
from pybind11_tests import custom_type_setup as m
1010

1111

@@ -36,15 +36,27 @@ def add_ref(obj):
3636
# PyPy does not seem to reliably garbage collect.
3737
@pytest.mark.skipif("env.PYPY or env.GRAALPY")
3838
def test_self_cycle(gc_tester):
39-
obj = m.OwnsPythonObjects()
40-
obj.value = obj
39+
obj = m.ContainerOwnsPythonObjects()
40+
obj.append(obj)
4141
gc_tester(obj)
4242

4343

4444
# PyPy does not seem to reliably garbage collect.
4545
@pytest.mark.skipif("env.PYPY or env.GRAALPY")
4646
def test_indirect_cycle(gc_tester):
47-
obj = m.OwnsPythonObjects()
48-
obj_list = [obj]
49-
obj.value = obj_list
47+
obj = m.ContainerOwnsPythonObjects()
48+
obj.append([obj])
5049
gc_tester(obj)
50+
51+
52+
@pytest.mark.skipif("env.PYPY or env.GRAALPY")
53+
def test_py_cast_useable_on_shutdown():
54+
env.check_script_success_in_subprocess(
55+
"""
56+
from pybind11_tests import custom_type_setup as m
57+
58+
obj = m.ContainerOwnsPythonObjects()
59+
obj.append(obj)
60+
m.add_gc_checkers_with_weakrefs(obj)
61+
"""
62+
)

tests/test_multiple_interpreters.py

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import contextlib
44
import os
55
import pickle
6-
import subprocess
76
import sys
87
import textwrap
98

@@ -269,36 +268,13 @@ def test_import_module_with_singleton_per_interpreter():
269268
interp.exec(code)
270269

271270

272-
def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None:
273-
"""Runs the given code in a subprocess."""
274-
code = textwrap.dedent(code).strip()
275-
try:
276-
for _ in range(rerun): # run flakily failing test multiple times
277-
subprocess.check_output(
278-
[sys.executable, "-c", code],
279-
cwd=os.getcwd(),
280-
stderr=subprocess.STDOUT,
281-
text=True,
282-
)
283-
except subprocess.CalledProcessError as ex:
284-
raise RuntimeError(
285-
f"Subprocess failed with exit code {ex.returncode}.\n\n"
286-
f"Code:\n"
287-
f"```python\n"
288-
f"{code}\n"
289-
f"```\n\n"
290-
f"Output:\n"
291-
f"{ex.output}"
292-
) from None
293-
294-
295271
@pytest.mark.skipif(
296272
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
297273
)
298274
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
299275
def test_import_in_subinterpreter_after_main():
300276
"""Tests that importing a module in a subinterpreter after the main interpreter works correctly"""
301-
check_script_success_in_subprocess(
277+
env.check_script_success_in_subprocess(
302278
PREAMBLE_CODE
303279
+ textwrap.dedent(
304280
"""
@@ -319,7 +295,7 @@ def test_import_in_subinterpreter_after_main():
319295
)
320296
)
321297

322-
check_script_success_in_subprocess(
298+
env.check_script_success_in_subprocess(
323299
PREAMBLE_CODE
324300
+ textwrap.dedent(
325301
"""
@@ -354,7 +330,7 @@ def test_import_in_subinterpreter_after_main():
354330
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
355331
def test_import_in_subinterpreter_before_main():
356332
"""Tests that importing a module in a subinterpreter before the main interpreter works correctly"""
357-
check_script_success_in_subprocess(
333+
env.check_script_success_in_subprocess(
358334
PREAMBLE_CODE
359335
+ textwrap.dedent(
360336
"""
@@ -375,7 +351,7 @@ def test_import_in_subinterpreter_before_main():
375351
)
376352
)
377353

378-
check_script_success_in_subprocess(
354+
env.check_script_success_in_subprocess(
379355
PREAMBLE_CODE
380356
+ textwrap.dedent(
381357
"""
@@ -401,7 +377,7 @@ def test_import_in_subinterpreter_before_main():
401377
)
402378
)
403379

404-
check_script_success_in_subprocess(
380+
env.check_script_success_in_subprocess(
405381
PREAMBLE_CODE
406382
+ textwrap.dedent(
407383
"""
@@ -434,7 +410,7 @@ def test_import_in_subinterpreter_before_main():
434410
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
435411
def test_import_in_subinterpreter_concurrently():
436412
"""Tests that importing a module in multiple subinterpreters concurrently works correctly"""
437-
check_script_success_in_subprocess(
413+
env.check_script_success_in_subprocess(
438414
PREAMBLE_CODE
439415
+ textwrap.dedent(
440416
"""

0 commit comments

Comments
 (0)