| 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" |