blob: 627ccc591d034a6e0d958629944f35af243bef81 [file] [log] [blame]
from __future__ import annotations
import contextlib
import os
import pickle
import sys
import pytest
# 3.14.0b3+, though sys.implementation.supports_isolated_interpreters is being added in b4
# Can be simplified when we drop support for the first three betas
CONCURRENT_INTERPRETERS_SUPPORT = (
sys.version_info >= (3, 14)
and (
sys.version_info != (3, 14, 0, "beta", 1)
and sys.version_info != (3, 14, 0, "beta", 2)
)
and (
sys.version_info == (3, 14, 0, "beta", 3)
or sys.implementation.supports_isolated_interpreters
)
)
def get_interpreters(*, modern: bool):
if modern and CONCURRENT_INTERPRETERS_SUPPORT:
from concurrent import interpreters
def create():
return contextlib.closing(interpreters.create())
def run_string(
interp: interpreters.Interpreter,
code: str,
*,
shared: dict[str, object] | None = None,
) -> Exception | None:
if shared:
interp.prepare_main(**shared)
try:
interp.exec(code)
return None
except interpreters.ExecutionFailed as err:
return err
return run_string, create
if sys.version_info >= (3, 12):
interpreters = pytest.importorskip(
"_interpreters" if sys.version_info >= (3, 13) else "_xxsubinterpreters"
)
@contextlib.contextmanager
def create(config: str = ""):
try:
if config:
interp = interpreters.create(config)
else:
interp = interpreters.create()
except TypeError:
pytest.skip(f"interpreters module needs to support {config} config")
try:
yield interp
finally:
interpreters.destroy(interp)
def run_string(
interp: int, code: str, shared: dict[str, object] | None = None
) -> Exception | None:
kwargs = {"shared": shared} if shared else {}
return interpreters.run_string(interp, code, **kwargs)
return run_string, create
pytest.skip("Test requires the interpreters stdlib module")
@pytest.mark.skipif(
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
)
def test_independent_subinterpreters():
"""Makes sure the internals object differs across independent subinterpreters"""
sys.path.append(".")
run_string, create = get_interpreters(modern=True)
m = pytest.importorskip("mod_per_interpreter_gil")
if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT:
pytest.skip("Does not have subinterpreter support compiled in")
code = """
import mod_per_interpreter_gil as m
import pickle
with open(pipeo, 'wb') as f:
pickle.dump(m.internals_at(), f)
"""
with create() as interp1, create() as interp2:
try:
res0 = run_string(interp1, "import mod_shared_interpreter_gil")
if res0 is not None:
res0 = str(res0)
except Exception as e:
res0 = str(e)
pipei, pipeo = os.pipe()
run_string(interp1, code, shared={"pipeo": pipeo})
with open(pipei, "rb") as f:
res1 = pickle.load(f)
pipei, pipeo = os.pipe()
run_string(interp2, code, shared={"pipeo": pipeo})
with open(pipei, "rb") as f:
res2 = pickle.load(f)
assert "does not support loading in subinterpreters" in res0, (
"cannot use shared_gil in a default subinterpreter"
)
assert res1 != m.internals_at(), "internals should differ from main interpreter"
assert res2 != m.internals_at(), "internals should differ from main interpreter"
assert res1 != res2, "internals should differ between interpreters"
@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_independent_subinterpreters_modern():
"""Makes sure the internals object differs across independent subinterpreters. Modern (3.14+) syntax."""
sys.path.append(".")
m = pytest.importorskip("mod_per_interpreter_gil")
if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT:
pytest.skip("Does not have subinterpreter support compiled in")
from concurrent import interpreters
code = """
import mod_per_interpreter_gil as m
values.put_nowait(m.internals_at())
"""
with contextlib.closing(interpreters.create()) as interp1, contextlib.closing(
interpreters.create()
) as interp2:
with pytest.raises(
interpreters.ExecutionFailed,
match="does not support loading in subinterpreters",
):
interp1.exec("import mod_shared_interpreter_gil")
values = interpreters.create_queue()
interp1.prepare_main(values=values)
interp1.exec(code)
res1 = values.get_nowait()
interp2.prepare_main(values=values)
interp2.exec(code)
res2 = values.get_nowait()
assert res1 != m.internals_at(), "internals should differ from main interpreter"
assert res2 != m.internals_at(), "internals should differ from main interpreter"
assert res1 != res2, "internals should differ between interpreters"
@pytest.mark.skipif(
sys.platform.startswith("emscripten"), reason="Requires loadable modules"
)
def test_dependent_subinterpreters():
"""Makes sure the internals object differs across subinterpreters"""
sys.path.append(".")
run_string, create = get_interpreters(modern=False)
m = pytest.importorskip("mod_shared_interpreter_gil")
if not m.defined_PYBIND11_HAS_SUBINTERPRETER_SUPPORT:
pytest.skip("Does not have subinterpreter support compiled in")
code = """
import mod_shared_interpreter_gil as m
import pickle
with open(pipeo, 'wb') as f:
pickle.dump(m.internals_at(), f)
"""
with create("legacy") as interp1:
pipei, pipeo = os.pipe()
run_string(interp1, code, shared={"pipeo": pipeo})
with open(pipei, "rb") as f:
res1 = pickle.load(f)
assert res1 != m.internals_at(), "internals should differ from main interpreter"