Skip to content

Commit 9d46ab8

Browse files
committed
Add a test for shared-GIL (legacy) subinterpreters
1 parent d7420ca commit 9d46ab8

3 files changed

Lines changed: 73 additions & 3 deletions

File tree

tests/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,9 +566,12 @@ add_custom_target(
566566
if(NOT PYBIND11_CUDA_TESTS)
567567
# This module doesn't get mixed with other test modules because those aren't subinterpreter safe.
568568
pybind11_add_module(mod_test_interpreters THIN_LTO mod_test_interpreters.cpp)
569+
pybind11_add_module(mod_test_interpreters2 THIN_LTO mod_test_interpreters2.cpp)
569570
set_target_properties(mod_test_interpreters PROPERTIES LIBRARY_OUTPUT_DIRECTORY
570571
"$<1:${CMAKE_CURRENT_BINARY_DIR}>")
571-
add_dependencies(pytest mod_test_interpreters)
572+
set_target_properties(mod_test_interpreters2 PROPERTIES LIBRARY_OUTPUT_DIRECTORY
573+
"$<1:${CMAKE_CURRENT_BINARY_DIR}>")
574+
add_dependencies(pytest mod_test_interpreters mod_test_interpreters2)
572575
endif()
573576

574577
if(PYBIND11_TEST_OVERRIDE)

tests/mod_test_interpreters2.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#include <pybind11/pybind11.h>
2+
3+
namespace py = pybind11;
4+
5+
/* Simple test module/test class to check that the referenced internals data of external pybind11
6+
* modules are different across subinterpreters
7+
*/
8+
9+
PYBIND11_MODULE(mod_test_interpreters2,
10+
m,
11+
py::multiple_interpreters(py::multiple_interpreters::shared_gil)) {
12+
m.def("internals_at",
13+
[]() { return reinterpret_cast<uintptr_t>(&py::detail::get_internals()); });
14+
}

tests/test_interpreters.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
@pytest.mark.skipif(
1111
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
1212
)
13-
def test_interpreters():
14-
"""Makes sure the internals object differs across subinterpreters"""
13+
def test_independent_subinterpreters():
14+
"""Makes sure the internals object differs across independent subinterpreters"""
1515

1616
sys.path.append(".")
1717

@@ -36,6 +36,8 @@ def test_interpreters():
3636
interp1 = interpreters.create()
3737
interp2 = interpreters.create()
3838
try:
39+
res0 = interpreters.run_string(interp1, "import mod_test_interpreters2")
40+
3941
pipei, pipeo = os.pipe()
4042
interpreters.run_string(interp1, code, shared={"pipeo": pipeo})
4143
with open(pipei, "rb") as f:
@@ -56,6 +58,7 @@ def test_interpreters():
5658
interpreters.destroy(interp1)
5759
interpreters.destroy(interp2)
5860

61+
assert "does not support loading in subinterpreters" in res0.msg, "cannot use shared_gil in a default subinterpreter"
5962
assert res1 != m.internals_at(), "internals should differ from main interpreter"
6063
assert res2 != m.internals_at(), "internals should differ from main interpreter"
6164
assert res1 != res2, "internals should differ between interpreters"
@@ -66,3 +69,53 @@ def test_interpreters():
6669
assert m.internals_at() == m3.internals_at(), (
6770
"internals should be the same within the main interpreter"
6871
)
72+
73+
@pytest.mark.skipif(
74+
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
75+
)
76+
def test_dependent_subinterpreters():
77+
"""Makes sure the internals object differs across subinterpreters"""
78+
79+
sys.path.append(".")
80+
81+
if sys.version_info >= (3, 14):
82+
import interpreters
83+
elif sys.version_info >= (3, 13):
84+
import _interpreters as interpreters
85+
elif sys.version_info >= (3, 12):
86+
import _xxsubinterpreters as interpreters
87+
else:
88+
pytest.skip("Test requires a the interpreters stdlib module")
89+
90+
import mod_test_interpreters2 as m
91+
92+
code = """
93+
import mod_test_interpreters2 as m
94+
import pickle
95+
with open(pipeo, 'wb') as f:
96+
pickle.dump(m.internals_at(), f)
97+
"""
98+
99+
interp1 = interpreters.create("legacy")
100+
try:
101+
pipei, pipeo = os.pipe()
102+
interpreters.run_string(interp1, code, shared={"pipeo": pipeo})
103+
with open(pipei, "rb") as f:
104+
res1 = pickle.load(f)
105+
106+
# do this while the two interpreters are active
107+
import mod_test_interpreters2 as m2
108+
109+
assert m.internals_at() == m2.internals_at(), (
110+
"internals should be the same within the main interpreter"
111+
)
112+
finally:
113+
interpreters.destroy(interp1)
114+
115+
assert res1 != m.internals_at(), "internals should differ from main interpreter"
116+
117+
# do this after the two interpreters are destroyed and only one remains
118+
import mod_test_interpreters2 as m3
119+
assert m.internals_at() == m3.internals_at(), (
120+
"internals should be the same within the main interpreter"
121+
)

0 commit comments

Comments
 (0)