Skip to content

Commit c60c149

Browse files
msimacekhenryiiipre-commit-ci[bot]
authored
tests: handle 3.12 and 3.13 implementations and 3.14.0b3+ (#5732)
* Use pytest.importorskip to get _xxsubinterpreters * tests: use modern interpreter API Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * tests: try to fix beta2 Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * fix: remove debug printout Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * tests: drop useless checks Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * tests: improve check for 3.14.0b3 and b4+ Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * style: pre-commit fixes --------- Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent be507b7 commit c60c149

File tree

1 file changed

+121
-62
lines changed

1 file changed

+121
-62
lines changed

tests/test_multiple_interpreters.py

Lines changed: 121 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,80 @@
11
from __future__ import annotations
22

3+
import contextlib
34
import os
45
import pickle
56
import sys
67

78
import pytest
89

10+
# 3.14.0b3+, though sys.implementation.supports_isolated_interpreters is being added in b4
11+
# Can be simplified when we drop support for the first three betas
12+
CONCURRENT_INTERPRETERS_SUPPORT = (
13+
sys.version_info >= (3, 14)
14+
and (
15+
sys.version_info != (3, 14, 0, "beta", 1)
16+
and sys.version_info != (3, 14, 0, "beta", 2)
17+
)
18+
and (
19+
sys.version_info == (3, 14, 0, "beta", 3)
20+
or sys.implementation.supports_isolated_interpreters
21+
)
22+
)
23+
24+
25+
def get_interpreters(*, modern: bool):
26+
if modern and CONCURRENT_INTERPRETERS_SUPPORT:
27+
from concurrent import interpreters
28+
29+
def create():
30+
return contextlib.closing(interpreters.create())
31+
32+
def run_string(
33+
interp: interpreters.Interpreter,
34+
code: str,
35+
*,
36+
shared: dict[str, object] | None = None,
37+
) -> Exception | None:
38+
if shared:
39+
interp.prepare_main(**shared)
40+
try:
41+
interp.exec(code)
42+
return None
43+
except interpreters.ExecutionFailed as err:
44+
return err
45+
46+
return run_string, create
47+
48+
if sys.version_info >= (3, 12):
49+
interpreters = pytest.importorskip(
50+
"_interpreters" if sys.version_info >= (3, 13) else "_xxsubinterpreters"
51+
)
52+
53+
@contextlib.contextmanager
54+
def create(config: str = ""):
55+
try:
56+
if config:
57+
interp = interpreters.create(config)
58+
else:
59+
interp = interpreters.create()
60+
except TypeError:
61+
pytest.skip(f"interpreters module needs to support {config} config")
62+
63+
try:
64+
yield interp
65+
finally:
66+
interpreters.destroy(interp)
67+
68+
def run_string(
69+
interp: int, code: str, shared: dict[str, object] | None = None
70+
) -> Exception | None:
71+
kwargs = {"shared": shared} if shared else {}
72+
return interpreters.run_string(interp, code, **kwargs)
73+
74+
return run_string, create
75+
76+
pytest.skip("Test requires the interpreters stdlib module")
77+
978

1079
@pytest.mark.skipif(
1180
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
@@ -15,15 +84,7 @@ def test_independent_subinterpreters():
1584

1685
sys.path.append(".")
1786

18-
# This is supposed to be added in 3.14.0b3
19-
if sys.version_info >= (3, 15):
20-
import interpreters
21-
elif sys.version_info >= (3, 13):
22-
import _interpreters as interpreters
23-
elif sys.version_info >= (3, 12):
24-
import _xxsubinterpreters as interpreters
25-
else:
26-
pytest.skip("Test requires the interpreters stdlib module")
87+
run_string, create = get_interpreters(modern=True)
2788

2889
m = pytest.importorskip("mod_per_interpreter_gil")
2990

@@ -37,49 +98,75 @@ def test_independent_subinterpreters():
3798
pickle.dump(m.internals_at(), f)
3899
"""
39100

40-
interp1 = interpreters.create()
41-
interp2 = interpreters.create()
42-
try:
101+
with create() as interp1, create() as interp2:
43102
try:
44-
res0 = interpreters.run_string(interp1, "import mod_shared_interpreter_gil")
103+
res0 = run_string(interp1, "import mod_shared_interpreter_gil")
45104
if res0 is not None:
46-
res0 = res0.msg
105+
res0 = str(res0)
47106
except Exception as e:
48107
res0 = str(e)
49108

50109
pipei, pipeo = os.pipe()
51-
interpreters.run_string(interp1, code, shared={"pipeo": pipeo})
110+
run_string(interp1, code, shared={"pipeo": pipeo})
52111
with open(pipei, "rb") as f:
53112
res1 = pickle.load(f)
54113

55114
pipei, pipeo = os.pipe()
56-
interpreters.run_string(interp2, code, shared={"pipeo": pipeo})
115+
run_string(interp2, code, shared={"pipeo": pipeo})
57116
with open(pipei, "rb") as f:
58117
res2 = pickle.load(f)
59118

60-
# do this while the two interpreters are active
61-
import mod_per_interpreter_gil as m2
62-
63-
assert m.internals_at() == m2.internals_at(), (
64-
"internals should be the same within the main interpreter"
65-
)
66-
finally:
67-
interpreters.destroy(interp1)
68-
interpreters.destroy(interp2)
69-
70119
assert "does not support loading in subinterpreters" in res0, (
71120
"cannot use shared_gil in a default subinterpreter"
72121
)
73122
assert res1 != m.internals_at(), "internals should differ from main interpreter"
74123
assert res2 != m.internals_at(), "internals should differ from main interpreter"
75124
assert res1 != res2, "internals should differ between interpreters"
76125

77-
# do this after the two interpreters are destroyed and only one remains
78-
import mod_per_interpreter_gil as m3
79126

80-
assert m.internals_at() == m3.internals_at(), (
81-
"internals should be the same within the main interpreter"
82-
)
127+
@pytest.mark.skipif(
128+
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
129+
)
130+
@pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+")
131+
def test_independent_subinterpreters_modern():
132+
"""Makes sure the internals object differs across independent subinterpreters. Modern (3.14+) syntax."""
133+
134+
sys.path.append(".")
135+
136+
m = pytest.importorskip("mod_per_interpreter_gil")
137+
138+
if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT:
139+
pytest.skip("Does not have subinterpreter support compiled in")
140+
141+
from concurrent import interpreters
142+
143+
code = """
144+
import mod_per_interpreter_gil as m
145+
146+
values.put_nowait(m.internals_at())
147+
"""
148+
149+
with contextlib.closing(interpreters.create()) as interp1, contextlib.closing(
150+
interpreters.create()
151+
) as interp2:
152+
with pytest.raises(
153+
interpreters.ExecutionFailed,
154+
match="does not support loading in subinterpreters",
155+
):
156+
interp1.exec("import mod_shared_interpreter_gil")
157+
158+
values = interpreters.create_queue()
159+
interp1.prepare_main(values=values)
160+
interp1.exec(code)
161+
res1 = values.get_nowait()
162+
163+
interp2.prepare_main(values=values)
164+
interp2.exec(code)
165+
res2 = values.get_nowait()
166+
167+
assert res1 != m.internals_at(), "internals should differ from main interpreter"
168+
assert res2 != m.internals_at(), "internals should differ from main interpreter"
169+
assert res1 != res2, "internals should differ between interpreters"
83170

84171

85172
@pytest.mark.skipif(
@@ -90,14 +177,7 @@ def test_dependent_subinterpreters():
90177

91178
sys.path.append(".")
92179

93-
if sys.version_info >= (3, 15):
94-
import interpreters
95-
elif sys.version_info >= (3, 13):
96-
import _interpreters as interpreters
97-
elif sys.version_info >= (3, 12):
98-
import _xxsubinterpreters as interpreters
99-
else:
100-
pytest.skip("Test requires the interpreters stdlib module")
180+
run_string, create = get_interpreters(modern=False)
101181

102182
m = pytest.importorskip("mod_shared_interpreter_gil")
103183

@@ -111,31 +191,10 @@ def test_dependent_subinterpreters():
111191
pickle.dump(m.internals_at(), f)
112192
"""
113193

114-
try:
115-
interp1 = interpreters.create("legacy")
116-
except TypeError:
117-
pytest.skip("interpreters module needs to support legacy config")
118-
119-
try:
194+
with create("legacy") as interp1:
120195
pipei, pipeo = os.pipe()
121-
interpreters.run_string(interp1, code, shared={"pipeo": pipeo})
196+
run_string(interp1, code, shared={"pipeo": pipeo})
122197
with open(pipei, "rb") as f:
123198
res1 = pickle.load(f)
124199

125-
# do this while the other interpreter is active
126-
import mod_shared_interpreter_gil as m2
127-
128-
assert m.internals_at() == m2.internals_at(), (
129-
"internals should be the same within the main interpreter"
130-
)
131-
finally:
132-
interpreters.destroy(interp1)
133-
134200
assert res1 != m.internals_at(), "internals should differ from main interpreter"
135-
136-
# do this after the other interpreters are destroyed and only one remains
137-
import mod_shared_interpreter_gil as m3
138-
139-
assert m.internals_at() == m3.internals_at(), (
140-
"internals should be the same within the main interpreter"
141-
)

0 commit comments

Comments
 (0)