11from __future__ import annotations
22
3+ import contextlib
34import os
45import pickle
56import sys
67
78import 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