Skip to content

[3.12] gh-109853: Fix sys.path[0] For Subinterpreters (gh-109994) #110701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52,966 changes: 26,489 additions & 26,477 deletions Doc/data/python3.12.abi

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Include/internal/pycore_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ typedef struct pyruntimestate {
/* All the objects that are shared by the runtime's interpreters. */
struct _Py_static_objects static_objects;

/* The value to use for sys.path[0] in new subinterpreters.
Normally this would be part of the PyConfig struct. However,
we cannot add it there in 3.12 since that's an ABI change. */
wchar_t *sys_path_0;

/* The following fields are here to avoid allocation during init.
The data is exposed through _PyRuntimeState pointer fields.
These fields should not be accessed directly outside of init.
Expand Down
151 changes: 151 additions & 0 deletions Lib/test/test_interpreters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import contextlib
import json
import os
import os.path
import sys
import threading
from textwrap import dedent
import unittest
Expand All @@ -8,6 +11,7 @@
from test import support
from test.support import import_helper
from test.support import threading_helper
from test.support import os_helper
_interpreters = import_helper.import_module('_xxsubinterpreters')
_channels = import_helper.import_module('_xxinterpchannels')
from test.support import interpreters
Expand Down Expand Up @@ -488,6 +492,153 @@ def task():
pass


class StartupTests(TestBase):

# We want to ensure the initial state of subinterpreters
# matches expectations.

_subtest_count = 0

@contextlib.contextmanager
def subTest(self, *args):
with super().subTest(*args) as ctx:
self._subtest_count += 1
try:
yield ctx
finally:
if self._debugged_in_subtest:
if self._subtest_count == 1:
# The first subtest adds a leading newline, so we
# compensate here by not printing a trailing newline.
print('### end subtest debug ###', end='')
else:
print('### end subtest debug ###')
self._debugged_in_subtest = False

def debug(self, msg, *, header=None):
if header:
self._debug(f'--- {header} ---')
if msg:
if msg.endswith(os.linesep):
self._debug(msg[:-len(os.linesep)])
else:
self._debug(msg)
self._debug('<no newline>')
self._debug('------')
else:
self._debug(msg)

_debugged = False
_debugged_in_subtest = False
def _debug(self, msg):
if not self._debugged:
print()
self._debugged = True
if self._subtest is not None:
if True:
if not self._debugged_in_subtest:
self._debugged_in_subtest = True
print('### start subtest debug ###')
print(msg)
else:
print(msg)

def create_temp_dir(self):
import tempfile
tmp = tempfile.mkdtemp(prefix='test_interpreters_')
tmp = os.path.realpath(tmp)
self.addCleanup(os_helper.rmtree, tmp)
return tmp

def write_script(self, *path, text):
filename = os.path.join(*path)
dirname = os.path.dirname(filename)
if dirname:
os.makedirs(dirname, exist_ok=True)
with open(filename, 'w', encoding='utf-8') as outfile:
outfile.write(dedent(text))
return filename

@support.requires_subprocess()
def run_python(self, argv, *, cwd=None):
# This method is inspired by
# EmbeddingTestsMixin.run_embedded_interpreter() in test_embed.py.
import shlex
import subprocess
if isinstance(argv, str):
argv = shlex.split(argv)
argv = [sys.executable, *argv]
try:
proc = subprocess.run(
argv,
cwd=cwd,
capture_output=True,
text=True,
)
except Exception as exc:
self.debug(f'# cmd: {shlex.join(argv)}')
if isinstance(exc, FileNotFoundError) and not exc.filename:
if os.path.exists(argv[0]):
exists = 'exists'
else:
exists = 'does not exist'
self.debug(f'{argv[0]} {exists}')
raise # re-raise
assert proc.stderr == '' or proc.returncode != 0, proc.stderr
if proc.returncode != 0 and support.verbose:
self.debug(f'# python3 {shlex.join(argv[1:])} failed:')
self.debug(proc.stdout, header='stdout')
self.debug(proc.stderr, header='stderr')
self.assertEqual(proc.returncode, 0)
self.assertEqual(proc.stderr, '')
return proc.stdout

def test_sys_path_0(self):
# The main interpreter's sys.path[0] should be used by subinterpreters.
script = '''
import sys
from test.support import interpreters

orig = sys.path[0]

interp = interpreters.create()
interp.run(f"""if True:
import json
import sys
print(json.dumps({{
'main': {orig!r},
'sub': sys.path[0],
}}, indent=4), flush=True)
""")
'''
# <tmp>/
# pkg/
# __init__.py
# __main__.py
# script.py
# script.py
cwd = self.create_temp_dir()
self.write_script(cwd, 'pkg', '__init__.py', text='')
self.write_script(cwd, 'pkg', '__main__.py', text=script)
self.write_script(cwd, 'pkg', 'script.py', text=script)
self.write_script(cwd, 'script.py', text=script)

cases = [
('script.py', cwd),
('-m script', cwd),
('-m pkg', cwd),
('-m pkg.script', cwd),
('-c "import script"', ''),
]
for argv, expected in cases:
with self.subTest(f'python3 {argv}'):
out = self.run_python(argv, cwd=cwd)
data = json.loads(out)
sp0_main, sp0_sub = data['main'], data['sub']
self.assertEqual(sp0_sub, sp0_main)
self.assertEqual(sp0_sub, expected)


class TestIsShareable(TestBase):

def test_default_shareables(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``sys.path[0]`` is now set correctly for subinterpreters.
36 changes: 26 additions & 10 deletions Modules/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@ pymain_run_python(int *exitcode)
goto error;
}

assert(interp->runtime->sys_path_0 == NULL);

if (config->run_filename != NULL) {
/* If filename is a package (ex: directory or ZIP file) which contains
__main__.py, main_importer_path is set to filename and will be
Expand All @@ -574,24 +576,38 @@ pymain_run_python(int *exitcode)
// import readline and rlcompleter before script dir is added to sys.path
pymain_import_readline(config);

PyObject *path0 = NULL;
if (main_importer_path != NULL) {
if (pymain_sys_path_add_path0(interp, main_importer_path) < 0) {
goto error;
}
path0 = Py_NewRef(main_importer_path);
}
else if (!config->safe_path) {
PyObject *path0 = NULL;
int res = _PyPathConfig_ComputeSysPath0(&config->argv, &path0);
if (res < 0) {
goto error;
}

if (res > 0) {
if (pymain_sys_path_add_path0(interp, path0) < 0) {
Py_DECREF(path0);
goto error;
}
else if (res == 0) {
Py_CLEAR(path0);
}
}
if (path0 != NULL) {
wchar_t *wstr = PyUnicode_AsWideCharString(path0, NULL);
if (wstr == NULL) {
Py_DECREF(path0);
goto error;
}
PyMemAllocatorEx old_alloc;
_PyMem_SetDefaultAllocator(PYMEM_DOMAIN_RAW, &old_alloc);
interp->runtime->sys_path_0 = _PyMem_RawWcsdup(wstr);
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc);
PyMem_Free(wstr);
if (interp->runtime->sys_path_0 == NULL) {
Py_DECREF(path0);
goto error;
}
int res = pymain_sys_path_add_path0(interp, path0);
Py_DECREF(path0);
if (res < 0) {
goto error;
}
}

Expand Down
26 changes: 26 additions & 0 deletions Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,32 @@ init_interp_main(PyThreadState *tstate)
#endif
}

if (!is_main_interp) {
// The main interpreter is handled in Py_Main(), for now.
wchar_t *sys_path_0 = interp->runtime->sys_path_0;
if (sys_path_0 != NULL) {
PyObject *path0 = PyUnicode_FromWideChar(sys_path_0, -1);
if (path0 == NULL) {
return _PyStatus_ERR("can't initialize sys.path[0]");
}
PyObject *sysdict = interp->sysdict;
if (sysdict == NULL) {
Py_DECREF(path0);
return _PyStatus_ERR("can't initialize sys.path[0]");
}
PyObject *sys_path = PyDict_GetItemWithError(sysdict, &_Py_ID(path));
if (sys_path == NULL) {
Py_DECREF(path0);
return _PyStatus_ERR("can't initialize sys.path[0]");
}
int res = PyList_Insert(sys_path, 0, path0);
Py_DECREF(path0);
if (res) {
return _PyStatus_ERR("can't initialize sys.path[0]");
}
}
}

assert(!_PyErr_Occurred(tstate));

return _PyStatus_OK();
Expand Down
4 changes: 4 additions & 0 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,10 @@ _PyRuntimeState_Fini(_PyRuntimeState *runtime)
}

#undef FREE_LOCK
if (runtime->sys_path_0 != NULL) {
PyMem_RawFree(runtime->sys_path_0);
runtime->sys_path_0 = NULL;
}
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc);
}

Expand Down